blob: 3a583f158bd58ff798932591844dd3ab067041c8 [file] [log] [blame]
Andreas Traczyk252a94a2018-04-20 16:36:20 -04001
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04002/*
Anthony Léonarde7d62ed2018-01-25 10:51:47 -05003 * Copyright (C) 2015-2018 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
22#import <QItemSelectionModel>
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040023#import <QPixmap>
24#import <QtMacExtras/qmacfunctions.h>
25
Anthony Léonard2382b562017-12-13 15:51:28 -050026// LRC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040027#import <globalinstances.h>
Anthony Léonard2382b562017-12-13 15:51:28 -050028#import <api/interaction.h>
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040029
30#import "MessagesVC.h"
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040031#import "views/IMTableCellView.h"
32#import "views/MessageBubbleView.h"
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040033#import "views/NSImage+Extensions.h"
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040034#import "INDSequentialTextSelectionManager.h"
Anthony Léonard2382b562017-12-13 15:51:28 -050035#import "delegates/ImageManipulationDelegate.h"
Anthony Léonard6f819752018-01-05 09:53:40 -050036#import "utils.h"
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040037#import "views/NSColor+RingTheme.h"
38
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040039
Anthony Léonard2382b562017-12-13 15:51:28 -050040@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource> {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040041
Anthony Léonard2382b562017-12-13 15:51:28 -050042 __unsafe_unretained IBOutlet NSTableView* conversationView;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040043
Anthony Léonard2382b562017-12-13 15:51:28 -050044 std::string convUid_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050045 lrc::api::ConversationModel* convModel_;
Anthony Léonard2382b562017-12-13 15:51:28 -050046 const lrc::api::conversation::Info* cachedConv_;
47
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050048 QMetaObject::Connection newInteractionSignal_;
Anthony Léonard2382b562017-12-13 15:51:28 -050049
50 // Both are needed to invalidate cached conversation as pointer
51 // may not be referencing the same conversation anymore
52 QMetaObject::Connection modelSortedSignal_;
53 QMetaObject::Connection filterChangedSignal_;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -050054 QMetaObject::Connection interactionStatusUpdatedSignal_;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040055}
56
57@property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager;
58
59@end
60
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050061// Tags for view
62NSInteger const GENERIC_INT_TEXT_TAG = 100;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040063NSInteger const GENERIC_INT_TIME_TAG = 200;
64
65// views size
66CGFloat const GENERIC_CELL_HEIGHT = 60;
67CGFloat const TIME_BOX_HEIGHT = 34;
68CGFloat const MESSAGE_TEXT_PADDING = 10;
69CGFloat const MAX_TRANSFERED_IMAGE_SIZE = 250;
70CGFloat const BUBBLE_HEIGHT_FOR_TRANSFERED_FILE = 87;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -050071
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040072@implementation MessagesVC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040073
Kateryna Kostiukae660fd2018-04-24 14:10:41 -040074//MessageBuble type
75typedef NS_ENUM(NSInteger, MessageSequencing) {
76 SINGLE_WITH_TIME = 0,
77 SINGLE_WITHOUT_TIME = 1,
78 FIRST_WITH_TIME = 2,
79 FIRST_WITHOUT_TIME = 3,
80 MIDDLE_IN_SEQUENCE = 5,
81 LAST_IN_SEQUENCE = 6,
82};
83
84- (void)awakeFromNib
85{
86 NSNib *cellNib = [[NSNib alloc] initWithNibNamed:@"MessageCells" bundle:nil];
87 [conversationView registerNib:cellNib forIdentifier:@"LeftIncomingFileView"];
88 [conversationView registerNib:cellNib forIdentifier:@"LeftOngoingFileView"];
89 [conversationView registerNib:cellNib forIdentifier:@"LeftFinishedFileView"];
90 [conversationView registerNib:cellNib forIdentifier:@"RightOngoingFileView"];
91 [conversationView registerNib:cellNib forIdentifier:@"RightFinishedFileView"];
92}
Andreas Traczyk252a94a2018-04-20 16:36:20 -040093-(void) clearData {
94 cachedConv_ = nil;
95 convUid_ = "";
96 convModel_ = nil;
97
98 QObject::disconnect(modelSortedSignal_);
99 QObject::disconnect(filterChangedSignal_);
100 QObject::disconnect(interactionStatusUpdatedSignal_);
101 QObject::disconnect(newInteractionSignal_);
102}
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400103
Anthony Léonard2382b562017-12-13 15:51:28 -0500104-(const lrc::api::conversation::Info*) getCurrentConversation
105{
106 if (convModel_ == nil || convUid_.empty())
107 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400108
Anthony Léonard2382b562017-12-13 15:51:28 -0500109 if (cachedConv_ != nil)
110 return cachedConv_;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400111
Anthony Léonard6f819752018-01-05 09:53:40 -0500112 auto it = getConversationFromUid(convUid_, *convModel_);
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400113 if (it != convModel_->allFilteredConversations().end())
Anthony Léonard2382b562017-12-13 15:51:28 -0500114 cachedConv_ = &(*it);
115
116 return cachedConv_;
117}
118
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400119-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update {
120 auto* conv = [self getCurrentConversation];
121
122 if (conv == nil)
123 return;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400124 auto it = conv->interactions.find(uid);
125 if (it == conv->interactions.end()) {
126 return;
127 }
128 auto itIndex = distance(conv->interactions.begin(),it);
129 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:itIndex];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400130 //reload previous message to update bubbleview
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400131 if (itIndex > 0) {
132 auto previousIt = it;
133 previousIt--;
134 auto previousInteraction = previousIt->second;
135 if (previousInteraction.type == lrc::api::interaction::Type::TEXT) {
136 NSRange range = NSMakeRange(itIndex - 1, 2);
137 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
138 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400139 }
140 if (update) {
141 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
142 }
143 [conversationView reloadDataForRowIndexes: indexSet
144 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400145 CGRect visibleRect = [conversationView enclosingScrollView].contentView.visibleRect;
146 NSRange range = [conversationView rowsInRect:visibleRect];
147 NSIndexSet* visibleIndexes = [NSIndexSet indexSetWithIndexesInRange:range];
148 NSUInteger lastvisibleRow = [visibleIndexes lastIndex];
149 if (([conversationView numberOfRows] > 0) &&
150 lastvisibleRow == ([conversationView numberOfRows] -1)) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400151 [conversationView scrollToEndOfDocument:nil];
152 }
153}
154
155-(void) reloadConversationForMessage:(uint64_t) uid shouldUpdateHeight:(bool)update updateConversation:(bool) updateConversation {
156 auto* conv = [self getCurrentConversation];
157
158 if (conv == nil)
159 return;
160 auto it = distance(conv->interactions.begin(),conv->interactions.find(uid));
161 NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:it];
162 //reload previous message to update bubbleview
163 if (it > 0) {
164 NSRange range = NSMakeRange(it - 1, it);
165 indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
166 }
167 if (update) {
168 [conversationView noteHeightOfRowsWithIndexesChanged:indexSet];
169 }
170 [conversationView reloadDataForRowIndexes: indexSet
171 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
172 if (update) {
173 [conversationView scrollToEndOfDocument:nil];
174 }
175}
176
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500177-(void)setConversationUid:(const std::string)convUid model:(lrc::api::ConversationModel *)model
Anthony Léonard2382b562017-12-13 15:51:28 -0500178{
179 if (convUid_ == convUid && convModel_ == model)
180 return;
181
182 cachedConv_ = nil;
183 convUid_ = convUid;
184 convModel_ = model;
185
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500186 // Signal triggered when messages are received or their status updated
187 QObject::disconnect(newInteractionSignal_);
188 QObject::disconnect(interactionStatusUpdatedSignal_);
189 newInteractionSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newInteraction,
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400190 [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
191 if (uid != convUid_)
192 return;
193 cachedConv_ = nil;
194 [conversationView noteNumberOfRowsChanged];
195 [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
196 });
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500197 interactionStatusUpdatedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::interactionStatusUpdated,
198 [self](const std::string& uid, uint64_t interactionId, const lrc::api::interaction::Info& interaction){
199 if (uid != convUid_)
200 return;
201 cachedConv_ = nil;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400202 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
203 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
204 convModel_->refreshFilter();
205 }
206 [self reloadConversationForMessage:interactionId shouldUpdateHeight:YES];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500207 });
Anthony Léonard2382b562017-12-13 15:51:28 -0500208
209 // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
210 // after a reordering.
211 QObject::disconnect(modelSortedSignal_);
212 QObject::disconnect(filterChangedSignal_);
213 modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelSorted,
214 [self](){
215 cachedConv_ = nil;
216 });
217 filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
218 [self](){
219 cachedConv_ = nil;
220 });
Anthony Léonard2382b562017-12-13 15:51:28 -0500221 [conversationView reloadData];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400222 [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400223}
224
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400225#pragma mark - configure cells
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400226
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400227-(NSTableCellView*) makeGenericInteractionViewForTableView:(NSTableView*)tableView withText:(NSString*)text andTime:(NSString*) time
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500228{
229 NSTableCellView* result = [tableView makeViewWithIdentifier:@"GenericInteractionView" owner:self];
230 NSTextField* textField = [result viewWithTag:GENERIC_INT_TEXT_TAG];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400231 NSTextField* timeField = [result viewWithTag:GENERIC_INT_TIME_TAG];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500232
233 // TODO: Fix symbol in LRC
234 NSString* fixedString = [text stringByReplacingOccurrencesOfString:@"🕽" withString:@"📞"];
235 [textField setStringValue:fixedString];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400236 [timeField setStringValue:time];
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500237
238 return result;
239}
240
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400241-(NSTableCellView*) configureViewforTransfer:(lrc::api::interaction::Info)interaction interactionID: (uint64_t) interactionID tableView:(NSTableView*)tableView
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500242{
243 IMTableCellView* result;
244
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400245 auto type = interaction.type;
246 auto status = interaction.status;
247
248 NSString* fileName = @"incoming file";
249
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500250 // First, view is created
251 if (type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
252 switch (status) {
253 case lrc::api::interaction::Status::TRANSFER_CREATED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400254 case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: {
255 result = [tableView makeViewWithIdentifier:@"LeftIncomingFileView" owner: conversationView];
256 [result.acceptButton setAction:@selector(acceptIncomingFile:)];
257 [result.acceptButton setTarget:self];
258 [result.declineButton setAction:@selector(declineIncomingFile:)];
259 [result.declineButton setTarget:self];
260 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500261 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400262 case lrc::api::interaction::Status::TRANSFER_ONGOING: {
263 result = [tableView makeViewWithIdentifier:@"LeftOngoingFileView" owner:conversationView];
264 [result.progressIndicator startAnimation:conversationView];
265 [result.declineButton setAction:@selector(declineIncomingFile:)];
266 [result.declineButton setTarget:self];
267 break;}
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500268 case lrc::api::interaction::Status::TRANSFER_FINISHED:
269 case lrc::api::interaction::Status::TRANSFER_CANCELED:
270 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400271 result = [tableView makeViewWithIdentifier:@"LeftFinishedFileView" owner:conversationView];
272 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500273 }
274 } else if (type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400275 NSString* fileName = @"sent file";
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500276 switch (status) {
277 case lrc::api::interaction::Status::TRANSFER_CREATED:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500278 case lrc::api::interaction::Status::TRANSFER_ONGOING:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400279 case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500280 case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400281 result = [tableView makeViewWithIdentifier:@"RightOngoingFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500282 [result.progressIndicator startAnimation:nil];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400283 [result.declineButton setAction:@selector(declineIncomingFile:)];
284 [result.declineButton setTarget:self];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500285 break;
286 case lrc::api::interaction::Status::TRANSFER_FINISHED:
287 case lrc::api::interaction::Status::TRANSFER_CANCELED:
288 case lrc::api::interaction::Status::TRANSFER_ERROR:
Olivier Soldanoe521a182018-02-26 16:55:19 -0500289 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400290 result = [tableView makeViewWithIdentifier:@"RightFinishedFileView" owner:conversationView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500291 }
292 }
293
294 // Then status label is updated if needed
295 switch (status) {
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400296 [result.statusLabel setTextColor:[NSColor textColor]];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500297 case lrc::api::interaction::Status::TRANSFER_FINISHED:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400298 [result.statusLabel setTextColor:[NSColor greenSuccessColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500299 [result.statusLabel setStringValue:NSLocalizedString(@"Success", @"File transfer successful label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500300 break;
301 case lrc::api::interaction::Status::TRANSFER_CANCELED:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400302 [result.statusLabel setTextColor:[NSColor orangeColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500303 [result.statusLabel setStringValue:NSLocalizedString(@"Canceled", @"File transfer canceled label")];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500304 break;
305 case lrc::api::interaction::Status::TRANSFER_ERROR:
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400306 [result.statusLabel setTextColor:[NSColor errorTransferColor]];
Anthony Léonard70638f02018-02-05 11:10:19 -0500307 [result.statusLabel setStringValue:NSLocalizedString(@"Failed", @"File transfer failed label")];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500308 break;
309 case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400310 [result.statusLabel setTextColor:[NSColor textColor]];
Olivier Soldanoe521a182018-02-26 16:55:19 -0500311 [result.statusLabel setStringValue:NSLocalizedString(@"Unjoinable", @"File transfer peer unjoinable label")];
312 break;
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500313 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400314 result.transferedImage.image = nil;
315 [result.msgBackground setHidden:NO];
316 [result invalidateImageConstraints];
317 NSString* name = @(interaction.body.c_str());
318 if (name.length > 0) {
Kateryna Kostiuk67735232018-05-10 15:05:32 -0400319 fileName = [name lastPathComponent];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400320 }
321 result.transferedFileName.stringValue = fileName;
322 if (status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
323 NSImage* image = [self getImageForFilePath:name];
324 if (([name rangeOfString:@"/"].location == NSNotFound)) {
325 image = [self getImageForFilePath:[self getDataTransferPath:interactionID]];
326 }
327 if(image != nil) {
328 result.transferedImage.image = [image roundCorners:14];
329 [result updateImageConstraint:image.size.width andHeight:image.size.height];
330 }
331 }
332 [result setupForInteraction:interactionID];
333 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
334 NSString* timeString = [self timeForMessage: msgTime];
335 result.timeLabel.stringValue = timeString;
336 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
337 if (!isOutgoing) {
338 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
339 auto* conv = [self getCurrentConversation];
340 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
341 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500342 return result;
343}
344
Anthony Léonard2382b562017-12-13 15:51:28 -0500345#pragma mark - NSTableViewDelegate methods
346- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400347{
348 return YES;
349}
350
Anthony Léonard2382b562017-12-13 15:51:28 -0500351- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400352{
Anthony Léonard2382b562017-12-13 15:51:28 -0500353 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400354}
355
Anthony Léonard2382b562017-12-13 15:51:28 -0500356- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400357{
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400358
Anthony Léonard2382b562017-12-13 15:51:28 -0500359 auto* conv = [self getCurrentConversation];
360
361 if (conv == nil)
362 return nil;
363
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500364 auto it = conv->interactions.begin();
Anthony Léonard2382b562017-12-13 15:51:28 -0500365
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500366 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500367
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400368 IMTableCellView* result;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400369 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500370 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
371
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500372 switch (interaction.type) {
373 case lrc::api::interaction::Type::TEXT:
374 if (isOutgoing) {
375 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
376 } else {
377 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
378 }
379 break;
380 case lrc::api::interaction::Type::INCOMING_DATA_TRANSFER:
381 case lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400382 return [self configureViewforTransfer:interaction interactionID: it->first tableView:tableView];
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500383 break;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500384 case lrc::api::interaction::Type::CONTACT:
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400385 case lrc::api::interaction::Type::CALL: {
386 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
387 NSString* timeString = [self timeForMessage: msgTime];
388 return [self makeGenericInteractionViewForTableView:tableView withText:@(interaction.body.c_str()) andTime:timeString];
389 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500390 default: // If interaction is not of a known type
391 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400392 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400393 MessageSequencing sequence = [self computeSequencingFor:row];
394 BubbleType type = SINGLE;
395 if (sequence == FIRST_WITHOUT_TIME || sequence == FIRST_WITH_TIME) {
396 type = FIRST;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400397 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400398 if (sequence == MIDDLE_IN_SEQUENCE) {
399 type = MIDDLE;
400 }
401 if (sequence == LAST_IN_SEQUENCE) {
402 type = LAST;
403 }
404 result.msgBackground.type = type;
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400405 bool sendingFail = false;
406 [result.messageStatus setHidden:YES];
407 if (interaction.type == lrc::api::interaction::Type::TEXT && isOutgoing) {
408 if (interaction.status == lrc::api::interaction::Status::SENDING) {
409 [result.messageStatus setHidden:NO];
410 [result.sendingMessageIndicator startAnimation:nil];
411 [result.messageFailed setHidden:YES];
412 } else if (interaction.status == lrc::api::interaction::Status::FAILED) {
413 [result.messageStatus setHidden:NO];
414 [result.sendingMessageIndicator setHidden:YES];
415 [result.messageFailed setHidden:NO];
416 sendingFail = true;
417 }
418 }
419 [result setupForInteraction:it->first isFailed: sendingFail];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400420 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400421 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400422 [result.msgBackground setNeedsDisplay:YES];
423 [result setNeedsDisplay:YES];
424 [result.timeBox setNeedsDisplay:YES];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400425
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400426 NSString *text = @(interaction.body.c_str());
427 text = [text removeEmptyLinesAtBorders];
428
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400429 NSMutableAttributedString* msgAttString =
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400430 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:text]
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400431 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400432
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400433 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400434
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400435 [result updateMessageConstraint:messageSize.width andHeight:messageSize.height timeIsVisible:shouldDisplayTime isTopPadding: shouldApplyPadding];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400436 [[result.msgView textStorage] appendAttributedString:msgAttString];
437 [result.msgView checkTextInDocument:nil];
Anthony Léonard2382b562017-12-13 15:51:28 -0500438
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400439 NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
440 NSArray *matches = [linkDetector matchesInString:result.msgView.string options:0 range:NSMakeRange(0, result.msgView.string.length)];
441
442 [result.msgView.textStorage beginEditing];
443
444 for (NSTextCheckingResult *match in matches) {
445 if (!match.URL) continue;
446
447 NSDictionary *linkAttributes = @{
448 NSLinkAttributeName: match.URL,
449 };
450 [result.msgView.textStorage addAttributes:linkAttributes range:match.range];
451 }
452
453 [result.msgView.textStorage endEditing];
454
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400455 if (shouldDisplayTime) {
456 NSDate* msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
457 NSString* timeString = [self timeForMessage: msgTime];
458 result.timeLabel.stringValue = timeString;
Anthony Léonard64e19672018-01-18 16:40:34 -0500459 }
460
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400461 bool shouldDisplayAvatar = (sequence != MIDDLE_IN_SEQUENCE && sequence != FIRST_WITHOUT_TIME
462 && sequence != FIRST_WITH_TIME) ? YES : NO;
463 [result.photoView setHidden:!shouldDisplayAvatar];
464 if (!isOutgoing && shouldDisplayAvatar) {
465 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
466 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
467 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400468 return result;
469}
470
Anthony Léonard2382b562017-12-13 15:51:28 -0500471- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400472{
Anthony Léonard2382b562017-12-13 15:51:28 -0500473 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400474
Anthony Léonard2382b562017-12-13 15:51:28 -0500475 auto* conv = [self getCurrentConversation];
476
477 if (conv == nil)
478 return 0;
479
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500480 auto it = conv->interactions.begin();
Anthony Léonard2382b562017-12-13 15:51:28 -0500481
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500482 std::advance(it, row);
Anthony Léonard2382b562017-12-13 15:51:28 -0500483
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400484 auto interaction = it->second;
Anthony Léonard2382b562017-12-13 15:51:28 -0500485
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400486 MessageSequencing sequence = [self computeSequencingFor:row];
487
488 bool shouldDisplayTime = (sequence == FIRST_WITH_TIME || sequence == SINGLE_WITH_TIME) ? YES : NO;
489
490
491 if(interaction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER || interaction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER) {
492
493 if( interaction.status == lrc::api::interaction::Status::TRANSFER_FINISHED) {
494 NSString* name = @(interaction.body.c_str());
495 NSImage* image = [self getImageForFilePath:name];
496 if (([name rangeOfString:@"/"].location == NSNotFound)) {
497 image = [self getImageForFilePath:[self getDataTransferPath:it->first]];
498 }
499 if (image != nil) {
500 return image.size.height + TIME_BOX_HEIGHT;
501 }
502 }
503 return BUBBLE_HEIGHT_FOR_TRANSFERED_FILE + TIME_BOX_HEIGHT;
504 }
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500505
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500506 if(interaction.type == lrc::api::interaction::Type::CONTACT || interaction.type == lrc::api::interaction::Type::CALL)
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400507 return GENERIC_CELL_HEIGHT;
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500508
Anthony Léonard2382b562017-12-13 15:51:28 -0500509 // TODO Implement interactions other than messages
510 if(interaction.type != lrc::api::interaction::Type::TEXT) {
511 return 0;
512 }
513
Kateryna Kostiuka0f16862018-05-04 09:11:41 -0400514 NSString *text = @(interaction.body.c_str());
515 text = [text removeEmptyLinesAtBorders];
516
517 CGSize messageSize = [self sizeFor: text maxWidth:tableView.frame.size.width * 0.7];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400518 CGFloat singleLignMessageHeight = 15;
519
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400520 bool shouldApplyPadding = (sequence == FIRST_WITHOUT_TIME || sequence == SINGLE_WITHOUT_TIME) ? YES : NO;
521
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400522 if (shouldDisplayTime) {
523 return MAX(messageSize.height + TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2,
524 TIME_BOX_HEIGHT + MESSAGE_TEXT_PADDING * 2 + singleLignMessageHeight);
525 }
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400526 if(shouldApplyPadding) {
527 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2 + 15,
528 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2 + 15);
529 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400530 return MAX(messageSize.height + MESSAGE_TEXT_PADDING * 2,
531 singleLignMessageHeight + MESSAGE_TEXT_PADDING * 2);
532}
533
534#pragma mark - message view parameters
535
536-(NSString *) getDataTransferPath:(uint64_t)interactionId {
537 lrc::api::datatransfer::Info info = {};
538 convModel_->getTransferInfo(interactionId, info);
539 double convertData = static_cast<double>(info.totalSize);
540 return @(info.path.c_str());
541}
542
543-(NSImage*) getImageForFilePath: (NSString *) path {
544 if (path.length <= 0) {return nil;}
545 if (![[NSFileManager defaultManager] fileExistsAtPath: path]) {return nil;}
546 NSImage* transferedImage = [[NSImage alloc] initWithContentsOfFile: path];
547 if(transferedImage != nil) {
548 return [transferedImage imageResizeInsideMax: MAX_TRANSFERED_IMAGE_SIZE];
549 }
550 return nil;
551}
552
553-(CGSize) sizeFor:(NSString *) message maxWidth:(CGFloat) width {
554 CGFloat horizaontalMargin = 6;
Anthony Léonard2382b562017-12-13 15:51:28 -0500555 NSMutableAttributedString* msgAttString =
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400556 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", message]
Anthony Léonard2382b562017-12-13 15:51:28 -0500557 attributes:[self messageAttributes]];
558
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400559 CGFloat finalWidth = MIN(msgAttString.size.width + horizaontalMargin * 2, width);
560 NSRect frame = NSMakeRect(0, 0, finalWidth, msgAttString.size.height);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400561 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400562 [[tv textStorage] setAttributedString:msgAttString];
563 [tv sizeToFit];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400564 return tv.frame.size;
565}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400566
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400567-(MessageSequencing) computeSequencingFor:(NSInteger) row {
568 auto* conv = [self getCurrentConversation];
569 if (conv == nil)
570 return SINGLE_WITHOUT_TIME;
571 auto it = conv->interactions.begin();
572 std::advance(it, row);
573 auto interaction = it->second;
574 if (interaction.type != lrc::api::interaction::Type::TEXT) {
575 return SINGLE_WITH_TIME;
576 }
577 if (row == 0) {
578 if (it == conv->interactions.end()) {
579 return SINGLE_WITH_TIME;
580 }
581 auto nextIt = it;
582 nextIt++;
583 auto nextInteraction = nextIt->second;
584 if ([self sequenceChangedFrom:interaction to: nextInteraction]) {
585 return SINGLE_WITH_TIME;
586 }
587 return FIRST_WITH_TIME;
588 }
589
590 if (row == conversationView.numberOfRows - 1) {
591 if(it == conv->interactions.begin()) {
592 return SINGLE_WITH_TIME;
593 }
594 auto previousIt = it;
595 previousIt--;
596 auto previousInteraction = previousIt->second;
597 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
598 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
599 if (!timeChanged && !authorChanged) {
600 return LAST_IN_SEQUENCE;
601 }
602 if (!timeChanged && authorChanged) {
603 return SINGLE_WITHOUT_TIME;
604 }
605 return SINGLE_WITH_TIME;
606 }
607 if(it == conv->interactions.begin() || it == conv->interactions.end()) {
608 return SINGLE_WITH_TIME;
609 }
610 auto previousIt = it;
611 previousIt--;
612 auto previousInteraction = previousIt->second;
613 auto nextIt = it;
614 nextIt++;
615 auto nextInteraction = nextIt->second;
616
617 bool timeChanged = [self sequenceTimeChangedFrom:interaction to:previousInteraction];
618 bool authorChanged = [self sequenceAuthorChangedFrom:interaction to:previousInteraction];
619 bool sequenceWillChange = [self sequenceChangedFrom:interaction to: nextInteraction];
Kateryna Kostiuk9d8b7922018-05-02 12:52:53 -0400620 if (previousInteraction.type == lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER ||
621 previousInteraction.type == lrc::api::interaction::Type::INCOMING_DATA_TRANSFER) {
622 if(!sequenceWillChange) {
623 return FIRST_WITH_TIME;
624 }
625 return SINGLE_WITH_TIME;
626 }
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400627 if (!sequenceWillChange) {
628 if (!timeChanged && !authorChanged) {
629 return MIDDLE_IN_SEQUENCE;
630 }
631 if (timeChanged) {
632 return FIRST_WITH_TIME;
633 }
634 return FIRST_WITHOUT_TIME;
635 } if (!timeChanged && !authorChanged) {
636 return LAST_IN_SEQUENCE;
637 } if (timeChanged) {
638 return SINGLE_WITH_TIME;
639 }
640 return SINGLE_WITHOUT_TIME;
641}
642
643-(bool) sequenceChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
644 return ([self sequenceTimeChangedFrom:firstInteraction to:secondInteraction] || [self sequenceAuthorChangedFrom:firstInteraction to:secondInteraction]);
645}
646
647-(bool) sequenceTimeChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
648 bool timeChanged = NO;
649 NSDate* firstMessageTime = [NSDate dateWithTimeIntervalSince1970:firstInteraction.timestamp];
650 NSDate* secondMessageTime = [NSDate dateWithTimeIntervalSince1970:secondInteraction.timestamp];
651 bool hourComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitHour];
652 bool minutComp = [[NSCalendar currentCalendar] compareDate:firstMessageTime toDate:secondMessageTime toUnitGranularity:NSCalendarUnitMinute];
653 if(hourComp != NSOrderedSame || minutComp != NSOrderedSame) {
654 timeChanged = YES;
655 }
656 return timeChanged;
657}
658
659-(bool) sequenceAuthorChangedFrom:(lrc::api::interaction::Info) firstInteraction to:(lrc::api::interaction::Info) secondInteraction {
660 bool authorChanged = YES;
661 bool isOutgoing = lrc::api::interaction::isOutgoing(firstInteraction);
662 if ((secondInteraction.type == lrc::api::interaction::Type::TEXT) && (isOutgoing == lrc::api::interaction::isOutgoing(secondInteraction))) {
663 authorChanged = NO;
664 }
665 return authorChanged;
666}
667
668-(NSString *)timeForMessage:(NSDate*) msgTime {
669 NSDate *today = [NSDate date];
670 NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
Kateryna Kostiukaf6d5e22018-06-12 15:00:00 -0400671 [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:[[NSLocale currentLocale] localeIdentifier]]];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400672 if ([[NSCalendar currentCalendar] compareDate:today
673 toDate:msgTime
674 toUnitGranularity:NSCalendarUnitYear]!= NSOrderedSame) {
675 return [NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle];
676 }
677
678 if ([[NSCalendar currentCalendar] compareDate:today
679 toDate:msgTime
680 toUnitGranularity:NSCalendarUnitDay]!= NSOrderedSame ||
681 [[NSCalendar currentCalendar] compareDate:today
682 toDate:msgTime
683 toUnitGranularity:NSCalendarUnitMonth]!= NSOrderedSame) {
684 [dateFormatter setDateFormat:@"MMM dd, HH:mm"];
685 return [dateFormatter stringFromDate:msgTime];
686 }
687
688 [dateFormatter setDateFormat:@"HH:mm"];
689 return [dateFormatter stringFromDate:msgTime];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400690}
691
Anthony Léonard2382b562017-12-13 15:51:28 -0500692#pragma mark - NSTableViewDataSource
693
694- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
695{
696 auto* conv = [self getCurrentConversation];
697
Anthony Léonardf2bb17d2018-02-15 17:18:09 -0500698 if (conv)
699 return conv->interactions.size();
700 else
701 return 0;
Anthony Léonard2382b562017-12-13 15:51:28 -0500702}
703
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400704#pragma mark - Text formatting
705
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400706- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400707{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400708 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400709 attrs[NSForegroundColorAttributeName] = [NSColor labelColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400710 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400711 return attrs;
712}
713
714- (NSParagraphStyle*) paragraphStyle
715{
716 /*
717 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
718 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
719 to copy, we use the default one.
720
721 The default values supplied by the default NSParagraphStyle are:
722 Alignment NSNaturalTextAlignment
723 Tab stops 12 left-aligned tabs, spaced by 28.0 points
724 Line break mode NSLineBreakByWordWrapping
725 All others 0.0
726 */
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400727 NSMutableParagraphStyle* aMutableParagraphStyle =
728 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
729 [aMutableParagraphStyle setHeadIndent:1.0];
730 [aMutableParagraphStyle setFirstLineHeadIndent:1.0];
731 return aMutableParagraphStyle;
732}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400733
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500734#pragma mark - Actions
735
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400736- (void)acceptIncomingFile:(id)sender {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500737 auto interId = [(IMTableCellView*)[[sender superview] superview] interaction];
738 auto& inter = [self getCurrentConversation]->interactions.find(interId)->second;
739 if (convModel_ && !convUid_.empty()) {
740 NSSavePanel* filePicker = [NSSavePanel savePanel];
741 [filePicker setNameFieldStringValue:@(inter.body.c_str())];
742
743 if ([filePicker runModal] == NSFileHandlingPanelOKButton) {
744 const char* fullPath = [[filePicker URL] fileSystemRepresentation];
745 convModel_->acceptTransfer(convUid_, interId, fullPath);
746 }
747 }
748}
749
Kateryna Kostiukae660fd2018-04-24 14:10:41 -0400750- (void)declineIncomingFile:(id)sender {
Anthony Léonarde7d62ed2018-01-25 10:51:47 -0500751 auto inter = [(IMTableCellView*)[[sender superview] superview] interaction];
752 if (convModel_ && !convUid_.empty()) {
753 convModel_->cancelTransfer(convUid_, inter);
754 }
755}
756
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400757@end