blob: 1a8cd7b54f2c8c9724a5a8e8fbcbbb1843b5eec6 [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
kkostiuk26f99fb2021-02-17 15:06:56 -050044@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource, QLPreviewPanelDataSource, NSTextViewDelegate> {
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;
kkostiuk26f99fb2021-02-17 15:06:56 -050048 __unsafe_unretained IBOutlet NSTextView* messageView;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050049 __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;
kkostiuk26f99fb2021-02-17 15:06:56 -050053 __unsafe_unretained IBOutlet NSLayoutConstraint* messageHeight;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050054 __unsafe_unretained IBOutlet NSLayoutConstraint* messagesBottomMargin;
kkostiuk26f99fb2021-02-17 15:06:56 -050055 __unsafe_unretained IBOutlet NSLayoutConstraint* textBottomConstraint;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040056 IBOutlet NSPopover *recordMessagePopover;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040057
Kateryna Kostiukc867eb92020-03-08 13:15:17 -040058 QString convUid_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050059 lrc::api::ConversationModel* convModel_;
Anthony Léonard2382b562017-12-13 15:51:28 -050060 const lrc::api::conversation::Info* cachedConv_;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040061 lrc::api::AVModel* avModel;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050062 QMetaObject::Connection newInteractionSignal_;
Anthony Léonard2382b562017-12-13 15:51:28 -050063
64 // Both are needed to invalidate cached conversation as pointer
65 // may not be referencing the same conversation anymore
66 QMetaObject::Connection modelSortedSignal_;
67 QMetaObject::Connection filterChangedSignal_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050068 QMetaObject::Connection interactionStatusUpdatedSignal_;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -040069 QMetaObject::Connection peerComposingMsgSignal_;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -040070 QMetaObject::Connection lastDisplayedChanged_;
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -040071 NSString* previewImage;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050072 NSMutableDictionary *pendingMessagesToSend;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -040073 RecordFileVC * recordingController;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040074}
75
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040076@end
77
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050078// Tags for view
79NSInteger const GENERIC_INT_TEXT_TAG = 100;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040080NSInteger const GENERIC_INT_TIME_TAG = 200;
81
82// views size
83CGFloat const GENERIC_CELL_HEIGHT = 60;
84CGFloat const TIME_BOX_HEIGHT = 34;
85CGFloat const MESSAGE_TEXT_PADDING = 10;
86CGFloat const MAX_TRANSFERED_IMAGE_SIZE = 250;
87CGFloat const BUBBLE_HEIGHT_FOR_TRANSFERED_FILE = 87;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -040088CGFloat const DEFAULT_ROW_HEIGHT = 10;
89CGFloat const HEIGHT_FOR_COMPOSING_INDICATOR = 46;
Kateryna Kostiuka1201922020-04-20 11:59:35 -040090CGFloat const HEIGHT_DEFAULT = 34;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -050091NSInteger const SEND_PANEL_DEFAULT_HEIGHT = 60;
kkostiuk26f99fb2021-02-17 15:06:56 -050092NSInteger const SEND_PANEL_MAX_HEIGHT = 167;
93NSInteger const SEND_PANEL_BOTTOM_MARGIN = 13;
kkostiuk153c2692021-02-23 21:30:23 -050094NSInteger MESSAGE_VIEW_DEFAULT_HEIGHT = 17;
kkostiuk26f99fb2021-02-17 15:06:56 -050095NSInteger const BOTTOM_MARGIN = 8;
96NSInteger const BOTTOM_MARGIN_MIN = 0;
97NSInteger const TOP_MARGIN = 20;
98NSInteger const TOP_MARGIN_MIN = 13;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050099
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400100BOOL peerComposingMessage = false;
101BOOL composingMessage = false;
102
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400103@implementation MessagesVC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400104
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400105
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400106//MessageBuble type
107typedef NS_ENUM(NSInteger, MessageSequencing) {
108 SINGLE_WITH_TIME = 0,
109 SINGLE_WITHOUT_TIME = 1,
110 FIRST_WITH_TIME = 2,
111 FIRST_WITHOUT_TIME = 3,
112 MIDDLE_IN_SEQUENCE = 5,
113 LAST_IN_SEQUENCE = 6,
114};
115
116- (void)awakeFromNib
117{
118 NSNib *cellNib = [[NSNib alloc] initWithNibNamed:@"MessageCells" bundle:nil];
119 [conversationView registerNib:cellNib forIdentifier:@"LeftIncomingFileView"];
120 [conversationView registerNib:cellNib forIdentifier:@"LeftOngoingFileView"];
121 [conversationView registerNib:cellNib forIdentifier:@"LeftFinishedFileView"];
122 [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"];
123 [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400124 [conversationView registerNib:cellNib forIdentifier:@"PeerComposingMsgView"];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500125 [[conversationView.enclosingScrollView contentView] setCopiesOnScroll:NO];
kkostiuk26f99fb2021-02-17 15:06:56 -0500126 [messageView setFont: [NSFont systemFontOfSize: 14 weight: NSFontWeightLight]];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500127 [conversationView setWantsLayer:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400128}
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500129
130- (instancetype)initWithCoder:(NSCoder *)coder
131{
132 self = [super initWithCoder:coder];
133 if (self) {
134 pendingMessagesToSend = [[NSMutableDictionary alloc] init];
135 }
136 return self;
137}
138
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -0400139-(void) setAVModel: (lrc::api::AVModel*) avmodel {
140 avModel = avmodel;
141 if (recordingController == nil) {
142 recordingController = [[RecordFileVC alloc] initWithNibName:@"RecordFileVC" bundle:nil avModel: self->avModel];
143 recordingController.delegate = self;
144 }
145}
146
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500147- (void)setMessage:(NSString *)newValue {
148 _message = [newValue removeEmptyLinesAtBorders];
149}
150
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400151-(void) clearData {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400152 if (!convUid_.isEmpty()) {
kkostiuk26f99fb2021-02-17 15:06:56 -0500153 pendingMessagesToSend[convUid_.toNSString()] = self.message;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500154 }
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400155 cachedConv_ = nil;
156 convUid_ = "";
157 convModel_ = nil;
158
159 QObject::disconnect(modelSortedSignal_);
160 QObject::disconnect(filterChangedSignal_);
161 QObject::disconnect(interactionStatusUpdatedSignal_);
162 QObject::disconnect(newInteractionSignal_);
Kateryna Kostiuk5acaefd2020-03-25 11:14:25 -0400163 QObject::disconnect(peerComposingMsgSignal_);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400164 QObject::disconnect(lastDisplayedChanged_);
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -0400165 [self closeRecordingView];
Andreas Traczyk252a94a2018-04-20 16:36:20 -0400166}
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400167
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500168-(void) scrollToBottom {
169 CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
170 NSRange range = [conversationView rowsInRect:visibleRect];
171 NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
172 NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400173 NSInteger numberOfRows = [conversationView numberOfRows];
174 if ((numberOfRows > 0) &&
175 lastvisibleRow > (numberOfRows - 5)) {
176 [conversationView scrollRowToVisible:numberOfRows - 1];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500177 }
178}
179
Anthony Léonard2382b562017-12-13 15:51:28 -0500180-(const lrc::api::conversation::Info*) getCurrentConversation
181{
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400182 if (convModel_ == nil || convUid_.isEmpty())
Anthony Léonard2382b562017-12-13 15:51:28 -0500183 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400184
Anthony Léonard2382b562017-12-13 15:51:28 -0500185 if (cachedConv_ != nil)
186 return cachedConv_;
Kateryna Kostiuka7b909c2020-10-19 11:46:26 -0400187 auto convOpt = getConversationFromUid(convUid_, *convModel_);
188 if (convOpt.has_value()) {
kkostiukf81c6372021-01-11 18:51:28 -0500189 lrc::api::conversation::Info& conversation = *convOpt;
Kateryna Kostiuka7b909c2020-10-19 11:46:26 -0400190 cachedConv_ = &conversation;
Kateryna Kostiuk3541ae22020-08-17 12:26:14 -0400191 }
Anthony Léonard2382b562017-12-13 15:51:28 -0500192 return cachedConv_;
193}
194
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400195-(void) reloadConversationForMessage:(uint64_t) uid updateSize:(BOOL) update {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400196 auto* conv = [self getCurrentConversation];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400197 if (conv == nil)
198 return;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400199 auto it = conv->interactions.find(uid);
200 if (it == conv->interactions.end()) {
201 return;
202 }
203 auto itIndex = distance(conv->interactions.begin(),it);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400204 if (itIndex >= ([conversationView numberOfRows] - 1) || itIndex >= conv->interactions.size()) {
Kateryna Kostiuka1201922020-04-20 11:59:35 -0400205 return;
206 }
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400207 NSRange rangeToUpdate = NSMakeRange(itIndex, 2);
208 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndexesInRange:rangeToUpdate];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400209 //reload previous message to update bubbleview
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400210 if (itIndex > 0) {
211 auto previousIt = it;
212 previousIt--;
213 auto previousInteraction = previousIt->second;
214 if (previousInteraction.type == lrc::api::interaction::Type::TEXT) {
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400215 NSRange range = NSMakeRange(itIndex - 1, 3);
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400216 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
217 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400218 }
219 if (update) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400220 NSRange insertRange = NSMakeRange(itIndex, 1);
221 NSIndexSet* insertRangeSet = [NSIndexSet indexSetWithIndexesInRange:insertRange];
222 [conversationView removeRowsAtIndexes:insertRangeSet withAnimation:(NSTableViewAnimationEffectNone)];
223 [conversationView insertRowsAtIndexes:insertRangeSet withAnimation:(NSTableViewAnimationEffectNone)];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400224 }
225 [conversationView reloadDataForRowIndexes: indexSet
226 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400227 [self scrollToBottom];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400228}
229
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400230-(void)setConversationUid:(const QString&)convUid model:(lrc::api::ConversationModel *)model
Anthony Léonard2382b562017-12-13 15:51:28 -0500231{
232 if (convUid_ == convUid && convModel_ == model)
233 return;
234
235 cachedConv_ = nil;
236 convUid_ = convUid;
237 convModel_ = model;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400238 peerComposingMessage = false;
239 composingMessage = false;
Anthony Léonard2382b562017-12-13 15:51:28 -0500240
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500241 // Signal triggered when messages are received or their status updated
242 QObject::disconnect(newInteractionSignal_);
243 QObject::disconnect(interactionStatusUpdatedSignal_);
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400244 QObject::disconnect(peerComposingMsgSignal_);
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400245 QObject::disconnect(lastDisplayedChanged_);
246 lastDisplayedChanged_ =
247 QObject::connect(convModel_,
248 &lrc::api::ConversationModel::displayedInteractionChanged,
249 [self](const QString &uid,
250 const QString &participantURI,
251 const uint64_t &previousUid,
252 const uint64_t &newdUid) {
253 if (uid != convUid_)
254 return;
255 [self reloadConversationForMessage:newdUid updateSize: NO];
256 [self reloadConversationForMessage:previousUid updateSize: NO];
257 });
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400258
259 peerComposingMsgSignal_ = QObject::connect(convModel_,
260 &lrc::api::ConversationModel::composingStatusChanged,
261 [self](const QString &uid,
262 const QString &contactUri,
263 bool isComposing) {
264 if (uid != convUid_)
265 return;
266 bool shouldUpdate = isComposing != peerComposingMessage;
267 if (!shouldUpdate) {
268 return;
269 }
270 // reload and update height for composing indicator
271 peerComposingMessage = isComposing;
272 auto* conv = [self getCurrentConversation];
273 if (conv == nil)
274 return;
275 auto row = [conversationView numberOfRows] - 1;
276 if (row < 0) {
277 return;
278 }
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400279 if(peerComposingMessage) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400280 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:row];
281 [conversationView reloadDataForRowIndexes: indexSet
282 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400283 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400284 [self scrollToBottom];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400285 } else {
286 //whait for possible incoming message to avoid view jumping
287 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400288 auto row = [conversationView numberOfRows] - 1;
289 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:row];
290 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
291 [conversationView reloadDataForRowIndexes: indexSet
292 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
293 [self scrollToBottom];
Kateryna Kostiukf5eb58c2020-03-25 16:50:35 -0400294 });
295 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400296 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500297 newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400298 [self](const QString& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400299 if (uid != convUid_)
300 return;
301 cachedConv_ = nil;
302 peerComposingMessage = false;
303 [conversationView noteNumberOfRowsChanged];
304 [self reloadConversationForMessage:interactionId updateSize: YES];
305 [self scrollToBottom];
306 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500307 interactionStatusUpdatedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400308 [self](const QString& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400309 if (uid != convUid_)
310 return;
311 cachedConv_ = nil;
312 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
313 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
314 convModel_->refreshFilter();
315 }
316 [self reloadConversationForMessage:interactionId updateSize: interaction.type == lrc::api::interaction::Type::DATA_TRANSFER];
317 [self scrollToBottom];
318 });
Anthony Léonard2382b562017-12-13 15:51:28 -0500319
320 // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
321 // after a reordering.
322 QObject::disconnect(modelSortedSignal_);
323 QObject::disconnect(filterChangedSignal_);
Kateryna Kostiuka7b909c2020-10-19 11:46:26 -0400324 modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelChanged,
Anthony Léonard2382b562017-12-13 15:51:28 -0500325 [self](){
326 cachedConv_ = nil;
327 });
328 filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
329 [self](){
330 cachedConv_ = nil;
331 });
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400332 if (pendingMessagesToSend[convUid_.toNSString()]) {
kkostiuk26f99fb2021-02-17 15:06:56 -0500333 NSString *mess = pendingMessagesToSend[convUid_.toNSString()];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400334 self.message = pendingMessagesToSend[convUid_.toNSString()];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500335 [self updateSendMessageHeight];
336 } else {
337 self.message = @"";
kkostiuk26f99fb2021-02-17 15:06:56 -0500338 [self resetSendMessagePanelToDefaultSize];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500339 }
340 conversationView.alphaValue = 0.0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500341 [conversationView reloadData];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400342 [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500343 CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"];
344 fadeIn.fromValue = [NSNumber numberWithFloat:0.0];
345 fadeIn.toValue = [NSNumber numberWithFloat:1.0];
346 fadeIn.duration = 0.4f;
347
348 [conversationView.layer addAnimation:fadeIn forKey:fadeIn.keyPath];
349 conversationView.alphaValue = 1;
350 auto* conv = [self getCurrentConversation];
351
352 if (conv == nil)
353 return;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400354 try {
355 [sendFileButton setEnabled:(convModel_->owner.contactModel->getContact(conv->participants[0]).profileInfo.type != lrc::api::profile::Type::SIP)];
356 } catch (std::out_of_range& e) {
357 NSLog(@"contact out of range");
358 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400359}
360
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400361#pragma mark - configure cells
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400362
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400363-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500364{
365 NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
366 NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400367 NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500368
369 // TODO: Fix symbol in LRC
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500370 NSString* fixedString = [[text stringByReplacingOccurrencesOfString:@"🕽" withString:@""] stringByReplacingOccurrencesOfString:@"📞" withString:@""];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500371 [textField setStringValue:fixedString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400372 [timeField setStringValue:time];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500373
374 return result;
375}
376
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400377-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500378{
379 IMTableCellView* result;
380
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400381 auto type = interaction.type;
382 auto status = interaction.status;
383
384 NSString* fileName = @"incoming file";
385
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500386 // First, view is created
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400387 if (!interaction.authorUri.isEmpty()) {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500388 switch (status) {
389 case lrc::api::interaction::Status::TRANSFER_CREATED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400390 case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
391 result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
392 [result.acceptButton setAction:@selector(acceptIncomingFile:)];
393 [result.acceptButton setTarget:self];
394 [result.declineButton setAction:@selector(declineIncomingFile:)];
395 [result.declineButton setTarget:self];
396 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500397 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400398 case lrc::api::interaction::Status::TRANSFER_ONGOING: {
399 result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
400 [result.progressIndicator startAnimation:conversationView];
401 [result.declineButton setAction:@selector(declineIncomingFile:)];
402 [result.declineButton setTarget:self];
403 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500404 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400405 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
406 [result.transferedFileName setAction:@selector(imagePreview:)];
407 [result.transferedFileName setTarget:self];
408 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
409 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500410 case lrc::api::interaction::Status::TRANSFER_CANCELED:
411 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400412 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
413 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500414 }
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400415 } else {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400416 NSString* fileName = @"sent file";
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500417 switch (status) {
418 case lrc::api::interaction::Status::TRANSFER_CREATED:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500419 case lrc::api::interaction::Status::TRANSFER_ONGOING:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400420 case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500421 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400422 result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500423 [result.progressIndicator startAnimation:nil];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400424 [result.declineButton setAction:@selector(declineIncomingFile:)];
425 [result.declineButton setTarget:self];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500426 break;
427 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400428 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
429 [result.transferedFileName setAction:@selector(imagePreview:)];
430 [result.transferedFileName setTarget:self];
431 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
432 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500433 case lrc::api::interaction::Status::TRANSFER_CANCELED:
434 case lrc::api::interaction::Status::TRANSFER_ERROR:
Olivier Soldanoe521a182018-02-26 16:55:19 -0500435 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400436 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500437 }
438 }
439
440 // Then status label is updated if needed
441 switch (status) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400442 [result.statusLabel setTextColor:[NSColor textColor]];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500443 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400444 [result.statusLabel setTextColor:[NSColor greenSuccessColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500445 [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500446 break;
447 case lrc::api::interaction::Status::TRANSFER_CANCELED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400448 [result.statusLabel setTextColor:[NSColor orangeColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500449 [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500450 break;
451 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400452 [result.statusLabel setTextColor:[NSColor errorTransferColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500453 [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500454 break;
455 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400456 [result.statusLabel setTextColor:[NSColor textColor]];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500457 [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
458 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500459 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400460 result.transferedImage.image = nil;
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400461 [result.openImagebutton setHidden:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400462 [result.msgBackground setHidden:NO];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400463 NSString* name = interaction.body.toNSString();
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400464 if (name.length > 0) {
Kateryna Kostiuk67735232018-05-10 15:05:32 -0400465 fileName = [name lastPathComponent];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400466 }
Kateryna Kostiukd508f752020-11-13 16:26:00 -0500467 NSFont *nameFont = [NSFont systemFontOfSize: 12 weight: NSFontWeightLight];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400468 NSColor *nameColor = [NSColor textColor];
469 NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
470 paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
471 paragraphStyle.alignment = NSTextAlignmentLeft;
472 NSDictionary *nameAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
473 nameColor,NSForegroundColorAttributeName,
474 paragraphStyle,NSParagraphStyleAttributeName, nil];
475 NSAttributedString* nameAttributedString = [[NSAttributedString alloc] initWithString:fileName attributes:nameAttr];
476 result.transferedFileName.attributedTitle = nameAttributedString;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400477 if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400478 NSColor *higlightColor = [NSColor grayColor];
479 NSDictionary *alternativeNametAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
480 higlightColor,NSForegroundColorAttributeName,
481 paragraphStyle,NSParagraphStyleAttributeName, nil];
482 NSAttributedString* alternativeString = [[NSAttributedString alloc] initWithString:fileName attributes:alternativeNametAttr];
483 result.transferedFileName.attributedAlternateTitle = alternativeString;
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400484 NSImage* image = [self getImageForFilePath:name];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400485 if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400486 image = [self getImageForFilePath:[self getDataTransferPath:interactionID]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400487 }
488 if(image != nil) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400489 result.transferedImage.image = image;
490 [result updateImageConstraintWithMax: MAX_TRANSFERED_IMAGE_SIZE];
491 [result.openImagebutton setAction:@selector(imagePreview:)];
492 [result.openImagebutton setTarget:self];
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400493 [result.openImagebutton setHidden:NO];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400494 }
495 }
496 [result setupForInteraction:interactionID];
497 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
498 NSString* timeString = [self timeForMessage: msgTime];
499 result.timeLabel.stringValue = timeString;
500 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
501 if (!isOutgoing) {
Kateryna Kostiukdc720842020-09-10 16:44:02 -0400502 @autoreleasepool {
503 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
504 auto* conv = [self getCurrentConversation];
505 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
506 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400507 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500508 return result;
509}
510
Anthony Léonard2382b562017-12-13 15:51:28 -0500511#pragma mark - NSTableViewDelegate methods
512- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400513{
514 return YES;
515}
516
Anthony Léonard2382b562017-12-13 15:51:28 -0500517- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400518{
Anthony Léonard2382b562017-12-13 15:51:28 -0500519 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400520}
521
Anthony Léonard2382b562017-12-13 15:51:28 -0500522- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400523{
Anthony Léonard2382b562017-12-13 15:51:28 -0500524 auto* conv = [self getCurrentConversation];
Anthony Léonard2382b562017-12-13 15:51:28 -0500525 if (conv == nil)
526 return nil;
527
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400528 IMTableCellView* result;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500529 auto it = conv->interactions.begin();
Kateryna Kostiuka1201922020-04-20 11:59:35 -0400530 auto size = [conversationView numberOfRows] - 1;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400531
532 if (row > size || row > conv->interactions.size()) {
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400533 return [[NSView alloc] init];
534 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400535
536 if (row == size) {
Kateryna Kostiukb39ca192020-03-27 14:01:00 -0400537 if (size < 1) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400538 return nil;
Kateryna Kostiukb39ca192020-03-27 14:01:00 -0400539 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400540 //last row peer composing view
541 result = [tableView makeViewWithIdentifier:@"PeerComposingMsgView" owner:conversationView];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400542 result.alphaValue = 0;
543 [result animateCompozingIndicator: NO];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400544 CGFloat alpha = peerComposingMessage ? 1 : 0;
545 CGFloat height = peerComposingMessage ? HEIGHT_FOR_COMPOSING_INDICATOR : DEFAULT_ROW_HEIGHT;
546 [result updateHeightConstraints: height];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400547 if (alpha == 1) {
548 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
549 if (peerComposingMessage) {
550 result.alphaValue = alpha;
551 [result animateCompozingIndicator: YES];
552 }
553 });
554 }
555 return result;
556 }
Anthony Léonard2382b562017-12-13 15:51:28 -0500557
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500558 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500559
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400560 if (it == conv->interactions.end()) {
561 return [[NSView alloc] init];
562 }
563
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400564 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500565 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
566
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500567 switch (interaction.type) {
568 case lrc::api::interaction::Type::TEXT:
569 if (isOutgoing) {
570 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
571 } else {
572 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
573 }
574 break;
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400575 case lrc::api::interaction::Type::DATA_TRANSFER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400576 return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500577 break;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500578 case lrc::api::interaction::Type::CONTACT:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400579 case lrc::api::interaction::Type::CALL: {
580 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
581 NSString* timeString = [self timeForMessage: msgTime];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400582 return [self makeGenericInteractionViewForTableView:tableView withText:interaction.body.toNSString() andTime:timeString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400583 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500584 default: // If interaction is not of a known type
585 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400586 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400587 MessageSequencing sequence = [self computeSequencingFor:row];
588 BubbleType type = SINGLE;
589 if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
590 type = FIRST;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400591 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400592 if (sequence == MIDDLE_IN_SEQUENCE) {
593 type = MIDDLE;
594 }
595 if (sequence == LAST_IN_SEQUENCE) {
596 type = LAST;
597 }
598 result.msgBackground.type = type;
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400599 bool sendingFail = false;
600 [result.messageStatus setHidden:YES];
601 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
602 if (interaction.status == lrc::api::interaction::Status::SENDING) {
603 [result.messageStatus setHidden:NO];
604 [result.sendingMessageIndicator startAnimation:nil];
605 [result.messageFailed setHidden:YES];
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400606 } else if (interaction.status == lrc::api::interaction::Status::FAILURE) {
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400607 [result.messageStatus setHidden:NO];
608 [result.sendingMessageIndicator setHidden:YES];
609 [result.messageFailed setHidden:NO];
610 sendingFail = true;
611 }
612 }
613 [result setupForInteraction:it->first isFailed: sendingFail];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400614 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400615 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400616 [result.msgBackground setNeedsDisplay:YES];
617 [result setNeedsDisplay:YES];
618 [result.timeBox setNeedsDisplay:YES];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400619
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400620 NSString *text = interaction.body.toNSString();
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400621 text = [text removeEmptyLinesAtBorders];
622
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400623 NSMutableAttributedString* msgAttString =
Kateryna Kostiuk5a41a8d2020-05-13 09:05:56 -0400624 [[NSMutableAttributedString alloc] initWithString:text
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400625 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400626
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400627 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400628
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400629 [result updateMessageConstraint:messageSize.width andHeight:messageSize.height timeIsVisible:shouldDisplayTime isTopPadding: shouldApplyPadding];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400630 [[result.msgView textStorage] appendAttributedString:msgAttString];
Anthony Léonard2382b562017-12-13 15:51:28 -0500631
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400632 NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
633 NSArray *matches = [linkDetector matchesInString:result.msgView.string options:0 range:NSMakeRange(0, result.msgView.string.length)];
634
635 [result.msgView.textStorage beginEditing];
636
637 for (NSTextCheckingResult *match in matches) {
638 if (!match.URL) continue;
639
640 NSDictionary *linkAttributes = @{
641 NSLinkAttributeName: match.URL,
642 };
643 [result.msgView.textStorage addAttributes:linkAttributes range:match.range];
644 }
645
646 [result.msgView.textStorage endEditing];
647
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400648 if (shouldDisplayTime) {
649 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
650 NSString* timeString = [self timeForMessage: msgTime];
651 result.timeLabel.stringValue = timeString;
Anthony Léonard64e19672018-01-18 16:40:34 -0500652 }
653
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400654 bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
655 && sequence != FIRST_WITH_TIME) ? YES : NO;
656 [result.photoView setHidden:!shouldDisplayAvatar];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400657 BOOL showIndicator = convModel_->isLastDisplayed(convUid_, it->first, conv->participants.front());
658 [result.readIndicator setHidden: !showIndicator];
Kateryna Kostiukdc720842020-09-10 16:44:02 -0400659 @autoreleasepool {
660 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
661 auto image = QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)));
662 [result.readIndicator setImage:image];
663 if (!isOutgoing && shouldDisplayAvatar) {
664 [result.photoView setImage:image];
665 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400666 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400667 return result;
668}
669
Anthony Léonard2382b562017-12-13 15:51:28 -0500670- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400671{
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400672 try {
673 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400674
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400675 auto* conv = [self getCurrentConversation];
Anthony Léonard2382b562017-12-13 15:51:28 -0500676
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400677 if (conv == nil)
678 return HEIGHT_DEFAULT;
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400679
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400680 auto size = [conversationView numberOfRows] - 1;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400681
682 if (row > size || row > conv->interactions.size()) {
683 return HEIGHT_DEFAULT;
684 }
685 if (row == size) {
686 return peerComposingMessage ? HEIGHT_FOR_COMPOSING_INDICATOR : DEFAULT_ROW_HEIGHT;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400687 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400688
689 auto it = conv->interactions.begin();
690
691 std::advance(it, row);
692
693 if (it == conv->interactions.end()) {
694 return HEIGHT_DEFAULT;
695 }
696
697 auto interaction = it->second;
698
699 MessageSequencing sequence = [self computeSequencingFor:row];
700
701 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
702
703 if(interaction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
704 if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
705 NSString* name = interaction.body.toNSString();
706 NSImage* image = [self getImageForFilePath:name];
707 if (([name rangeOfString:@"/"].location == NSNotFound)) {
708 image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
709 }
710 if (image != nil) {
711 CGFloat widthScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.width;
712 CGFloat heightScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.height;
713 CGFloat heigt = 0;
714 if((widthScaleFactor >= 1) && (heightScaleFactor >= 1)) {
715 heigt = image.size.height;
716 } else {
717 CGFloat scale = MIN(widthScaleFactor, heightScaleFactor);
718 heigt = image.size.height * scale;
719 }
720 return heigt + TIME_BOX_HEIGHT;
721 }
722 }
723 return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
724 }
725
726 if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
727 return GENERIC_CELL_HEIGHT;
728
729 NSString *text = interaction.body.toNSString();
730 text = [text removeEmptyLinesAtBorders];
731
732 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
733 CGFloat singleLignMessageHeight = 15;
734
735 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
736
737 if (shouldDisplayTime) {
738 return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
739 TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
740 }
741 if(shouldApplyPadding) {
742 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2 + 15,
743 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2 + 15);
744 }
745 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
746 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
747 } catch (std::out_of_range& e) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400748 return DEFAULT_ROW_HEIGHT;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400749 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400750}
751
752#pragma mark - message view parameters
753
754-(NSString *) getDataTransferPath:(uint64_t)interactionId {
755 lrc::api::datatransfer::Info info = {};
756 convModel_->getTransferInfo(interactionId, info);
757 double convertData = static_cast<double>(info.totalSize);
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400758 return info.path.toNSString();
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400759}
760
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400761-(NSImage*) getImageForFilePath: (NSString *) path {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400762 if (path.length <= 0) {return nil;}
763 if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
764 NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400765 return transferedImage;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400766}
767
768-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
769 CGFloat horizaontalMargin = 6;
Anthony Léonard2382b562017-12-13 15:51:28 -0500770 NSMutableAttributedString* msgAttString =
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400771 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
Anthony Léonard2382b562017-12-13 15:51:28 -0500772 attributes:[self messageAttributes]];
773
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400774 CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
775 NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400776 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400777 [[tv textStorage] setAttributedString:msgAttString];
778 [tv sizeToFit];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400779 return tv.frame.size;
780}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400781
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400782-(MessageSequencing) computeSequencingFor:(NSInteger) row {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400783 try {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400784 auto* conv = [self getCurrentConversation];
785 if (row >= conversationView.numberOfRows - 1 || row >= conv->interactions.size()) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400786 return SINGLE_WITHOUT_TIME;
787 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400788 if (conv == nil)
789 return SINGLE_WITHOUT_TIME;
790 auto it = conv->interactions.begin();
791 std::advance(it, row);
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400792 if (it == conv->interactions.end()) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400793 return SINGLE_WITHOUT_TIME;
794 }
795 auto interaction = it->second;
796 if (interaction.type != lrc::api::interaction::Type::TEXT) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400797 return SINGLE_WITH_TIME;
798 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400799 // first message in comversation
800 if (row == 0) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400801 if (it == conv->interactions.end() || conv->interactions.size() < 2) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400802 return SINGLE_WITH_TIME;
803 }
804 auto nextIt = it;
805 nextIt++;
806 if (nextIt == conv->interactions.end()) {
807 return SINGLE_WITH_TIME;
808 }
809 auto nextInteraction = nextIt->second;
810 if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
811 return SINGLE_WITH_TIME;
812 }
813 return FIRST_WITH_TIME;
814 }
815 // last message in comversation
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400816 if (row == conv->interactions.size() - 1) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400817 if(it == conv->interactions.begin()) {
818 return SINGLE_WITH_TIME;
819 }
820 auto previousIt = it;
821 previousIt--;
822 auto previousInteraction = previousIt->second;
823 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
824 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
825 if (!timeChanged && !authorChanged) {
826 return LAST_IN_SEQUENCE;
827 }
828 if (!timeChanged && authorChanged) {
829 return SINGLE_WITHOUT_TIME;
830 }
Kateryna Kostiukcd9397f2020-04-13 09:40:53 -0400831 return SINGLE_WITH_TIME;
832 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400833 // single message in comversation
834 if(it == conv->interactions.begin() || it == conv->interactions.end()) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400835 return SINGLE_WITH_TIME;
836 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400837 // message in the middle of conversation
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400838 auto previousIt = it;
839 previousIt--;
840 auto previousInteraction = previousIt->second;
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400841 auto nextIt = it;
842 nextIt++;
843 if (nextIt == conv->interactions.end()) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400844 return SINGLE_WITHOUT_TIME;
845 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400846 auto nextInteraction = nextIt->second;
847
848 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
849 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
850 bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
851 if (previousInteraction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
852 if(!sequenceWillChange) {
853 return FIRST_WITH_TIME;
854 }
855 return SINGLE_WITH_TIME;
856 }
857 if (!sequenceWillChange) {
858 if (!timeChanged && !authorChanged) {
859 return MIDDLE_IN_SEQUENCE;
860 }
861 if (timeChanged) {
862 return FIRST_WITH_TIME;
863 }
864 return FIRST_WITHOUT_TIME;
865 } if (!timeChanged && !authorChanged) {
866 return LAST_IN_SEQUENCE;
867 } if (timeChanged) {
868 return SINGLE_WITH_TIME;
869 }
870 return SINGLE_WITHOUT_TIME;
871 } catch (std::out_of_range& e) {
Kateryna Kostiukcd9397f2020-04-13 09:40:53 -0400872 return SINGLE_WITHOUT_TIME;
873 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400874}
875
876-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
877 return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
878}
879
880-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
881 bool timeChanged = NO;
882 NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
883 NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
884 bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
885 bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
886 if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
887 timeChanged = YES;
888 }
889 return timeChanged;
890}
891
892-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
893 bool authorChanged = YES;
894 bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
895 if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
896 authorChanged = NO;
897 }
898 return authorChanged;
899}
900
901-(NSString *)timeForMessage:(NSDate*) msgTime {
902 NSDate *today = [NSDate date];
903 NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
Kateryna Kostiukaf6d5e22018-06-12 15:00:00 -0400904 [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[NSLocale currentLocale] localeIdentifier]]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400905 if ([[NSCalendar currentCalendar] compareDate:today
906 toDate:msgTime
907 toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500908 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterLongStyle timeStyle:NSDateFormatterMediumStyle];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400909 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400910 if ([[NSCalendar currentCalendar] compareDate:today
911 toDate:msgTime
912 toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
913 [[NSCalendar currentCalendar] compareDate:today
914 toDate:msgTime
915 toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500916 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterShortStyle];
917 }
918 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterNoStyle timeStyle:NSDateFormatterShortStyle];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400919}
920
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400921- (void) updateSendMessageHeight {
kkostiuk26f99fb2021-02-17 15:06:56 -0500922 NSAttributedString *msgAttString = messageView.attributedString;
kkostiuk153c2692021-02-23 21:30:23 -0500923 if (!msgAttString || msgAttString.length == 0) {
924 [self resetSendMessagePanelToDefaultSize];
kkostiuk26f99fb2021-02-17 15:06:56 -0500925 return;
926 }
927 NSRect frame = NSMakeRect(0, 0, messageView.frame.size.width, msgAttString.size.height);
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400928 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
929 [[tv textStorage] setAttributedString:msgAttString];
930 [tv sizeToFit];
kkostiuk153c2692021-02-23 21:30:23 -0500931 // check the height of one line and update default line height if it does not match
932 NSAttributedString *firstLetter = [msgAttString attributedSubstringFromRange:NSMakeRange(0, 1)];
933 auto lineHeight = firstLetter.size.height;
934 // we do not want to update constraints if number of lines does not change. Save difference between actual line height
935 // and default height and use it after to check messageHeight.constant
936 auto accuracy = abs(lineHeight - MESSAGE_VIEW_DEFAULT_HEIGHT);
937 if (MESSAGE_VIEW_DEFAULT_HEIGHT != lineHeight) {
938 MESSAGE_VIEW_DEFAULT_HEIGHT = lineHeight;
939 }
940 // 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 -0500941 auto top = tv.frame.size.height > MESSAGE_VIEW_DEFAULT_HEIGHT ? TOP_MARGIN_MIN : TOP_MARGIN;
942 auto bottom = tv.frame.size.height > MESSAGE_VIEW_DEFAULT_HEIGHT ? BOTTOM_MARGIN_MIN : BOTTOM_MARGIN;
943 CGFloat heightWithMargins = tv.frame.size.height + top + bottom + SEND_PANEL_BOTTOM_MARGIN;
944 CGFloat newSendPanelHeight = MIN(SEND_PANEL_MAX_HEIGHT, MAX(SEND_PANEL_DEFAULT_HEIGHT, heightWithMargins));
945 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 -0500946 if (abs(messageHeight.constant - msgHeight) <= accuracy) {
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400947 return;
948 }
kkostiuk26f99fb2021-02-17 15:06:56 -0500949 messagesBottomMargin.constant = newSendPanelHeight;
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400950 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
951 [self scrollToBottom];
kkostiuk26f99fb2021-02-17 15:06:56 -0500952 messageHeight.constant = msgHeight;
953 textBottomConstraint.constant = bottom;
954 sendPanelHeight.constant = newSendPanelHeight;
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400955 });
956}
957
Anthony Léonard2382b562017-12-13 15:51:28 -0500958#pragma mark - NSTableViewDataSource
959
960- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
961{
962 auto* conv = [self getCurrentConversation];
963
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400964 // return conversation +1 view for composing indicator
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500965 if (conv)
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400966 return conv->interactions.size() + 1;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500967 else
968 return 0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500969}
970
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400971#pragma mark - Text formatting
972
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400973- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400974{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400975 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiukd508f752020-11-13 16:26:00 -0500976 NSFont *font = [NSFont systemFontOfSize: 12 weight: NSFontWeightLight];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400977 attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400978 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
Kateryna Kostiukd508f752020-11-13 16:26:00 -0500979 attrs[NSFontAttributeName] = font;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400980 return attrs;
981}
982
983- (NSParagraphStyle*) paragraphStyle
984{
985 /*
986 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
987 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
988 to copy, we use the default one.
989
990 The default values supplied by the default NSParagraphStyle are:
991 Alignment NSNaturalTextAlignment
992 Tab stops 12 left-aligned tabs, spaced by 28.0 points
993 Line break mode NSLineBreakByWordWrapping
994 All others 0.0
995 */
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400996 NSMutableParagraphStyle* aMutableParagraphStyle =
997 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
998 [aMutableParagraphStyle setHeadIndent:1.0];
999 [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
1000 return aMutableParagraphStyle;
1001}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001002
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001003#pragma mark - Actions
1004
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001005- (void)acceptIncomingFile:(id)sender {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001006 auto interId = [(IMTableCellView*)[[[[[[sender superview] superview] superview] superview] superview] superview] interaction];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001007 auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001008 if (convModel_ && !convUid_.isEmpty()) {
Kateryna Kostiuk26405ac2020-05-27 14:25:13 -04001009 convModel_->acceptTransfer(convUid_, interId);
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001010 }
1011}
1012
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001013- (void)declineIncomingFile:(id)sender {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001014 auto inter = [(IMTableCellView*)[[[[[[sender superview] superview] superview] superview] superview] superview] interaction];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001015 if (convModel_ && !convUid_.isEmpty()) {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001016 convModel_->cancelTransfer(convUid_, inter);
1017 }
1018}
1019
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001020- (void)imagePreview:(id)sender {
1021 uint64_t interId;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001022 if ([[[[[[sender superview] superview] superview] superview] superview] isKindOfClass:[IMTableCellView class]]) {
1023 interId = [(IMTableCellView*)[[[[[sender superview] superview] superview] superview] superview] interaction];
1024 } else if ([[[[[sender superview] superview] superview] superview] isKindOfClass:[IMTableCellView class]]) {
1025 interId = [(IMTableCellView*)[[[[sender superview] superview] superview] superview] interaction];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001026 } else {
1027 return;
1028 }
1029 auto it = [self getCurrentConversation]->interactions.find(interId);
1030 if (it == [self getCurrentConversation]->interactions.end()) {
1031 return;
1032 }
1033 auto& interaction = it->second;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001034 NSString* name = interaction.body.toNSString();
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001035 if (([name rangeOfString:@"/"].location == NSNotFound)) {
1036 name = [self getDataTransferPath:interId];
1037 }
1038 previewImage = name;
Kateryna Kostiuk30c6ac22020-05-06 17:42:59 -04001039 if (!previewImage || previewImage.length <= 0) {
1040 return;
1041 }
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001042 [self addToResponderChain];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001043 if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
1044 [[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
1045 } else {
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001046 dispatch_async(dispatch_get_main_queue(), ^{
1047 [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:self];
1048 });
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001049 }
1050}
1051
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001052- (IBAction)sendMessage:(id)sender {
1053 NSString* text = self.message;
Kateryna Kostiuka8b6b562019-02-01 13:26:18 -05001054 unichar separatorChar = NSLineSeparatorCharacter;
1055 NSString *separatorString = [NSString stringWithCharacters:&separatorChar length:1];
1056 text = [text stringByReplacingOccurrencesOfString: separatorString withString: @"\n"];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001057 if (text && text.length > 0) {
1058 auto* conv = [self getCurrentConversation];
Kateryna Kostiuk0c068552020-03-30 09:48:17 -04001059 if (conv == nil)
1060 return;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001061 convModel_->sendMessage(convUid_, QString::fromNSString(text));
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001062 self.message = @"";
kkostiuk26f99fb2021-02-17 15:06:56 -05001063 [self resetSendMessagePanelToDefaultSize];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001064 if (composingMessage) {
1065 composingMessage = false;
1066 convModel_->setIsComposing(convUid_, composingMessage);
1067 }
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001068 }
1069}
1070
kkostiuk26f99fb2021-02-17 15:06:56 -05001071-(void) resetSendMessagePanelToDefaultSize {
1072 if(messageHeight.constant != MESSAGE_VIEW_DEFAULT_HEIGHT) {
1073 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
1074 messageHeight.constant = MESSAGE_VIEW_DEFAULT_HEIGHT;
1075 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
1076 textBottomConstraint.constant = BOTTOM_MARGIN;
1077 [self scrollToBottom];
1078 }
1079}
1080
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001081- (IBAction)openEmojy:(id)sender {
kkostiuk26f99fb2021-02-17 15:06:56 -05001082 [messageView.window makeFirstResponder: messageView];
1083 [NSApp orderFrontCharacterPalette: messageView];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001084}
1085
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001086- (IBAction)startVideoMessage:(id)sender
1087{
1088 [self startRecording:NO];
1089}
1090
1091- (IBAction)startAudioMessage:(id)sender
1092{
1093 [self startRecording:YES];
1094}
1095-(void) startRecording:(BOOL)isAudio {
1096 if (recordingController == nil) {
1097 recordingController = [[RecordFileVC alloc] initWithNibName:@"RecordFileVC" bundle:nil avModel: self->avModel];
1098 recordingController.delegate = self;
1099 }
1100 if(recordMessagePopover != nil)
1101 {
1102 [self closeRecordingView];
1103 return;
1104 }
1105 recordMessagePopover = [[NSPopover alloc] init];
1106 [recordingController prepareRecordingView: isAudio];
1107 [recordMessagePopover setContentSize: recordingController.view.frame.size];
1108 [recordMessagePopover setContentViewController:recordingController];
1109 [recordMessagePopover setAnimates:YES];
1110 NSButton *anchorButton = isAudio ? recordAudioButton : recordVideoButton;
1111 [recordMessagePopover showRelativeToRect: anchorButton.bounds
1112 ofView: anchorButton
1113 preferredEdge: NSMaxYEdge];
1114}
1115
1116-(void) sendFile:(NSString *) name withFilePath:(NSString *) path {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001117 convModel_->sendFile(convUid_, QString::fromNSString(path), QString::fromNSString(name));
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001118}
1119
1120-(void) closeRecordingView {
1121 if(recordMessagePopover != nil) {
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001122 [recordMessagePopover close];
1123 recordMessagePopover = nil;
Kateryna Kostiuka7404812019-10-28 12:24:46 -04001124 recordingController.stopRecordingView;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001125 }
1126}
1127
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001128- (IBAction)sendFile:(id)sender {
1129 NSOpenPanel* filePicker = [NSOpenPanel openPanel];
1130 [filePicker setCanChooseFiles:YES];
1131 [filePicker setCanChooseDirectories:NO];
1132 [filePicker setAllowsMultipleSelection:NO];
1133
1134 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
1135 if ([[filePicker URLs] count] == 1) {
1136 NSURL* url = [[filePicker URLs] objectAtIndex:0];
1137 const char* fullPath = [url fileSystemRepresentation];
1138 NSString* fileName = [url lastPathComponent];
1139 if (convModel_) {
1140 auto* conv = [self getCurrentConversation];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001141 convModel_->sendFile(convUid_, QString::fromStdString(fullPath), QString::fromNSString(fileName));
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001142 }
1143 }
1144 }
1145}
1146
1147
kkostiuk26f99fb2021-02-17 15:06:56 -05001148#pragma mark - NSTextViewDelegate
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001149
kkostiuk26f99fb2021-02-17 15:06:56 -05001150- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001151 if (commandSelector == @selector(insertNewline:)) {
1152 if(self.message.length > 0) {
1153 [self sendMessage: nil];
kkostiuk26f99fb2021-02-17 15:06:56 -05001154 return YES;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001155 }
kkostiuk26f99fb2021-02-17 15:06:56 -05001156 [self resetSendMessagePanelToDefaultSize];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001157 return YES;
1158 }
1159 return NO;
1160}
1161
kkostiuk26f99fb2021-02-17 15:06:56 -05001162-(void) textDidChange:(NSNotification *)notification {
1163 [self checkIfComposingMsg];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001164}
1165
kkostiuk26f99fb2021-02-17 15:06:56 -05001166- (void) checkIfComposingMsg {
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001167 [self updateSendMessageHeight];
kkostiuk26f99fb2021-02-17 15:06:56 -05001168 BOOL haveText = [messageView.string removeEmptyLinesAtBorders].length != 0;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001169 if (haveText != composingMessage) {
1170 composingMessage = haveText;
1171 convModel_->setIsComposing(convUid_, composingMessage);
1172 }
1173}
1174
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001175#pragma mark - QLPreviewPanelDataSource
1176
1177-(void)beginPreviewPanelControl:(QLPreviewPanel *)panel
1178{
1179 panel.dataSource = self;
1180}
1181
1182- (void)endPreviewPanelControl:(QLPreviewPanel *)panel {
1183 panel.dataSource = nil;
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001184 [self removeFromResponderChain];
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001185}
1186
1187-(BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel
1188{
1189 return YES;
1190}
1191
1192- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel {
1193 return 1;
1194}
1195
1196- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index {
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001197 if (previewImage == nil) {
1198 return nil;
1199 }
Kateryna Kostiuk30c6ac22020-05-06 17:42:59 -04001200 try {
1201 return [NSURL fileURLWithPath: previewImage];
1202 } catch (NSException *exception) {
1203 nil;
1204 }
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001205}
1206
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001207- (void)addToResponderChain {
1208 if (conversationView.window &&
1209 ![[conversationView.window nextResponder] isEqual:self]) {
1210 NSResponder * aNextResponder = [conversationView.window nextResponder];
1211 [conversationView.window setNextResponder:self];
1212 }
1213}
1214
1215
1216- (void)removeFromResponderChain {
1217 if (conversationView.window &&
1218 [[conversationView.window nextResponder] isEqual:self]) {
1219 NSResponder * aNextResponder = [conversationView.window nextResponder];
1220 [conversationView.window setNextResponder:[self nextResponder]];
1221 }
1222}
1223
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001224@end