blob: e02f90d7e2c66e3e03b8da3a14dd37b0ce19d703 [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)) {
kkostiukd83b4742021-03-10 14:29:37 -0500176 [conversationView scrollToEndOfDocument: nil];
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 }
kkostiuke6c33db2021-02-24 16:20:50 -0500340 dispatch_async(dispatch_get_main_queue(), ^{
341 [messageView.window makeFirstResponder: messageView];
342 });
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500343 conversationView.alphaValue = 0.0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500344 [conversationView reloadData];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400345 [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -0500346 CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"];
347 fadeIn.fromValue = [NSNumber numberWithFloat:0.0];
348 fadeIn.toValue = [NSNumber numberWithFloat:1.0];
349 fadeIn.duration = 0.4f;
350
351 [conversationView.layer addAnimation:fadeIn forKey:fadeIn.keyPath];
352 conversationView.alphaValue = 1;
353 auto* conv = [self getCurrentConversation];
354
355 if (conv == nil)
356 return;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400357 try {
358 [sendFileButton setEnabled:(convModel_->owner.contactModel->getContact(conv->participants[0]).profileInfo.type != lrc::api::profile::Type::SIP)];
359 } catch (std::out_of_range& e) {
360 NSLog(@"contact out of range");
361 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400362}
363
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400364#pragma mark - configure cells
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400365
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400366-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500367{
368 NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
369 NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400370 NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500371
372 // TODO: Fix symbol in LRC
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500373 NSString* fixedString = [[text stringByReplacingOccurrencesOfString:@"🕽" withString:@""] stringByReplacingOccurrencesOfString:@"📞" withString:@""];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500374 [textField setStringValue:fixedString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400375 [timeField setStringValue:time];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500376
377 return result;
378}
379
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400380-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500381{
382 IMTableCellView* result;
383
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400384 auto type = interaction.type;
385 auto status = interaction.status;
386
387 NSString* fileName = @"incoming file";
388
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500389 // First, view is created
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400390 if (!interaction.authorUri.isEmpty()) {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500391 switch (status) {
392 case lrc::api::interaction::Status::TRANSFER_CREATED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400393 case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
394 result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
395 [result.acceptButton setAction:@selector(acceptIncomingFile:)];
396 [result.acceptButton setTarget:self];
397 [result.declineButton setAction:@selector(declineIncomingFile:)];
398 [result.declineButton setTarget:self];
399 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500400 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400401 case lrc::api::interaction::Status::TRANSFER_ONGOING: {
402 result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
403 [result.progressIndicator startAnimation:conversationView];
404 [result.declineButton setAction:@selector(declineIncomingFile:)];
405 [result.declineButton setTarget:self];
406 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500407 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400408 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
409 [result.transferedFileName setAction:@selector(imagePreview:)];
410 [result.transferedFileName setTarget:self];
411 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
412 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500413 case lrc::api::interaction::Status::TRANSFER_CANCELED:
414 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400415 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
416 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500417 }
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400418 } else {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400419 NSString* fileName = @"sent file";
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500420 switch (status) {
421 case lrc::api::interaction::Status::TRANSFER_CREATED:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500422 case lrc::api::interaction::Status::TRANSFER_ONGOING:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400423 case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500424 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400425 result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500426 [result.progressIndicator startAnimation:nil];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400427 [result.declineButton setAction:@selector(declineIncomingFile:)];
428 [result.declineButton setTarget:self];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500429 break;
430 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400431 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
432 [result.transferedFileName setAction:@selector(imagePreview:)];
433 [result.transferedFileName setTarget:self];
434 [result.transferedFileName.cell setHighlightsBy:NSContentsCellMask];
435 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500436 case lrc::api::interaction::Status::TRANSFER_CANCELED:
437 case lrc::api::interaction::Status::TRANSFER_ERROR:
Olivier Soldanoe521a182018-02-26 16:55:19 -0500438 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400439 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500440 }
441 }
442
443 // Then status label is updated if needed
444 switch (status) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400445 [result.statusLabel setTextColor:[NSColor textColor]];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500446 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400447 [result.statusLabel setTextColor:[NSColor greenSuccessColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500448 [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500449 break;
450 case lrc::api::interaction::Status::TRANSFER_CANCELED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400451 [result.statusLabel setTextColor:[NSColor orangeColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500452 [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500453 break;
454 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400455 [result.statusLabel setTextColor:[NSColor errorTransferColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500456 [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500457 break;
458 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400459 [result.statusLabel setTextColor:[NSColor textColor]];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500460 [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
461 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500462 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400463 result.transferedImage.image = nil;
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400464 [result.openImagebutton setHidden:YES];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400465 [result.msgBackground setHidden:NO];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400466 NSString* name = interaction.body.toNSString();
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400467 if (name.length > 0) {
Kateryna Kostiuk67735232018-05-10 15:05:32 -0400468 fileName = [name lastPathComponent];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400469 }
Kateryna Kostiukd508f752020-11-13 16:26:00 -0500470 NSFont *nameFont = [NSFont systemFontOfSize: 12 weight: NSFontWeightLight];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400471 NSColor *nameColor = [NSColor textColor];
472 NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
473 paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
474 paragraphStyle.alignment = NSTextAlignmentLeft;
475 NSDictionary *nameAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
476 nameColor,NSForegroundColorAttributeName,
477 paragraphStyle,NSParagraphStyleAttributeName, nil];
478 NSAttributedString* nameAttributedString = [[NSAttributedString alloc] initWithString:fileName attributes:nameAttr];
479 result.transferedFileName.attributedTitle = nameAttributedString;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400480 if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -0400481 NSColor *higlightColor = [NSColor grayColor];
482 NSDictionary *alternativeNametAttr = [NSDictionary dictionaryWithObjectsAndKeys:nameFont,NSFontAttributeName,
483 higlightColor,NSForegroundColorAttributeName,
484 paragraphStyle,NSParagraphStyleAttributeName, nil];
485 NSAttributedString* alternativeString = [[NSAttributedString alloc] initWithString:fileName attributes:alternativeNametAttr];
486 result.transferedFileName.attributedAlternateTitle = alternativeString;
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400487 NSImage* image = [self getImageForFilePath:name];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400488 if (([name rangeOfString:@"/"].location == NSNotFound)) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400489 image = [self getImageForFilePath:[self getDataTransferPath:interactionID]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400490 }
491 if(image != nil) {
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400492 result.transferedImage.image = image;
493 [result updateImageConstraintWithMax: MAX_TRANSFERED_IMAGE_SIZE];
494 [result.openImagebutton setAction:@selector(imagePreview:)];
495 [result.openImagebutton setTarget:self];
Kateryna Kostiukeaf1bc82018-10-12 14:33:50 -0400496 [result.openImagebutton setHidden:NO];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400497 }
498 }
499 [result setupForInteraction:interactionID];
500 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
501 NSString* timeString = [self timeForMessage: msgTime];
502 result.timeLabel.stringValue = timeString;
503 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
504 if (!isOutgoing) {
Kateryna Kostiukdc720842020-09-10 16:44:02 -0400505 @autoreleasepool {
506 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
507 auto* conv = [self getCurrentConversation];
508 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
509 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400510 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500511 return result;
512}
513
Anthony Léonard2382b562017-12-13 15:51:28 -0500514#pragma mark - NSTableViewDelegate methods
515- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400516{
517 return YES;
518}
519
Anthony Léonard2382b562017-12-13 15:51:28 -0500520- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400521{
Anthony Léonard2382b562017-12-13 15:51:28 -0500522 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400523}
524
Anthony Léonard2382b562017-12-13 15:51:28 -0500525- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400526{
Anthony Léonard2382b562017-12-13 15:51:28 -0500527 auto* conv = [self getCurrentConversation];
Anthony Léonard2382b562017-12-13 15:51:28 -0500528 if (conv == nil)
529 return nil;
530
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400531 IMTableCellView* result;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500532 auto it = conv->interactions.begin();
Kateryna Kostiuka1201922020-04-20 11:59:35 -0400533 auto size = [conversationView numberOfRows] - 1;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400534
535 if (row > size || row > conv->interactions.size()) {
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400536 return [[NSView alloc] init];
537 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400538
539 if (row == size) {
Kateryna Kostiukb39ca192020-03-27 14:01:00 -0400540 if (size < 1) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400541 return nil;
Kateryna Kostiukb39ca192020-03-27 14:01:00 -0400542 }
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400543 //last row peer composing view
544 result = [tableView makeViewWithIdentifier:@"PeerComposingMsgView" owner:conversationView];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400545 result.alphaValue = 0;
546 [result animateCompozingIndicator: NO];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400547 CGFloat alpha = peerComposingMessage ? 1 : 0;
548 CGFloat height = peerComposingMessage ? HEIGHT_FOR_COMPOSING_INDICATOR : DEFAULT_ROW_HEIGHT;
549 [result updateHeightConstraints: height];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400550 if (alpha == 1) {
551 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
552 if (peerComposingMessage) {
553 result.alphaValue = alpha;
554 [result animateCompozingIndicator: YES];
555 }
556 });
557 }
558 return result;
559 }
Anthony Léonard2382b562017-12-13 15:51:28 -0500560
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500561 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500562
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400563 if (it == conv->interactions.end()) {
564 return [[NSView alloc] init];
565 }
566
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400567 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500568 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
569
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500570 switch (interaction.type) {
571 case lrc::api::interaction::Type::TEXT:
572 if (isOutgoing) {
573 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
574 } else {
575 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
576 }
577 break;
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400578 case lrc::api::interaction::Type::DATA_TRANSFER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400579 return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500580 break;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500581 case lrc::api::interaction::Type::CONTACT:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400582 case lrc::api::interaction::Type::CALL: {
583 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
584 NSString* timeString = [self timeForMessage: msgTime];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400585 return [self makeGenericInteractionViewForTableView:tableView withText:interaction.body.toNSString() andTime:timeString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400586 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500587 default: // If interaction is not of a known type
588 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400589 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400590 MessageSequencing sequence = [self computeSequencingFor:row];
591 BubbleType type = SINGLE;
592 if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
593 type = FIRST;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400594 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400595 if (sequence == MIDDLE_IN_SEQUENCE) {
596 type = MIDDLE;
597 }
598 if (sequence == LAST_IN_SEQUENCE) {
599 type = LAST;
600 }
601 result.msgBackground.type = type;
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400602 bool sendingFail = false;
603 [result.messageStatus setHidden:YES];
604 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
605 if (interaction.status == lrc::api::interaction::Status::SENDING) {
606 [result.messageStatus setHidden:NO];
607 [result.sendingMessageIndicator startAnimation:nil];
608 [result.messageFailed setHidden:YES];
Kateryna Kostiuk209a6302019-08-14 16:46:21 -0400609 } else if (interaction.status == lrc::api::interaction::Status::FAILURE) {
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400610 [result.messageStatus setHidden:NO];
611 [result.sendingMessageIndicator setHidden:YES];
612 [result.messageFailed setHidden:NO];
613 sendingFail = true;
614 }
615 }
616 [result setupForInteraction:it->first isFailed: sendingFail];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400617 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400618 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400619 [result.msgBackground setNeedsDisplay:YES];
620 [result setNeedsDisplay:YES];
621 [result.timeBox setNeedsDisplay:YES];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400622
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400623 NSString *text = interaction.body.toNSString();
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400624 text = [text removeEmptyLinesAtBorders];
625
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400626 NSMutableAttributedString* msgAttString =
Kateryna Kostiuk5a41a8d2020-05-13 09:05:56 -0400627 [[NSMutableAttributedString alloc] initWithString:text
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400628 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400629
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400630 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400631
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400632 [result updateMessageConstraint:messageSize.width andHeight:messageSize.height timeIsVisible:shouldDisplayTime isTopPadding: shouldApplyPadding];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400633 [[result.msgView textStorage] appendAttributedString:msgAttString];
Anthony Léonard2382b562017-12-13 15:51:28 -0500634
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400635 NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
636 NSArray *matches = [linkDetector matchesInString:result.msgView.string options:0 range:NSMakeRange(0, result.msgView.string.length)];
637
638 [result.msgView.textStorage beginEditing];
639
640 for (NSTextCheckingResult *match in matches) {
641 if (!match.URL) continue;
642
643 NSDictionary *linkAttributes = @{
644 NSLinkAttributeName: match.URL,
645 };
646 [result.msgView.textStorage addAttributes:linkAttributes range:match.range];
647 }
648
649 [result.msgView.textStorage endEditing];
650
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400651 if (shouldDisplayTime) {
652 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
653 NSString* timeString = [self timeForMessage: msgTime];
654 result.timeLabel.stringValue = timeString;
Anthony Léonard64e19672018-01-18 16:40:34 -0500655 }
656
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400657 bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
658 && sequence != FIRST_WITH_TIME) ? YES : NO;
659 [result.photoView setHidden:!shouldDisplayAvatar];
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400660 BOOL showIndicator = convModel_->isLastDisplayed(convUid_, it->first, conv->participants.front());
661 [result.readIndicator setHidden: !showIndicator];
Kateryna Kostiukdc720842020-09-10 16:44:02 -0400662 @autoreleasepool {
663 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
664 auto image = QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)));
665 [result.readIndicator setImage:image];
666 if (!isOutgoing && shouldDisplayAvatar) {
667 [result.photoView setImage:image];
668 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400669 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400670 return result;
671}
672
Anthony Léonard2382b562017-12-13 15:51:28 -0500673- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400674{
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400675 try {
676 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400677
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400678 auto* conv = [self getCurrentConversation];
Anthony Léonard2382b562017-12-13 15:51:28 -0500679
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400680 if (conv == nil)
681 return HEIGHT_DEFAULT;
Kateryna Kostiuk0c068552020-03-30 09:48:17 -0400682
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400683 auto size = [conversationView numberOfRows] - 1;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400684
685 if (row > size || row > conv->interactions.size()) {
686 return HEIGHT_DEFAULT;
687 }
688 if (row == size) {
689 return peerComposingMessage ? HEIGHT_FOR_COMPOSING_INDICATOR : DEFAULT_ROW_HEIGHT;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400690 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400691
692 auto it = conv->interactions.begin();
693
694 std::advance(it, row);
695
696 if (it == conv->interactions.end()) {
697 return HEIGHT_DEFAULT;
698 }
699
700 auto interaction = it->second;
701
702 MessageSequencing sequence = [self computeSequencingFor:row];
703
704 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
705
706 if(interaction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
707 if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
708 NSString* name = interaction.body.toNSString();
709 NSImage* image = [self getImageForFilePath:name];
710 if (([name rangeOfString:@"/"].location == NSNotFound)) {
711 image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
712 }
713 if (image != nil) {
714 CGFloat widthScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.width;
715 CGFloat heightScaleFactor = MAX_TRANSFERED_IMAGE_SIZE / image.size.height;
716 CGFloat heigt = 0;
717 if((widthScaleFactor >= 1) && (heightScaleFactor >= 1)) {
718 heigt = image.size.height;
719 } else {
720 CGFloat scale = MIN(widthScaleFactor, heightScaleFactor);
721 heigt = image.size.height * scale;
722 }
723 return heigt + TIME_BOX_HEIGHT;
724 }
725 }
726 return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
727 }
728
729 if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
730 return GENERIC_CELL_HEIGHT;
731
732 NSString *text = interaction.body.toNSString();
733 text = [text removeEmptyLinesAtBorders];
734
735 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
736 CGFloat singleLignMessageHeight = 15;
737
738 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
739
740 if (shouldDisplayTime) {
741 return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
742 TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
743 }
744 if(shouldApplyPadding) {
745 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2 + 15,
746 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2 + 15);
747 }
748 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
749 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
750 } catch (std::out_of_range& e) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400751 return DEFAULT_ROW_HEIGHT;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400752 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400753}
754
755#pragma mark - message view parameters
756
757-(NSString *) getDataTransferPath:(uint64_t)interactionId {
758 lrc::api::datatransfer::Info info = {};
759 convModel_->getTransferInfo(interactionId, info);
760 double convertData = static_cast<double>(info.totalSize);
Kateryna Kostiukc867eb92020-03-08 13:15:17 -0400761 return info.path.toNSString();
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400762}
763
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400764-(NSImage*) getImageForFilePath: (NSString *) path {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400765 if (path.length <= 0) {return nil;}
766 if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
767 NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
Kateryna Kostiukefc665d2018-09-17 15:42:43 -0400768 return transferedImage;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400769}
770
771-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
772 CGFloat horizaontalMargin = 6;
Anthony Léonard2382b562017-12-13 15:51:28 -0500773 NSMutableAttributedString* msgAttString =
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400774 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
Anthony Léonard2382b562017-12-13 15:51:28 -0500775 attributes:[self messageAttributes]];
776
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400777 CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
778 NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400779 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400780 [[tv textStorage] setAttributedString:msgAttString];
781 [tv sizeToFit];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400782 return tv.frame.size;
783}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400784
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400785-(MessageSequencing) computeSequencingFor:(NSInteger) row {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400786 try {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400787 auto* conv = [self getCurrentConversation];
788 if (row >= conversationView.numberOfRows - 1 || row >= conv->interactions.size()) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400789 return SINGLE_WITHOUT_TIME;
790 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400791 if (conv == nil)
792 return SINGLE_WITHOUT_TIME;
793 auto it = conv->interactions.begin();
794 std::advance(it, row);
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400795 if (it == conv->interactions.end()) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400796 return SINGLE_WITHOUT_TIME;
797 }
798 auto interaction = it->second;
799 if (interaction.type != lrc::api::interaction::Type::TEXT) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400800 return SINGLE_WITH_TIME;
801 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400802 // first message in comversation
803 if (row == 0) {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400804 if (it == conv->interactions.end() || conv->interactions.size() < 2) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400805 return SINGLE_WITH_TIME;
806 }
807 auto nextIt = it;
808 nextIt++;
809 if (nextIt == conv->interactions.end()) {
810 return SINGLE_WITH_TIME;
811 }
812 auto nextInteraction = nextIt->second;
813 if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
814 return SINGLE_WITH_TIME;
815 }
816 return FIRST_WITH_TIME;
817 }
818 // last message in comversation
Kateryna Kostiuk456cd812020-06-05 15:10:01 -0400819 if (row == conv->interactions.size() - 1) {
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400820 if(it == conv->interactions.begin()) {
821 return SINGLE_WITH_TIME;
822 }
823 auto previousIt = it;
824 previousIt--;
825 auto previousInteraction = previousIt->second;
826 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
827 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
828 if (!timeChanged && !authorChanged) {
829 return LAST_IN_SEQUENCE;
830 }
831 if (!timeChanged && authorChanged) {
832 return SINGLE_WITHOUT_TIME;
833 }
Kateryna Kostiukcd9397f2020-04-13 09:40:53 -0400834 return SINGLE_WITH_TIME;
835 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400836 // single message in comversation
837 if(it == conv->interactions.begin() || it == conv->interactions.end()) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400838 return SINGLE_WITH_TIME;
839 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400840 // message in the middle of conversation
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400841 auto previousIt = it;
842 previousIt--;
843 auto previousInteraction = previousIt->second;
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400844 auto nextIt = it;
845 nextIt++;
846 if (nextIt == conv->interactions.end()) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400847 return SINGLE_WITHOUT_TIME;
848 }
Kateryna Kostiukbea05222020-05-29 11:20:33 -0400849 auto nextInteraction = nextIt->second;
850
851 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
852 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
853 bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
854 if (previousInteraction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
855 if(!sequenceWillChange) {
856 return FIRST_WITH_TIME;
857 }
858 return SINGLE_WITH_TIME;
859 }
860 if (!sequenceWillChange) {
861 if (!timeChanged && !authorChanged) {
862 return MIDDLE_IN_SEQUENCE;
863 }
864 if (timeChanged) {
865 return FIRST_WITH_TIME;
866 }
867 return FIRST_WITHOUT_TIME;
868 } if (!timeChanged && !authorChanged) {
869 return LAST_IN_SEQUENCE;
870 } if (timeChanged) {
871 return SINGLE_WITH_TIME;
872 }
873 return SINGLE_WITHOUT_TIME;
874 } catch (std::out_of_range& e) {
Kateryna Kostiukcd9397f2020-04-13 09:40:53 -0400875 return SINGLE_WITHOUT_TIME;
876 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400877}
878
879-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
880 return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
881}
882
883-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
884 bool timeChanged = NO;
885 NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
886 NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
887 bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
888 bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
889 if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
890 timeChanged = YES;
891 }
892 return timeChanged;
893}
894
895-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
896 bool authorChanged = YES;
897 bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
898 if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
899 authorChanged = NO;
900 }
901 return authorChanged;
902}
903
904-(NSString *)timeForMessage:(NSDate*) msgTime {
905 NSDate *today = [NSDate date];
906 NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
Kateryna Kostiukaf6d5e22018-06-12 15:00:00 -0400907 [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[NSLocale currentLocale] localeIdentifier]]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400908 if ([[NSCalendar currentCalendar] compareDate:today
909 toDate:msgTime
910 toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500911 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterLongStyle timeStyle:NSDateFormatterMediumStyle];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400912 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400913 if ([[NSCalendar currentCalendar] compareDate:today
914 toDate:msgTime
915 toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
916 [[NSCalendar currentCalendar] compareDate:today
917 toDate:msgTime
918 toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
Kateryna Kostiuk66406432019-11-09 17:20:34 -0500919 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterShortStyle timeStyle:NSDateFormatterShortStyle];
920 }
921 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterNoStyle timeStyle:NSDateFormatterShortStyle];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400922}
923
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400924- (void) updateSendMessageHeight {
kkostiuk26f99fb2021-02-17 15:06:56 -0500925 NSAttributedString *msgAttString = messageView.attributedString;
kkostiuk153c2692021-02-23 21:30:23 -0500926 if (!msgAttString || msgAttString.length == 0) {
927 [self resetSendMessagePanelToDefaultSize];
kkostiuk26f99fb2021-02-17 15:06:56 -0500928 return;
929 }
930 NSRect frame = NSMakeRect(0, 0, messageView.frame.size.width, msgAttString.size.height);
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400931 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
932 [[tv textStorage] setAttributedString:msgAttString];
933 [tv sizeToFit];
kkostiuk153c2692021-02-23 21:30:23 -0500934 // check the height of one line and update default line height if it does not match
935 NSAttributedString *firstLetter = [msgAttString attributedSubstringFromRange:NSMakeRange(0, 1)];
936 auto lineHeight = firstLetter.size.height;
kkostiuke6c33db2021-02-24 16:20:50 -0500937 // 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 -0500938 auto accuracy = abs(lineHeight - MESSAGE_VIEW_DEFAULT_HEIGHT);
kkostiuk153c2692021-02-23 21:30:23 -0500939 // 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 -0500940 auto top = tv.frame.size.height > MESSAGE_VIEW_DEFAULT_HEIGHT ? TOP_MARGIN_MIN : TOP_MARGIN;
941 auto bottom = tv.frame.size.height > MESSAGE_VIEW_DEFAULT_HEIGHT ? BOTTOM_MARGIN_MIN : BOTTOM_MARGIN;
942 CGFloat heightWithMargins = tv.frame.size.height + top + bottom + SEND_PANEL_BOTTOM_MARGIN;
943 CGFloat newSendPanelHeight = MIN(SEND_PANEL_MAX_HEIGHT, MAX(SEND_PANEL_DEFAULT_HEIGHT, heightWithMargins));
944 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 -0500945 if (abs(messageHeight.constant - msgHeight) <= accuracy) {
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400946 return;
947 }
kkostiuke6c33db2021-02-24 16:20:50 -0500948 if (MESSAGE_VIEW_DEFAULT_HEIGHT != lineHeight) {
949 MESSAGE_VIEW_DEFAULT_HEIGHT = lineHeight;
950 }
kkostiuk26f99fb2021-02-17 15:06:56 -0500951 messagesBottomMargin.constant = newSendPanelHeight;
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400952 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
953 [self scrollToBottom];
kkostiuk26f99fb2021-02-17 15:06:56 -0500954 messageHeight.constant = msgHeight;
955 textBottomConstraint.constant = bottom;
956 sendPanelHeight.constant = newSendPanelHeight;
Kateryna Kostiukf6317422018-09-27 17:08:20 -0400957 });
958}
959
Anthony Léonard2382b562017-12-13 15:51:28 -0500960#pragma mark - NSTableViewDataSource
961
962- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
963{
964 auto* conv = [self getCurrentConversation];
965
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400966 // return conversation +1 view for composing indicator
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500967 if (conv)
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -0400968 return conv->interactions.size() + 1;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500969 else
970 return 0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500971}
972
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400973#pragma mark - Text formatting
974
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400975- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400976{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400977 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiukd508f752020-11-13 16:26:00 -0500978 NSFont *font = [NSFont systemFontOfSize: 12 weight: NSFontWeightLight];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400979 attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400980 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
Kateryna Kostiukd508f752020-11-13 16:26:00 -0500981 attrs[NSFontAttributeName] = font;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400982 return attrs;
983}
984
985- (NSParagraphStyle*) paragraphStyle
986{
987 /*
988 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
989 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
990 to copy, we use the default one.
991
992 The default values supplied by the default NSParagraphStyle are:
993 Alignment NSNaturalTextAlignment
994 Tab stops 12 left-aligned tabs, spaced by 28.0 points
995 Line break mode NSLineBreakByWordWrapping
996 All others 0.0
997 */
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400998 NSMutableParagraphStyle* aMutableParagraphStyle =
999 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1000 [aMutableParagraphStyle setHeadIndent:1.0];
1001 [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
1002 return aMutableParagraphStyle;
1003}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001004
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001005#pragma mark - Actions
1006
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001007- (void)acceptIncomingFile:(id)sender {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001008 auto interId = [(IMTableCellView*)[[[[[[sender superview] superview] superview] superview] superview] superview] interaction];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001009 auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001010 if (convModel_ && !convUid_.isEmpty()) {
Kateryna Kostiuk26405ac2020-05-27 14:25:13 -04001011 convModel_->acceptTransfer(convUid_, interId);
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001012 }
1013}
1014
Kateryna Kostiukae660fd2018-04-24 14:10:41 -04001015- (void)declineIncomingFile:(id)sender {
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001016 auto inter = [(IMTableCellView*)[[[[[[sender superview] superview] superview] superview] superview] superview] interaction];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001017 if (convModel_ && !convUid_.isEmpty()) {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05001018 convModel_->cancelTransfer(convUid_, inter);
1019 }
1020}
1021
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001022- (void)imagePreview:(id)sender {
1023 uint64_t interId;
Kateryna Kostiuk456cd812020-06-05 15:10:01 -04001024 if ([[[[[[sender superview] superview] superview] superview] superview] isKindOfClass:[IMTableCellView class]]) {
1025 interId = [(IMTableCellView*)[[[[[sender superview] superview] superview] superview] superview] interaction];
1026 } else if ([[[[[sender superview] superview] superview] superview] isKindOfClass:[IMTableCellView class]]) {
1027 interId = [(IMTableCellView*)[[[[sender superview] superview] superview] superview] interaction];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001028 } else {
1029 return;
1030 }
1031 auto it = [self getCurrentConversation]->interactions.find(interId);
1032 if (it == [self getCurrentConversation]->interactions.end()) {
1033 return;
1034 }
1035 auto& interaction = it->second;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001036 NSString* name = interaction.body.toNSString();
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001037 if (([name rangeOfString:@"/"].location == NSNotFound)) {
1038 name = [self getDataTransferPath:interId];
1039 }
1040 previewImage = name;
Kateryna Kostiuk30c6ac22020-05-06 17:42:59 -04001041 if (!previewImage || previewImage.length <= 0) {
1042 return;
1043 }
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001044 [self addToResponderChain];
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001045 if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
1046 [[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
1047 } else {
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001048 dispatch_async(dispatch_get_main_queue(), ^{
1049 [[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:self];
1050 });
Kateryna Kostiuk0f0ba992018-06-07 14:22:58 -04001051 }
1052}
1053
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001054- (IBAction)sendMessage:(id)sender {
1055 NSString* text = self.message;
Kateryna Kostiuka8b6b562019-02-01 13:26:18 -05001056 unichar separatorChar = NSLineSeparatorCharacter;
1057 NSString *separatorString = [NSString stringWithCharacters:&separatorChar length:1];
1058 text = [text stringByReplacingOccurrencesOfString: separatorString withString: @"\n"];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001059 if (text && text.length > 0) {
1060 auto* conv = [self getCurrentConversation];
Kateryna Kostiuk0c068552020-03-30 09:48:17 -04001061 if (conv == nil)
1062 return;
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001063 convModel_->sendMessage(convUid_, QString::fromNSString(text));
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001064 self.message = @"";
kkostiuk26f99fb2021-02-17 15:06:56 -05001065 [self resetSendMessagePanelToDefaultSize];
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001066 if (composingMessage) {
1067 composingMessage = false;
1068 convModel_->setIsComposing(convUid_, composingMessage);
1069 }
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001070 }
1071}
1072
kkostiuk26f99fb2021-02-17 15:06:56 -05001073-(void) resetSendMessagePanelToDefaultSize {
1074 if(messageHeight.constant != MESSAGE_VIEW_DEFAULT_HEIGHT) {
1075 sendPanelHeight.constant = SEND_PANEL_DEFAULT_HEIGHT;
1076 messageHeight.constant = MESSAGE_VIEW_DEFAULT_HEIGHT;
1077 messagesBottomMargin.constant = SEND_PANEL_DEFAULT_HEIGHT;
1078 textBottomConstraint.constant = BOTTOM_MARGIN;
1079 [self scrollToBottom];
1080 }
1081}
1082
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001083- (IBAction)openEmojy:(id)sender {
kkostiuk26f99fb2021-02-17 15:06:56 -05001084 [messageView.window makeFirstResponder: messageView];
1085 [NSApp orderFrontCharacterPalette: messageView];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001086}
1087
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001088- (IBAction)startVideoMessage:(id)sender
1089{
1090 [self startRecording:NO];
1091}
1092
1093- (IBAction)startAudioMessage:(id)sender
1094{
1095 [self startRecording:YES];
1096}
1097-(void) startRecording:(BOOL)isAudio {
1098 if (recordingController == nil) {
1099 recordingController = [[RecordFileVC alloc] initWithNibName:@"RecordFileVC" bundle:nil avModel: self->avModel];
1100 recordingController.delegate = self;
1101 }
1102 if(recordMessagePopover != nil)
1103 {
1104 [self closeRecordingView];
1105 return;
1106 }
1107 recordMessagePopover = [[NSPopover alloc] init];
1108 [recordingController prepareRecordingView: isAudio];
1109 [recordMessagePopover setContentSize: recordingController.view.frame.size];
1110 [recordMessagePopover setContentViewController:recordingController];
1111 [recordMessagePopover setAnimates:YES];
1112 NSButton *anchorButton = isAudio ? recordAudioButton : recordVideoButton;
1113 [recordMessagePopover showRelativeToRect: anchorButton.bounds
1114 ofView: anchorButton
1115 preferredEdge: NSMaxYEdge];
1116}
1117
1118-(void) sendFile:(NSString *) name withFilePath:(NSString *) path {
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001119 convModel_->sendFile(convUid_, QString::fromNSString(path), QString::fromNSString(name));
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001120}
1121
1122-(void) closeRecordingView {
1123 if(recordMessagePopover != nil) {
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001124 [recordMessagePopover close];
1125 recordMessagePopover = nil;
Kateryna Kostiuka7404812019-10-28 12:24:46 -04001126 recordingController.stopRecordingView;
Kateryna Kostiukfbe1b2f2019-10-07 17:32:26 -04001127 }
1128}
1129
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001130- (IBAction)sendFile:(id)sender {
1131 NSOpenPanel* filePicker = [NSOpenPanel openPanel];
1132 [filePicker setCanChooseFiles:YES];
1133 [filePicker setCanChooseDirectories:NO];
1134 [filePicker setAllowsMultipleSelection:NO];
1135
1136 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
1137 if ([[filePicker URLs] count] == 1) {
1138 NSURL* url = [[filePicker URLs] objectAtIndex:0];
1139 const char* fullPath = [url fileSystemRepresentation];
1140 NSString* fileName = [url lastPathComponent];
1141 if (convModel_) {
1142 auto* conv = [self getCurrentConversation];
Kateryna Kostiukc867eb92020-03-08 13:15:17 -04001143 convModel_->sendFile(convUid_, QString::fromStdString(fullPath), QString::fromNSString(fileName));
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001144 }
1145 }
1146 }
1147}
1148
1149
kkostiuk26f99fb2021-02-17 15:06:56 -05001150#pragma mark - NSTextViewDelegate
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001151
kkostiuk26f99fb2021-02-17 15:06:56 -05001152- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001153 if (commandSelector == @selector(insertNewline:)) {
1154 if(self.message.length > 0) {
1155 [self sendMessage: nil];
kkostiuk26f99fb2021-02-17 15:06:56 -05001156 return YES;
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001157 }
kkostiuk26f99fb2021-02-17 15:06:56 -05001158 [self resetSendMessagePanelToDefaultSize];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001159 return YES;
1160 }
1161 return NO;
1162}
1163
kkostiuk26f99fb2021-02-17 15:06:56 -05001164-(void) textDidChange:(NSNotification *)notification {
1165 [self checkIfComposingMsg];
Kateryna Kostiuk4f37d952018-12-04 13:19:17 -05001166}
1167
kkostiuk26f99fb2021-02-17 15:06:56 -05001168- (void) checkIfComposingMsg {
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001169 [self updateSendMessageHeight];
kkostiuk26f99fb2021-02-17 15:06:56 -05001170 BOOL haveText = [messageView.string removeEmptyLinesAtBorders].length != 0;
Kateryna Kostiuk10c896b2020-03-24 12:46:17 -04001171 if (haveText != composingMessage) {
1172 composingMessage = haveText;
1173 convModel_->setIsComposing(convUid_, composingMessage);
1174 }
1175}
1176
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001177#pragma mark - QLPreviewPanelDataSource
1178
1179-(void)beginPreviewPanelControl:(QLPreviewPanel *)panel
1180{
1181 panel.dataSource = self;
1182}
1183
1184- (void)endPreviewPanelControl:(QLPreviewPanel *)panel {
1185 panel.dataSource = nil;
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001186 [self removeFromResponderChain];
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001187}
1188
1189-(BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel
1190{
1191 return YES;
1192}
1193
1194- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel {
1195 return 1;
1196}
1197
1198- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index {
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001199 if (previewImage == nil) {
1200 return nil;
1201 }
Kateryna Kostiuk30c6ac22020-05-06 17:42:59 -04001202 try {
1203 return [NSURL fileURLWithPath: previewImage];
1204 } catch (NSException *exception) {
1205 nil;
1206 }
Kateryna Kostiukf6317422018-09-27 17:08:20 -04001207}
1208
Kateryna Kostiuka116a0a2020-07-09 11:39:22 -04001209- (void)addToResponderChain {
1210 if (conversationView.window &&
1211 ![[conversationView.window nextResponder] isEqual:self]) {
1212 NSResponder * aNextResponder = [conversationView.window nextResponder];
1213 [conversationView.window setNextResponder:self];
1214 }
1215}
1216
1217
1218- (void)removeFromResponderChain {
1219 if (conversationView.window &&
1220 [[conversationView.window nextResponder] isEqual:self]) {
1221 NSResponder * aNextResponder = [conversationView.window nextResponder];
1222 [conversationView.window setNextResponder:[self nextResponder]];
1223 }
1224}
1225
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001226@end