blob: ce0c926e851bcd71faee2c0ea9e8750dc6a6e902 [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"
kkostiukcf0757f2021-03-26 12:29:47 -040037#import "views/TextViewWithPlaceholder.h"
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040038#import <QuickLook/QuickLook.h>
39#import <Quartz/Quartz.h>
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040040#import <AVFoundation/AVFoundation.h>
41
42#import "RecordFileVC.h"
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040043
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040044
kkostiuk26f99fb2021-02-17 15:06:56 -050045@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource, QLPreviewPanelDataSource, NSTextViewDelegate> {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040046
Anthony Léonard2382b562017-12-13 15:51:28 -050047 __unsafe_unretained IBOutlet NSTableView* conversationView;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040048 __unsafe_unretained IBOutlet NSView* containerView;
kkostiukcf0757f2021-03-26 12:29:47 -040049 __unsafe_unretained IBOutlet TextViewWithPlaceholder* messageView;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050050 __unsafe_unretained IBOutlet IconButton *sendFileButton;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040051 __unsafe_unretained IBOutlet IconButton *recordVideoButton;
52 __unsafe_unretained IBOutlet IconButton *recordAudioButton;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050053 __unsafe_unretained IBOutlet NSLayoutConstraint* sendPanelHeight;
kkostiuk26f99fb2021-02-17 15:06:56 -050054 __unsafe_unretained IBOutlet NSLayoutConstraint* messageHeight;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050055 __unsafe_unretained IBOutlet NSLayoutConstraint* messagesBottomMargin;
kkostiuk26f99fb2021-02-17 15:06:56 -050056 __unsafe_unretained IBOutlet NSLayoutConstraint* textBottomConstraint;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040057 IBOutlet NSPopover *recordMessagePopover;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040058
Kateryna Kostiukc867eb92020-03-08 13:15:17 -040059 QString convUid_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050060 lrc::api::ConversationModel* convModel_;
Anthony Léonard2382b562017-12-13 15:51:28 -050061 const lrc::api::conversation::Info* cachedConv_;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040062 lrc::api::AVModel* avModel;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050063 QMetaObject::Connection newInteractionSignal_;
Anthony Léonard2382b562017-12-13 15:51:28 -050064
65 // Both are needed to invalidate cached conversation as pointer
66 // may not be referencing the same conversation anymore
67 QMetaObject::Connection modelSortedSignal_;
68 QMetaObject::Connection filterChangedSignal_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050069 QMetaObject::Connection interactionStatusUpdatedSignal_;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -040070 QMetaObject::Connection peerComposingMsgSignal_;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -040071 QMetaObject::Connection lastDisplayedChanged_;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040072 NSString* previewImage;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050073 NSMutableDictionary *pendingMessagesToSend;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040074 RecordFileVC * recordingController;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040075}
76
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040077@end
78
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050079// Tags for view
80NSInteger const GENERIC_INT_TEXT_TAG = 100;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040081NSInteger const GENERIC_INT_TIME_TAG = 200;
82
83// views size
84CGFloat const GENERIC_CELL_HEIGHT = 60;
85CGFloat const TIME_BOX_HEIGHT = 34;
86CGFloat const MESSAGE_TEXT_PADDING = 10;
87CGFloat const MAX_TRANSFERED_IMAGE_SIZE = 250;
88CGFloat const BUBBLE_HEIGHT_FOR_TRANSFERED_FILE = 87;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -040089CGFloat const DEFAULT_ROW_HEIGHT = 10;
90CGFloat const HEIGHT_FOR_COMPOSING_INDICATOR = 46;
Kateryna Kostiuka1201922020-04-20 11:59:35 -040091CGFloat const HEIGHT_DEFAULT = 34;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050092NSInteger const SEND_PANEL_DEFAULT_HEIGHT = 60;
kkostiuk26f99fb2021-02-17 15:06:56 -050093NSInteger const SEND_PANEL_MAX_HEIGHT = 167;
94NSInteger const SEND_PANEL_BOTTOM_MARGIN = 13;
kkostiuk153c2692021-02-23 21:30:23 -050095NSInteger MESSAGE_VIEW_DEFAULT_HEIGHT = 17;
kkostiuk26f99fb2021-02-17 15:06:56 -050096NSInteger const BOTTOM_MARGIN = 8;
97NSInteger const BOTTOM_MARGIN_MIN = 0;
98NSInteger const TOP_MARGIN = 20;
99NSInteger const TOP_MARGIN_MIN = 13;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500100
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400101BOOL peerComposingMessage = false;
102BOOL composingMessage = false;
103
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400104@implementation MessagesVC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400105
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400106
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400107//MessageBuble type
108typedef NS_ENUM(NSInteger, MessageSequencing) {
109 SINGLE_WITH_TIME = 0,
110 SINGLE_WITHOUT_TIME = 1,
111 FIRST_WITH_TIME = 2,
112 FIRST_WITHOUT_TIME = 3,
113 MIDDLE_IN_SEQUENCE = 5,
114 LAST_IN_SEQUENCE = 6,
115};
116
117- (void)awakeFromNib
118{
119 NSNib *cellNib = [[NSNib alloc] initWithNibNamed:@"MessageCells" bundle:nil];
120 [conversationView registerNib:cellNib forIdentifier:@"LeftIncomingFileView"];
121 [conversationView registerNib:cellNib forIdentifier:@"LeftOngoingFileView"];
122 [conversationView registerNib:cellNib forIdentifier:@"LeftFinishedFileView"];
123 [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"];
124 [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400125 [conversationView registerNib:cellNib forIdentifier:@"PeerComposingMsgView"];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500126 [[conversationView.enclosingScrollView contentView] setCopiesOnScroll:NO];
kkostiuk26f99fb2021-02-17 15:06:56 -0500127 [messageView setFont: [NSFont systemFontOfSize: 14 weight: NSFontWeightLight]];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500128 [conversationView setWantsLayer:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400129}
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500130
131- (instancetype)initWithCoder:(NSCoder *)coder
132{
133 self = [super initWithCoder:coder];
134 if (self) {
135 pendingMessagesToSend = [[NSMutableDictionary alloc] init];
136 }
137 return self;
138}
139
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -0400140-(void) setAVModel: (lrc::api::AVModel*) avmodel {
141 avModel = avmodel;
142 if (recordingController == nil) {
143 recordingController = [[RecordFileVC alloc] initWithNibName:@"RecordFileVC" bundle:nil avModel: self->avModel];
144 recordingController.delegate = self;
145 }
146}
147
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500148- (void)setMessage:(NSString *)newValue {
149 _message = [newValue removeEmptyLinesAtBorders];
150}
151
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400152-(void) clearData {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400153 if (!convUid_.isEmpty()) {
kkostiuk26f99fb2021-02-17 15:06:56 -0500154 pendingMessagesToSend[convUid_.toNSString()] = self.message;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500155 }
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400156 cachedConv_ = nil;
157 convUid_ = "";
158 convModel_ = nil;
159
160 QObject::disconnect(modelSortedSignal_);
161 QObject::disconnect(filterChangedSignal_);
162 QObject::disconnect(interactionStatusUpdatedSignal_);
163 QObject::disconnect(newInteractionSignal_);
Kateryna Kostiuk5acaefd2020-03-25 11:14:25 -0400164 QObject::disconnect(peerComposingMsgSignal_);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400165 QObject::disconnect(lastDisplayedChanged_);
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -0400166 [self closeRecordingView];
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400167}
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400168
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500169-(void) scrollToBottom {
170 CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
171 NSRange range = [conversationView rowsInRect:visibleRect];
172 NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
173 NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400174 NSInteger numberOfRows = [conversationView numberOfRows];
175 if ((numberOfRows > 0) &&
176 lastvisibleRow > (numberOfRows - 5)) {
kkostiukd83b4742021-03-10 14:29:37 -0500177 [conversationView scrollToEndOfDocument: nil];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500178 }
179}
180
Anthony Léonard2382b562017-12-13 15:51:28 -0500181-(const lrc::api::conversation::Info*) getCurrentConversation
182{
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400183 if (convModel_ == nil || convUid_.isEmpty())
Anthony Léonard2382b562017-12-13 15:51:28 -0500184 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400185
Anthony Léonard2382b562017-12-13 15:51:28 -0500186 if (cachedConv_ != nil)
187 return cachedConv_;
Kateryna Kostiuka7b909c2020-10-19 11:46:26 -0400188 auto convOpt = getConversationFromUid(convUid_, *convModel_);
189 if (convOpt.has_value()) {
kkostiukf81c6372021-01-11 18:51:28 -0500190 lrc::api::conversation::Info& conversation = *convOpt;
Kateryna Kostiuka7b909c2020-10-19 11:46:26 -0400191 cachedConv_ = &conversation;
Kateryna Kostiuk3541ae22020-08-17 12:26:14 -0400192 }
Anthony Léonard2382b562017-12-13 15:51:28 -0500193 return cachedConv_;
194}
195
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400196-(void) reloadConversationForMessage:(uint64_t) uid updateSize:(BOOL) update {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400197 auto* conv = [self getCurrentConversation];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400198 if (conv == nil)
199 return;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400200 auto it = conv->interactions.find(uid);
201 if (it == conv->interactions.end()) {
202 return;
203 }
204 auto itIndex = distance(conv->interactions.begin(),it);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400205 if (itIndex >= ([conversationView numberOfRows] - 1) || itIndex >= conv->interactions.size()) {
Kateryna Kostiuka1201922020-04-20 11:59:35 -0400206 return;
207 }
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400208 NSRange rangeToUpdate = NSMakeRange(itIndex, 2);
209 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndexesInRange:rangeToUpdate];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400210 //reload previous message to update bubbleview
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400211 if (itIndex > 0) {
212 auto previousIt = it;
213 previousIt--;
214 auto previousInteraction = previousIt->second;
215 if (previousInteraction.type == lrc::api::interaction::Type::TEXT) {
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400216 NSRange range = NSMakeRange(itIndex - 1, 3);
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400217 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
218 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400219 }
220 if (update) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400221 NSRange insertRange = NSMakeRange(itIndex, 1);
222 NSIndexSet* insertRangeSet = [NSIndexSet indexSetWithIndexesInRange:insertRange];
223 [conversationView removeRowsAtIndexes:insertRangeSet withAnimation:(NSTableViewAnimationEffectNone)];
224 [conversationView insertRowsAtIndexes:insertRangeSet withAnimation:(NSTableViewAnimationEffectNone)];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400225 }
226 [conversationView reloadDataForRowIndexes: indexSet
227 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400228 [self scrollToBottom];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400229}
230
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400231-(void)setConversationUid:(const QString&)convUid model:(lrc::api::ConversationModel *)model
Anthony Léonard2382b562017-12-13 15:51:28 -0500232{
233 if (convUid_ == convUid && convModel_ == model)
234 return;
235
236 cachedConv_ = nil;
237 convUid_ = convUid;
238 convModel_ = model;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400239 peerComposingMessage = false;
240 composingMessage = false;
Anthony Léonard2382b562017-12-13 15:51:28 -0500241
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500242 // Signal triggered when messages are received or their status updated
243 QObject::disconnect(newInteractionSignal_);
244 QObject::disconnect(interactionStatusUpdatedSignal_);
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400245 QObject::disconnect(peerComposingMsgSignal_);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400246 QObject::disconnect(lastDisplayedChanged_);
247 lastDisplayedChanged_ =
248 QObject::connect(convModel_,
249 &lrc::api::ConversationModel::displayedInteractionChanged,
250 [self](const QString &uid,
251 const QString &participantURI,
252 const uint64_t &previousUid,
253 const uint64_t &newdUid) {
254 if (uid != convUid_)
255 return;
256 [self reloadConversationForMessage:newdUid updateSize: NO];
257 [self reloadConversationForMessage:previousUid updateSize: NO];
258 });
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400259
260 peerComposingMsgSignal_ = QObject::connect(convModel_,
261 &lrc::api::ConversationModel::composingStatusChanged,
262 [self](const QString &uid,
263 const QString &contactUri,
264 bool isComposing) {
265 if (uid != convUid_)
266 return;
267 bool shouldUpdate = isComposing != peerComposingMessage;
268 if (!shouldUpdate) {
269 return;
270 }
271 // reload and update height for composing indicator
272 peerComposingMessage = isComposing;
273 auto* conv = [self getCurrentConversation];
274 if (conv == nil)
275 return;
276 auto row = [conversationView numberOfRows] - 1;
277 if (row < 0) {
278 return;
279 }
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400280 if(peerComposingMessage) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400281 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:row];
282 [conversationView reloadDataForRowIndexes: indexSet
283 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400284 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400285 [self scrollToBottom];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400286 } else {
287 //whait for possible incoming message to avoid view jumping
288 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400289 auto row = [conversationView numberOfRows] - 1;
290 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:row];
291 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
292 [conversationView reloadDataForRowIndexes: indexSet
293 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
294 [self scrollToBottom];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400295 });
296 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400297 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500298 newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400299 [self](const QString& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400300 if (uid != convUid_)
301 return;
302 cachedConv_ = nil;
303 peerComposingMessage = false;
304 [conversationView noteNumberOfRowsChanged];
305 [self reloadConversationForMessage:interactionId updateSize: YES];
306 [self scrollToBottom];
307 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500308 interactionStatusUpdatedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400309 [self](const QString& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400310 if (uid != convUid_)
311 return;
312 cachedConv_ = nil;
313 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
314 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
315 convModel_->refreshFilter();
316 }
317 [self reloadConversationForMessage:interactionId updateSize: interaction.type == lrc::api::interaction::Type::DATA_TRANSFER];
318 [self scrollToBottom];
319 });
Anthony Léonard2382b562017-12-13 15:51:28 -0500320
321 // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
322 // after a reordering.
323 QObject::disconnect(modelSortedSignal_);
324 QObject::disconnect(filterChangedSignal_);
Kateryna Kostiuka7b909c2020-10-19 11:46:26 -0400325 modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelChanged,
Anthony Léonard2382b562017-12-13 15:51:28 -0500326 [self](){
327 cachedConv_ = nil;
328 });
329 filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
330 [self](){
331 cachedConv_ = nil;
332 });
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400333 if (pendingMessagesToSend[convUid_.toNSString()]) {
kkostiuk26f99fb2021-02-17 15:06:56 -0500334 NSString *mess = pendingMessagesToSend[convUid_.toNSString()];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400335 self.message = pendingMessagesToSend[convUid_.toNSString()];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500336 [self updateSendMessageHeight];
337 } else {
338 self.message = @"";
kkostiuk26f99fb2021-02-17 15:06:56 -0500339 [self resetSendMessagePanelToDefaultSize];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500340 }
kkostiuke6c33db2021-02-24 16:20:50 -0500341 dispatch_async(dispatch_get_main_queue(), ^{
342 [messageView.window makeFirstResponder: messageView];
343 });
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500344 conversationView.alphaValue = 0.0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500345 [conversationView reloadData];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400346 [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500347 CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"];
348 fadeIn.fromValue = [NSNumber numberWithFloat:0.0];
349 fadeIn.toValue = [NSNumber numberWithFloat:1.0];
350 fadeIn.duration = 0.4f;
351
352 [conversationView.layer addAnimation:fadeIn forKey:fadeIn.keyPath];
353 conversationView.alphaValue = 1;
354 auto* conv = [self getCurrentConversation];
355
356 if (conv == nil)
357 return;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400358 try {
359 [sendFileButton setEnabled:(convModel_->owner.contactModel->getContact(conv->participants[0]).profileInfo.type != lrc::api::profile::Type::SIP)];
360 } catch (std::out_of_range& e) {
361 NSLog(@"contact out of range");
362 }
kkostiukcf0757f2021-03-26 12:29:47 -0400363 NSString* name = bestNameForConversation(*conv, *convModel_);
364 NSString *placeholder = [NSString stringWithFormat:@"%@%@", @"Write to ", name];
365
366 NSFont *fontName = [NSFont systemFontOfSize: 14.0 weight: NSFontWeightRegular];
367 NSColor *color = [NSColor tertiaryLabelColor];
368 NSDictionary *nameAttrs = [NSDictionary dictionaryWithObjectsAndKeys:
369 fontName, NSFontAttributeName,
370 color, NSForegroundColorAttributeName,
371 nil];
372 NSAttributedString* attributedPlaceholder = [[NSAttributedString alloc] initWithString: placeholder attributes:nameAttrs];
373 messageView.placeholderAttributedString = attributedPlaceholder;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400374}
375
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400376#pragma mark - configure cells
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400377
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400378-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500379{
380 NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
381 NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400382 NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500383
384 // TODO: Fix symbol in LRC
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500385 NSString* fixedString = [[text stringByReplacingOccurrencesOfString:@"🕽" withString:@""] stringByReplacingOccurrencesOfString:@"📞" withString:@""];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500386 [textField setStringValue:fixedString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400387 [timeField setStringValue:time];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500388
389 return result;
390}
391
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400392-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500393{
394 IMTableCellView* result;
395
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400396 auto type = interaction.type;
397 auto status = interaction.status;
398
399 NSString* fileName = @"incoming file";
400
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500401 // First, view is created
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400402 if (!interaction.authorUri.isEmpty()) {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500403 switch (status) {
404 case lrc::api::interaction::Status::TRANSFER_CREATED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400405 case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
406 result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
407 [result.acceptButton setAction:@selector(acceptIncomingFile:)];
408 [result.acceptButton setTarget:self];
409 [result.declineButton setAction:@selector(declineIncomingFile:)];
410 [result.declineButton setTarget:self];
411 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500412 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400413 case lrc::api::interaction::Status::TRANSFER_ONGOING: {
414 result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
415 [result.progressIndicator startAnimation:conversationView];
416 [result.declineButton setAction:@selector(declineIncomingFile:)];
417 [result.declineButton setTarget:self];
418 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500419 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400420 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
421 [result.transferedFileName setAction:@selector(imagePreview:)];
422 [result.transferedFileName setTarget:self];
423 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
424 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500425 case lrc::api::interaction::Status::TRANSFER_CANCELED:
426 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400427 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
428 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500429 }
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400430 } else {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400431 NSString* fileName = @"sent file";
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500432 switch (status) {
433 case lrc::api::interaction::Status::TRANSFER_CREATED:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500434 case lrc::api::interaction::Status::TRANSFER_ONGOING:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400435 case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500436 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400437 result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500438 [result.progressIndicator startAnimation:nil];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400439 [result.declineButton setAction:@selector(declineIncomingFile:)];
440 [result.declineButton setTarget:self];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500441 break;
442 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400443 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
444 [result.transferedFileName setAction:@selector(imagePreview:)];
445 [result.transferedFileName setTarget:self];
446 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
447 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500448 case lrc::api::interaction::Status::TRANSFER_CANCELED:
449 case lrc::api::interaction::Status::TRANSFER_ERROR:
Olivier Soldanoe521a182018-02-26 16:55:19 -0500450 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400451 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500452 }
453 }
454
455 // Then status label is updated if needed
456 switch (status) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400457 [result.statusLabel setTextColor:[NSColor textColor]];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500458 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400459 [result.statusLabel setTextColor:[NSColor greenSuccessColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500460 [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500461 break;
462 case lrc::api::interaction::Status::TRANSFER_CANCELED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400463 [result.statusLabel setTextColor:[NSColor orangeColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500464 [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500465 break;
466 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400467 [result.statusLabel setTextColor:[NSColor errorTransferColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500468 [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500469 break;
470 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400471 [result.statusLabel setTextColor:[NSColor textColor]];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500472 [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
473 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500474 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400475 result.transferedImage.image = nil;
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400476 [result.openImagebutton setHidden:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400477 [result.msgBackground setHidden:NO];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400478 NSString* name = interaction.body.toNSString();
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400479 if (name.length > 0) {
Kateryna Kostiuk67735232018-05-10 15:05:32 -0400480 fileName = [name lastPathComponent];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400481 }
Kateryna Kostiukd508f752020-11-13 16:26:00 -0500482 NSFont *nameFont = [NSFont systemFontOfSize: 12 weight: NSFontWeightLight];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400483 NSColor *nameColor = [NSColor textColor];
484 NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
485 paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
486 paragraphStyle.alignment = NSTextAlignmentLeft;
487 NSDictionary *nameAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
488 nameColor,NSForegroundColorAttributeName,
489 paragraphStyle,NSParagraphStyleAttributeName, nil];
490 NSAttributedString* nameAttributedString = [[NSAttributedString alloc] initWithString:fileName attributes:nameAttr];
491 result.transferedFileName.attributedTitle = nameAttributedString;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400492 if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400493 NSColor *higlightColor = [NSColor grayColor];
494 NSDictionary *alternativeNametAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
495 higlightColor,NSForegroundColorAttributeName,
496 paragraphStyle,NSParagraphStyleAttributeName, nil];
497 NSAttributedString* alternativeString = [[NSAttributedString alloc] initWithString:fileName attributes:alternativeNametAttr];
498 result.transferedFileName.attributedAlternateTitle = alternativeString;
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400499 NSImage* image = [self getImageForFilePath:name];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400500 if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400501 image = [self getImageForFilePath:[self getDataTransferPath:interactionID]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400502 }
503 if(image != nil) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400504 result.transferedImage.image = image;
505 [result updateImageConstraintWithMax: MAX_TRANSFERED_IMAGE_SIZE];
506 [result.openImagebutton setAction:@selector(imagePreview:)];
507 [result.openImagebutton setTarget:self];
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400508 [result.openImagebutton setHidden:NO];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400509 }
510 }
511 [result setupForInteraction:interactionID];
512 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
513 NSString* timeString = [self timeForMessage: msgTime];
514 result.timeLabel.stringValue = timeString;
515 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
516 if (!isOutgoing) {
Kateryna Kostiukdc720842020-09-10 16:44:02 -0400517 @autoreleasepool {
518 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
519 auto* conv = [self getCurrentConversation];
520 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
521 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400522 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500523 return result;
524}
525
Anthony Léonard2382b562017-12-13 15:51:28 -0500526#pragma mark - NSTableViewDelegate methods
527- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400528{
529 return YES;
530}
531
Anthony Léonard2382b562017-12-13 15:51:28 -0500532- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400533{
Anthony Léonard2382b562017-12-13 15:51:28 -0500534 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400535}
536
Anthony Léonard2382b562017-12-13 15:51:28 -0500537- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400538{
Anthony Léonard2382b562017-12-13 15:51:28 -0500539 auto* conv = [self getCurrentConversation];
Anthony Léonard2382b562017-12-13 15:51:28 -0500540 if (conv == nil)
541 return nil;
542
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400543 IMTableCellView* result;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500544 auto it = conv->interactions.begin();
Kateryna Kostiuka1201922020-04-20 11:59:35 -0400545 auto size = [conversationView numberOfRows] - 1;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400546
547 if (row > size || row > conv->interactions.size()) {
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400548 return [[NSView alloc] init];
549 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400550
551 if (row == size) {
Kateryna Kostiukb39ca192020-03-27 14:01:00 -0400552 if (size < 1) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400553 return nil;
Kateryna Kostiukb39ca192020-03-27 14:01:00 -0400554 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400555 //last row peer composing view
556 result = [tableView makeViewWithIdentifier:@"PeerComposingMsgView" owner:conversationView];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400557 result.alphaValue = 0;
558 [result animateCompozingIndicator: NO];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400559 CGFloat alpha = peerComposingMessage ? 1 : 0;
560 CGFloat height = peerComposingMessage ? HEIGHT_FOR_COMPOSING_INDICATOR : DEFAULT_ROW_HEIGHT;
561 [result updateHeightConstraints: height];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400562 if (alpha == 1) {
563 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
564 if (peerComposingMessage) {
565 result.alphaValue = alpha;
566 [result animateCompozingIndicator: YES];
567 }
568 });
569 }
570 return result;
571 }
Anthony Léonard2382b562017-12-13 15:51:28 -0500572
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500573 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500574
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400575 if (it == conv->interactions.end()) {
576 return [[NSView alloc] init];
577 }
578
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400579 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500580 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
581
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500582 switch (interaction.type) {
583 case lrc::api::interaction::Type::TEXT:
584 if (isOutgoing) {
585 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
586 } else {
587 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
588 }
589 break;
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400590 case lrc::api::interaction::Type::DATA_TRANSFER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400591 return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500592 break;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500593 case lrc::api::interaction::Type::CONTACT:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400594 case lrc::api::interaction::Type::CALL: {
595 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
596 NSString* timeString = [self timeForMessage: msgTime];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400597 return [self makeGenericInteractionViewForTableView:tableView withText:interaction.body.toNSString() andTime:timeString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400598 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500599 default: // If interaction is not of a known type
600 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400601 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400602 MessageSequencing sequence = [self computeSequencingFor:row];
603 BubbleType type = SINGLE;
604 if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
605 type = FIRST;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400606 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400607 if (sequence == MIDDLE_IN_SEQUENCE) {
608 type = MIDDLE;
609 }
610 if (sequence == LAST_IN_SEQUENCE) {
611 type = LAST;
612 }
613 result.msgBackground.type = type;
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400614 bool sendingFail = false;
615 [result.messageStatus setHidden:YES];
616 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
617 if (interaction.status == lrc::api::interaction::Status::SENDING) {
618 [result.messageStatus setHidden:NO];
619 [result.sendingMessageIndicator startAnimation:nil];
620 [result.messageFailed setHidden:YES];
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400621 } else if (interaction.status == lrc::api::interaction::Status::FAILURE) {
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400622 [result.messageStatus setHidden:NO];
623 [result.sendingMessageIndicator setHidden:YES];
624 [result.messageFailed setHidden:NO];
625 sendingFail = true;
626 }
627 }
628 [result setupForInteraction:it->first isFailed: sendingFail];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400629 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400630 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400631 [result.msgBackground setNeedsDisplay:YES];
632 [result setNeedsDisplay:YES];
633 [result.timeBox setNeedsDisplay:YES];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400634
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400635 NSString *text = interaction.body.toNSString();
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400636 text = [text removeEmptyLinesAtBorders];
637
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400638 NSMutableAttributedString* msgAttString =
Kateryna Kostiuk5a41a8d2020-05-13 09:05:56 -0400639 [[NSMutableAttributedString alloc] initWithString:text
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400640 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400641
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400642 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400643
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400644 [result updateMessageConstraint:messageSize.width andHeight:messageSize.height timeIsVisible:shouldDisplayTime isTopPadding: shouldApplyPadding];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400645 [[result.msgView textStorage] appendAttributedString:msgAttString];
Anthony Léonard2382b562017-12-13 15:51:28 -0500646
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400647 NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
648 NSArray *matches = [linkDetector matchesInString:result.msgView.string options:0 range:NSMakeRange(0, result.msgView.string.length)];
649
650 [result.msgView.textStorage beginEditing];
651
652 for (NSTextCheckingResult *match in matches) {
653 if (!match.URL) continue;
654
655 NSDictionary *linkAttributes = @{
656 NSLinkAttributeName: match.URL,
657 };
658 [result.msgView.textStorage addAttributes:linkAttributes range:match.range];
659 }
660
661 [result.msgView.textStorage endEditing];
662
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400663 if (shouldDisplayTime) {
664 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
665 NSString* timeString = [self timeForMessage: msgTime];
666 result.timeLabel.stringValue = timeString;
Anthony Léonard64e19672018-01-18 16:40:34 -0500667 }
668
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400669 bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
670 && sequence != FIRST_WITH_TIME) ? YES : NO;
671 [result.photoView setHidden:!shouldDisplayAvatar];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400672 BOOL showIndicator = convModel_->isLastDisplayed(convUid_, it->first, conv->participants.front());
673 [result.readIndicator setHidden: !showIndicator];
Kateryna Kostiukdc720842020-09-10 16:44:02 -0400674 @autoreleasepool {
675 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
676 auto image = QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)));
677 [result.readIndicator setImage:image];
678 if (!isOutgoing && shouldDisplayAvatar) {
679 [result.photoView setImage:image];
680 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400681 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400682 return result;
683}
684
Anthony Léonard2382b562017-12-13 15:51:28 -0500685- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400686{
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400687 try {
688 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400689
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400690 auto* conv = [self getCurrentConversation];
Anthony Léonard2382b562017-12-13 15:51:28 -0500691
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400692 if (conv == nil)
693 return HEIGHT_DEFAULT;
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400694
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400695 auto size = [conversationView numberOfRows] - 1;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400696
697 if (row > size || row > conv->interactions.size()) {
698 return HEIGHT_DEFAULT;
699 }
700 if (row == size) {
701 return peerComposingMessage ? HEIGHT_FOR_COMPOSING_INDICATOR : DEFAULT_ROW_HEIGHT;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400702 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400703
704 auto it = conv->interactions.begin();
705
706 std::advance(it, row);
707
708 if (it == conv->interactions.end()) {
709 return HEIGHT_DEFAULT;
710 }
711
712 auto interaction = it->second;
713
714 MessageSequencing sequence = [self computeSequencingFor:row];
715
716 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
717
718 if(interaction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
719 if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
720 NSString* name = interaction.body.toNSString();
721 NSImage* image = [self getImageForFilePath:name];
722 if (([name rangeOfString:@"/"].location == NSNotFound)) {
723 image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
724 }
725 if (image != nil) {
726 CGFloat widthScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.width;
727 CGFloat heightScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.height;
728 CGFloat heigt = 0;
729 if((widthScaleFactor >= 1) && (heightScaleFactor >= 1)) {
730 heigt = image.size.height;
731 } else {
732 CGFloat scale = MIN(widthScaleFactor, heightScaleFactor);
733 heigt = image.size.height * scale;
734 }
735 return heigt + TIME_BOX_HEIGHT;
736 }
737 }
738 return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
739 }
740
741 if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
742 return GENERIC_CELL_HEIGHT;
743
744 NSString *text = interaction.body.toNSString();
745 text = [text removeEmptyLinesAtBorders];
746
747 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
748 CGFloat singleLignMessageHeight = 15;
749
750 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
751
752 if (shouldDisplayTime) {
753 return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
754 TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
755 }
756 if(shouldApplyPadding) {
757 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2 + 15,
758 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2 + 15);
759 }
760 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
761 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
762 } catch (std::out_of_range& e) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400763 return DEFAULT_ROW_HEIGHT;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400764 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400765}
766
767#pragma mark - message view parameters
768
769-(NSString *) getDataTransferPath:(uint64_t)interactionId {
770 lrc::api::datatransfer::Info info = {};
771 convModel_->getTransferInfo(interactionId, info);
772 double convertData = static_cast<double>(info.totalSize);
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400773 return info.path.toNSString();
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400774}
775
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400776-(NSImage*) getImageForFilePath: (NSString *) path {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400777 if (path.length <= 0) {return nil;}
778 if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
779 NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400780 return transferedImage;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400781}
782
783-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
784 CGFloat horizaontalMargin = 6;
Anthony Léonard2382b562017-12-13 15:51:28 -0500785 NSMutableAttributedString* msgAttString =
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400786 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
Anthony Léonard2382b562017-12-13 15:51:28 -0500787 attributes:[self messageAttributes]];
788
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400789 CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
790 NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400791 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400792 [[tv textStorage] setAttributedString:msgAttString];
793 [tv sizeToFit];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400794 return tv.frame.size;
795}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400796
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400797-(MessageSequencing) computeSequencingFor:(NSInteger) row {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400798 try {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400799 auto* conv = [self getCurrentConversation];
800 if (row >= conversationView.numberOfRows - 1 || row >= conv->interactions.size()) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400801 return SINGLE_WITHOUT_TIME;
802 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400803 if (conv == nil)
804 return SINGLE_WITHOUT_TIME;
805 auto it = conv->interactions.begin();
806 std::advance(it, row);
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400807 if (it == conv->interactions.end()) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400808 return SINGLE_WITHOUT_TIME;
809 }
810 auto interaction = it->second;
811 if (interaction.type != lrc::api::interaction::Type::TEXT) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400812 return SINGLE_WITH_TIME;
813 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400814 // first message in comversation
815 if (row == 0) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400816 if (it == conv->interactions.end() || conv->interactions.size() < 2) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400817 return SINGLE_WITH_TIME;
818 }
819 auto nextIt = it;
820 nextIt++;
821 if (nextIt == conv->interactions.end()) {
822 return SINGLE_WITH_TIME;
823 }
824 auto nextInteraction = nextIt->second;
825 if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
826 return SINGLE_WITH_TIME;
827 }
828 return FIRST_WITH_TIME;
829 }
830 // last message in comversation
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400831 if (row == conv->interactions.size() - 1) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400832 if(it == conv->interactions.begin()) {
833 return SINGLE_WITH_TIME;
834 }
835 auto previousIt = it;
836 previousIt--;
837 auto previousInteraction = previousIt->second;
838 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
839 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
840 if (!timeChanged && !authorChanged) {
841 return LAST_IN_SEQUENCE;
842 }
843 if (!timeChanged && authorChanged) {
844 return SINGLE_WITHOUT_TIME;
845 }
Kateryna Kostiukcd9397f2020-04-13 09:40:53 -0400846 return SINGLE_WITH_TIME;
847 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400848 // single message in comversation
849 if(it == conv->interactions.begin() || it == conv->interactions.end()) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400850 return SINGLE_WITH_TIME;
851 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400852 // message in the middle of conversation
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400853 auto previousIt = it;
854 previousIt--;
855 auto previousInteraction = previousIt->second;
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400856 auto nextIt = it;
857 nextIt++;
858 if (nextIt == conv->interactions.end()) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400859 return SINGLE_WITHOUT_TIME;
860 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400861 auto nextInteraction = nextIt->second;
862
863 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
864 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
865 bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
866 if (previousInteraction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
867 if(!sequenceWillChange) {
868 return FIRST_WITH_TIME;
869 }
870 return SINGLE_WITH_TIME;
871 }
872 if (!sequenceWillChange) {
873 if (!timeChanged && !authorChanged) {
874 return MIDDLE_IN_SEQUENCE;
875 }
876 if (timeChanged) {
877 return FIRST_WITH_TIME;
878 }
879 return FIRST_WITHOUT_TIME;
880 } if (!timeChanged && !authorChanged) {
881 return LAST_IN_SEQUENCE;
882 } if (timeChanged) {
883 return SINGLE_WITH_TIME;
884 }
885 return SINGLE_WITHOUT_TIME;
886 } catch (std::out_of_range& e) {
Kateryna Kostiukcd9397f2020-04-13 09:40:53 -0400887 return SINGLE_WITHOUT_TIME;
888 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400889}
890
891-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
892 return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
893}
894
895-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
896 bool timeChanged = NO;
897 NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
898 NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
899 bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
900 bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
901 if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
902 timeChanged = YES;
903 }
904 return timeChanged;
905}
906
907-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
908 bool authorChanged = YES;
909 bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
910 if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
911 authorChanged = NO;
912 }
913 return authorChanged;
914}
915
916-(NSString *)timeForMessage:(NSDate*) msgTime {
917 NSDate *today = [NSDate date];
918 NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
Kateryna Kostiukaf6d5e22018-06-12 15:00:00 -0400919 [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[NSLocale currentLocale] localeIdentifier]]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400920 if ([[NSCalendar currentCalendar] compareDate:today
921 toDate:msgTime
922 toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500923 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterLongStyle timeStyle:NSDateFormatterMediumStyle];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400924 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400925 if ([[NSCalendar currentCalendar] compareDate:today
926 toDate:msgTime
927 toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
928 [[NSCalendar currentCalendar] compareDate:today
929 toDate:msgTime
930 toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500931 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterShortStyle];
932 }
933 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterNoStyle timeStyle:NSDateFormatterShortStyle];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400934}
935
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400936- (void) updateSendMessageHeight {
kkostiuk26f99fb2021-02-17 15:06:56 -0500937 NSAttributedString *msgAttString = messageView.attributedString;
kkostiuk153c2692021-02-23 21:30:23 -0500938 if (!msgAttString || msgAttString.length == 0) {
939 [self resetSendMessagePanelToDefaultSize];
kkostiuk26f99fb2021-02-17 15:06:56 -0500940 return;
941 }
942 NSRect frame = NSMakeRect(0, 0, messageView.frame.size.width, msgAttString.size.height);
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400943 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
944 [[tv textStorage] setAttributedString:msgAttString];
945 [tv sizeToFit];
kkostiuk153c2692021-02-23 21:30:23 -0500946 // check the height of one line and update default line height if it does not match
947 NSAttributedString *firstLetter = [msgAttString attributedSubstringFromRange:NSMakeRange(0, 1)];
948 auto lineHeight = firstLetter.size.height;
kkostiuke6c33db2021-02-24 16:20:50 -0500949 // 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 -0500950 auto accuracy = abs(lineHeight - MESSAGE_VIEW_DEFAULT_HEIGHT);
kkostiuk153c2692021-02-23 21:30:23 -0500951 // 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 -0500952 auto top = tv.frame.size.height > MESSAGE_VIEW_DEFAULT_HEIGHT ? TOP_MARGIN_MIN : TOP_MARGIN;
953 auto bottom = tv.frame.size.height > MESSAGE_VIEW_DEFAULT_HEIGHT ? BOTTOM_MARGIN_MIN : BOTTOM_MARGIN;
954 CGFloat heightWithMargins = tv.frame.size.height + top + bottom + SEND_PANEL_BOTTOM_MARGIN;
955 CGFloat newSendPanelHeight = MIN(SEND_PANEL_MAX_HEIGHT, MAX(SEND_PANEL_DEFAULT_HEIGHT, heightWithMargins));
956 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 -0500957 if (abs(messageHeight.constant - msgHeight) <= accuracy) {
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400958 return;
959 }
kkostiuke6c33db2021-02-24 16:20:50 -0500960 if (MESSAGE_VIEW_DEFAULT_HEIGHT != lineHeight) {
961 MESSAGE_VIEW_DEFAULT_HEIGHT = lineHeight;
962 }
kkostiuk26f99fb2021-02-17 15:06:56 -0500963 messagesBottomMargin.constant = newSendPanelHeight;
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400964 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
965 [self scrollToBottom];
kkostiuk26f99fb2021-02-17 15:06:56 -0500966 messageHeight.constant = msgHeight;
967 textBottomConstraint.constant = bottom;
968 sendPanelHeight.constant = newSendPanelHeight;
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400969 });
970}
971
Anthony Léonard2382b562017-12-13 15:51:28 -0500972#pragma mark - NSTableViewDataSource
973
974- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
975{
976 auto* conv = [self getCurrentConversation];
977
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400978 // return conversation +1 view for composing indicator
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500979 if (conv)
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400980 return conv->interactions.size() + 1;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500981 else
982 return 0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500983}
984
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400985#pragma mark - Text formatting
986
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400987- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400988{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400989 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiukd508f752020-11-13 16:26:00 -0500990 NSFont *font = [NSFont systemFontOfSize: 12 weight: NSFontWeightLight];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400991 attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400992 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
Kateryna Kostiukd508f752020-11-13 16:26:00 -0500993 attrs[NSFontAttributeName] = font;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400994 return attrs;
995}
996
997- (NSParagraphStyle*) paragraphStyle
998{
999 /*
1000 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
1001 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
1002 to copy, we use the default one.
1003
1004 The default values supplied by the default NSParagraphStyle are:
1005 Alignment NSNaturalTextAlignment
1006 Tab stops 12 left-aligned tabs, spaced by 28.0 points
1007 Line break mode NSLineBreakByWordWrapping
1008 All others 0.0
1009 */
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001010 NSMutableParagraphStyle* aMutableParagraphStyle =
1011 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1012 [aMutableParagraphStyle setHeadIndent:1.0];
1013 [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
1014 return aMutableParagraphStyle;
1015}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001016
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001017#pragma mark - Actions
1018
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001019- (void)acceptIncomingFile:(id)sender {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001020 auto interId = [(IMTableCellView*)[[[[[[sender superview] superview] superview] superview] superview] superview] interaction];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001021 auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001022 if (convModel_ && !convUid_.isEmpty()) {
Kateryna Kostiuk26405ac2020-05-27 14:25:13 -04001023 convModel_->acceptTransfer(convUid_, interId);
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001024 }
1025}
1026
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001027- (void)declineIncomingFile:(id)sender {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001028 auto inter = [(IMTableCellView*)[[[[[[sender superview] superview] superview] superview] superview] superview] interaction];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001029 if (convModel_ && !convUid_.isEmpty()) {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001030 convModel_->cancelTransfer(convUid_, inter);
1031 }
1032}
1033
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001034- (void)imagePreview:(id)sender {
1035 uint64_t interId;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001036 if ([[[[[[sender superview] superview] superview] superview] superview] isKindOfClass:[IMTableCellView class]]) {
1037 interId = [(IMTableCellView*)[[[[[sender superview] superview] superview] superview] superview] interaction];
1038 } else if ([[[[[sender superview] superview] superview] superview] isKindOfClass:[IMTableCellView class]]) {
1039 interId = [(IMTableCellView*)[[[[sender superview] superview] superview] superview] interaction];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001040 } else {
1041 return;
1042 }
1043 auto it = [self getCurrentConversation]->interactions.find(interId);
1044 if (it == [self getCurrentConversation]->interactions.end()) {
1045 return;
1046 }
1047 auto& interaction = it->second;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001048 NSString* name = interaction.body.toNSString();
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001049 if (([name rangeOfString:@"/"].location == NSNotFound)) {
1050 name = [self getDataTransferPath:interId];
1051 }
1052 previewImage = name;
Kateryna Kostiuk30c6ac22020-05-06 17:42:59 -04001053 if (!previewImage || previewImage.length <= 0) {
1054 return;
1055 }
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001056 [self addToResponderChain];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001057 if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
1058 [[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
1059 } else {
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001060 dispatch_async(dispatch_get_main_queue(), ^{
1061 [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:self];
1062 });
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001063 }
1064}
1065
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001066- (IBAction)sendMessage:(id)sender {
1067 NSString* text = self.message;
Kateryna Kostiuka8b6b562019-02-01 13:26:18 -05001068 unichar separatorChar = NSLineSeparatorCharacter;
1069 NSString *separatorString = [NSString stringWithCharacters:&separatorChar length:1];
1070 text = [text stringByReplacingOccurrencesOfString: separatorString withString: @"\n"];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001071 if (text && text.length > 0) {
1072 auto* conv = [self getCurrentConversation];
Kateryna Kostiuk0c068552020-03-30 09:48:17 -04001073 if (conv == nil)
1074 return;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001075 convModel_->sendMessage(convUid_, QString::fromNSString(text));
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001076 self.message = @"";
kkostiuk26f99fb2021-02-17 15:06:56 -05001077 [self resetSendMessagePanelToDefaultSize];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001078 if (composingMessage) {
1079 composingMessage = false;
1080 convModel_->setIsComposing(convUid_, composingMessage);
1081 }
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001082 }
1083}
1084
kkostiuk26f99fb2021-02-17 15:06:56 -05001085-(void) resetSendMessagePanelToDefaultSize {
1086 if(messageHeight.constant != MESSAGE_VIEW_DEFAULT_HEIGHT) {
1087 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
1088 messageHeight.constant = MESSAGE_VIEW_DEFAULT_HEIGHT;
1089 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
1090 textBottomConstraint.constant = BOTTOM_MARGIN;
1091 [self scrollToBottom];
1092 }
1093}
1094
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001095- (IBAction)openEmojy:(id)sender {
kkostiuk26f99fb2021-02-17 15:06:56 -05001096 [messageView.window makeFirstResponder: messageView];
1097 [NSApp orderFrontCharacterPalette: messageView];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001098}
1099
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001100- (IBAction)startVideoMessage:(id)sender
1101{
1102 [self startRecording:NO];
1103}
1104
1105- (IBAction)startAudioMessage:(id)sender
1106{
1107 [self startRecording:YES];
1108}
1109-(void) startRecording:(BOOL)isAudio {
1110 if (recordingController == nil) {
1111 recordingController = [[RecordFileVC alloc] initWithNibName:@"RecordFileVC" bundle:nil avModel: self->avModel];
1112 recordingController.delegate = self;
1113 }
1114 if(recordMessagePopover != nil)
1115 {
1116 [self closeRecordingView];
1117 return;
1118 }
1119 recordMessagePopover = [[NSPopover alloc] init];
1120 [recordingController prepareRecordingView: isAudio];
1121 [recordMessagePopover setContentSize: recordingController.view.frame.size];
1122 [recordMessagePopover setContentViewController:recordingController];
1123 [recordMessagePopover setAnimates:YES];
1124 NSButton *anchorButton = isAudio ? recordAudioButton : recordVideoButton;
1125 [recordMessagePopover showRelativeToRect: anchorButton.bounds
1126 ofView: anchorButton
1127 preferredEdge: NSMaxYEdge];
1128}
1129
1130-(void) sendFile:(NSString *) name withFilePath:(NSString *) path {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001131 convModel_->sendFile(convUid_, QString::fromNSString(path), QString::fromNSString(name));
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001132}
1133
1134-(void) closeRecordingView {
1135 if(recordMessagePopover != nil) {
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001136 [recordMessagePopover close];
1137 recordMessagePopover = nil;
Kateryna Kostiuka7404812019-10-28 12:24:46 -04001138 recordingController.stopRecordingView;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001139 }
1140}
1141
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001142- (IBAction)sendFile:(id)sender {
1143 NSOpenPanel* filePicker = [NSOpenPanel openPanel];
1144 [filePicker setCanChooseFiles:YES];
1145 [filePicker setCanChooseDirectories:NO];
1146 [filePicker setAllowsMultipleSelection:NO];
1147
1148 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
1149 if ([[filePicker URLs] count] == 1) {
1150 NSURL* url = [[filePicker URLs] objectAtIndex:0];
1151 const char* fullPath = [url fileSystemRepresentation];
1152 NSString* fileName = [url lastPathComponent];
1153 if (convModel_) {
1154 auto* conv = [self getCurrentConversation];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001155 convModel_->sendFile(convUid_, QString::fromStdString(fullPath), QString::fromNSString(fileName));
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001156 }
1157 }
1158 }
1159}
1160
1161
kkostiuk26f99fb2021-02-17 15:06:56 -05001162#pragma mark - NSTextViewDelegate
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001163
kkostiuk26f99fb2021-02-17 15:06:56 -05001164- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001165 if (commandSelector == @selector(insertNewline:)) {
1166 if(self.message.length > 0) {
1167 [self sendMessage: nil];
kkostiuk26f99fb2021-02-17 15:06:56 -05001168 return YES;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001169 }
kkostiuk26f99fb2021-02-17 15:06:56 -05001170 [self resetSendMessagePanelToDefaultSize];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001171 return YES;
1172 }
1173 return NO;
1174}
1175
kkostiuk26f99fb2021-02-17 15:06:56 -05001176-(void) textDidChange:(NSNotification *)notification {
1177 [self checkIfComposingMsg];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001178}
1179
kkostiuk26f99fb2021-02-17 15:06:56 -05001180- (void) checkIfComposingMsg {
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001181 [self updateSendMessageHeight];
kkostiuk26f99fb2021-02-17 15:06:56 -05001182 BOOL haveText = [messageView.string removeEmptyLinesAtBorders].length != 0;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001183 if (haveText != composingMessage) {
1184 composingMessage = haveText;
1185 convModel_->setIsComposing(convUid_, composingMessage);
1186 }
1187}
1188
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001189#pragma mark - QLPreviewPanelDataSource
1190
1191-(void)beginPreviewPanelControl:(QLPreviewPanel *)panel
1192{
1193 panel.dataSource = self;
1194}
1195
1196- (void)endPreviewPanelControl:(QLPreviewPanel *)panel {
1197 panel.dataSource = nil;
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001198 [self removeFromResponderChain];
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001199}
1200
1201-(BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel
1202{
1203 return YES;
1204}
1205
1206- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel {
1207 return 1;
1208}
1209
1210- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index {
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001211 if (previewImage == nil) {
1212 return nil;
1213 }
Kateryna Kostiuk30c6ac22020-05-06 17:42:59 -04001214 try {
1215 return [NSURL fileURLWithPath: previewImage];
1216 } catch (NSException *exception) {
1217 nil;
1218 }
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001219}
1220
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001221- (void)addToResponderChain {
1222 if (conversationView.window &&
1223 ![[conversationView.window nextResponder] isEqual:self]) {
1224 NSResponder * aNextResponder = [conversationView.window nextResponder];
1225 [conversationView.window setNextResponder:self];
1226 }
1227}
1228
1229
1230- (void)removeFromResponderChain {
1231 if (conversationView.window &&
1232 [[conversationView.window nextResponder] isEqual:self]) {
1233 NSResponder * aNextResponder = [conversationView.window nextResponder];
1234 [conversationView.window setNextResponder:[self nextResponder]];
1235 }
1236}
1237
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001238@end