blob: a7e0c27d821cd4e22533ad0b10520029bcfe1474 [file] [log] [blame]
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04001/*
2 * Copyright (C) 2015-2017 Savoir-faire Linux Inc.
3 * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
Anthony Léonard2382b562017-12-13 15:51:28 -05004 * Anthony Léonard <anthony.leonard@savoirfairelinux.com>
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -04005 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 */
20
21#import <QItemSelectionModel>
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"
32#import "INDSequentialTextSelectionManager.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 Kostiuk58276bc2017-06-07 08:50:48 -040035
Anthony Léonard2382b562017-12-13 15:51:28 -050036@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource> {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040037
Anthony Léonard2382b562017-12-13 15:51:28 -050038 __unsafe_unretained IBOutlet NSTableView* conversationView;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040039
Anthony Léonard2382b562017-12-13 15:51:28 -050040 std::string convUid_;
41 const lrc::api::ConversationModel* convModel_;
42 const lrc::api::conversation::Info* cachedConv_;
43
44 QMetaObject::Connection newMessageSignal_;
45
46 // Both are needed to invalidate cached conversation as pointer
47 // may not be referencing the same conversation anymore
48 QMetaObject::Connection modelSortedSignal_;
49 QMetaObject::Connection filterChangedSignal_;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040050}
51
52@property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager;
53
54@end
55
56@implementation MessagesVC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040057
Anthony Léonard2382b562017-12-13 15:51:28 -050058-(const lrc::api::conversation::Info*) getCurrentConversation
59{
60 if (convModel_ == nil || convUid_.empty())
61 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040062
Anthony Léonard2382b562017-12-13 15:51:28 -050063 if (cachedConv_ != nil)
64 return cachedConv_;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040065
Anthony Léonard2382b562017-12-13 15:51:28 -050066 auto& convQueue = convModel_->allFilteredConversations();
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040067
Anthony Léonard6f819752018-01-05 09:53:40 -050068 auto it = getConversationFromUid(convUid_, *convModel_);
Anthony Léonard2382b562017-12-13 15:51:28 -050069
70 if (it != convQueue.end())
71 cachedConv_ = &(*it);
72
73 return cachedConv_;
74}
75
76-(void)setConversationUid:(const std::string)convUid model:(const lrc::api::ConversationModel *)model
77{
78 if (convUid_ == convUid && convModel_ == model)
79 return;
80
81 cachedConv_ = nil;
82 convUid_ = convUid;
83 convModel_ = model;
84
85 // Signal triggered when messages are received
86 QObject::disconnect(newMessageSignal_);
87 newMessageSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newUnreadMessage,
88 [self](const std::string& uid, uint64_t msgId, const lrc::api::interaction::Info& msg){
89 if (uid != convUid_)
90 return;
91 [conversationView reloadData];
92 [conversationView scrollToEndOfDocument:nil];
93 });
94
95 // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
96 // after a reordering.
97 QObject::disconnect(modelSortedSignal_);
98 QObject::disconnect(filterChangedSignal_);
99 modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelSorted,
100 [self](){
101 cachedConv_ = nil;
102 });
103 filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
104 [self](){
105 cachedConv_ = nil;
106 });
107
108
109
110 [conversationView reloadData];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400111 [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400112}
113
Anthony Léonard2382b562017-12-13 15:51:28 -0500114-(void)newMessageSent
115{
116 [conversationView reloadData];
117 [conversationView scrollToEndOfDocument:nil];
118}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400119
Anthony Léonard2382b562017-12-13 15:51:28 -0500120#pragma mark - NSTableViewDelegate methods
121- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400122{
123 return YES;
124}
125
Anthony Léonard2382b562017-12-13 15:51:28 -0500126- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400127{
Anthony Léonard2382b562017-12-13 15:51:28 -0500128 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400129}
130
Anthony Léonard2382b562017-12-13 15:51:28 -0500131- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400132{
Anthony Léonard2382b562017-12-13 15:51:28 -0500133 auto* conv = [self getCurrentConversation];
134
135 if (conv == nil)
136 return nil;
137
138 // HACK HACK HACK HACK HACK
139 // The following code has to be replaced when every views are implemented for every interaction types
140 // This is an iterator which "jumps over" any interaction which is not a text one.
141 // It behaves as if interaction list was only containing text interactions.
142 std::map<uint64_t, lrc::api::interaction::Info>::const_iterator it;
143
144 {
145 int msgCount = 0;
146 it = std::find_if(conv->interactions.begin(), conv->interactions.end(), [&msgCount, row](const std::pair<uint64_t, lrc::api::interaction::Info>& inter) {
147 if (inter.second.type == lrc::api::interaction::Type::TEXT) {
148 if (msgCount == row) {
149 return true;
150 } else {
151 msgCount++;
152 return false;
153 }
154 }
155 return false;
156 });
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400157 }
Anthony Léonard2382b562017-12-13 15:51:28 -0500158
159 if (it == conv->interactions.end())
160 return nil;
161
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400162 IMTableCellView* result;
163
Anthony Léonard2382b562017-12-13 15:51:28 -0500164 auto& interaction = it->second;
165
166 // TODO Implement interactions other than messages
167 if(interaction.type != lrc::api::interaction::Type::TEXT) {
168 return nil;
169 }
170
171 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
172
173 if (isOutgoing) {
174 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400175 } else {
Anthony Léonard2382b562017-12-13 15:51:28 -0500176 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400177 }
178
179 // check if the message first in incoming or outgoing messages sequence
180 Boolean isFirstInSequence = true;
Anthony Léonard2382b562017-12-13 15:51:28 -0500181 if (it != conv->interactions.begin()) {
182 auto previousIt = it;
183 previousIt--;
184 auto& previousInteraction = previousIt->second;
185 if (previousInteraction.type == lrc::api::interaction::Type::TEXT && (isOutgoing == lrc::api::interaction::isOutgoing(previousInteraction)))
186 isFirstInSequence = false;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400187 }
188 [result.photoView setHidden:!isFirstInSequence];
189 result.msgBackground.needPointer = isFirstInSequence;
190 [result setup];
191
192 NSMutableAttributedString* msgAttString =
Anthony Léonard2382b562017-12-13 15:51:28 -0500193 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",@(interaction.body.c_str())]
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400194 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400195
Anthony Léonard2382b562017-12-13 15:51:28 -0500196 NSDate *msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400197 NSAttributedString* timestampAttrString =
Anthony Léonard2382b562017-12-13 15:51:28 -0500198 [[NSAttributedString alloc] initWithString:[NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle]
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400199 attributes:[self timestampAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400200
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400201 CGFloat finalWidth = MAX(msgAttString.size.width, timestampAttrString.size.width);
202
Anthony Léonard2382b562017-12-13 15:51:28 -0500203 finalWidth = MIN(finalWidth + 30, tableView.frame.size.width * 0.7);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400204
205 [msgAttString appendAttributedString:timestampAttrString];
206 [[result.msgView textStorage] appendAttributedString:msgAttString];
207 [result.msgView checkTextInDocument:nil];
208 [result updateWidthConstraint:finalWidth];
Anthony Léonard2382b562017-12-13 15:51:28 -0500209
210 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
211 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400212 return result;
213}
214
Anthony Léonard2382b562017-12-13 15:51:28 -0500215- (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400216{
Anthony Léonard2382b562017-12-13 15:51:28 -0500217 if (IMTableCellView* cellView = [tableView viewAtColumn:0 row:row makeIfNecessary:NO]) {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400218 [self.selectionManager registerTextView:cellView.msgView withUniqueIdentifier:@(row).stringValue];
219 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400220}
221
Anthony Léonard2382b562017-12-13 15:51:28 -0500222- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400223{
Anthony Léonard2382b562017-12-13 15:51:28 -0500224 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400225
Anthony Léonard2382b562017-12-13 15:51:28 -0500226 auto* conv = [self getCurrentConversation];
227
228 if (conv == nil)
229 return 0;
230
231 // HACK HACK HACK HACK HACK
232 // The following code has to be replaced when every views are implemented for every interaction types
233 // This is an iterator which "jumps over" any interaction which is not a text one.
234 // It behaves as if interaction list was only containing text interactions.
235 std::map<uint64_t, lrc::api::interaction::Info>::const_iterator it;
236
237 {
238 int msgCount = 0;
239 it = std::find_if(conv->interactions.begin(), conv->interactions.end(), [&msgCount, row](const std::pair<uint64_t, lrc::api::interaction::Info>& inter) {
240 if (inter.second.type == lrc::api::interaction::Type::TEXT) {
241 if (msgCount == row) {
242 return true;
243 } else {
244 msgCount++;
245 return false;
246 }
247 }
248 return false;
249 });
250 }
251
252 if (it == conv->interactions.end())
253 return 0;
254
255 auto& interaction = it->second;
256
257 // TODO Implement interactions other than messages
258 if(interaction.type != lrc::api::interaction::Type::TEXT) {
259 return 0;
260 }
261
262 NSMutableAttributedString* msgAttString =
263 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",@(interaction.body.c_str())]
264 attributes:[self messageAttributes]];
265
266 NSDate *msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
267 NSAttributedString* timestampAttrString =
268 [[NSAttributedString alloc] initWithString:[NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle]
269 attributes:[self timestampAttributes]];
270
271 [msgAttString appendAttributedString:timestampAttrString];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400272
273 [msgAttString appendAttributedString:timestampAttrString];
274
275 NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT);
276 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
277 [tv setEnabledTextCheckingTypes:NSTextCheckingTypeLink];
278 [tv setAutomaticLinkDetectionEnabled:YES];
279 [[tv textStorage] setAttributedString:msgAttString];
280 [tv sizeToFit];
281
282 double height = tv.frame.size.height + 10;
283 return MAX(height, 50.0f);
284}
285
Anthony Léonard2382b562017-12-13 15:51:28 -0500286#pragma mark - NSTableViewDataSource
287
288- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
289{
290 auto* conv = [self getCurrentConversation];
291
292 if (conv) {
293 int count;
294 count = std::count_if(conv->interactions.begin(), conv->interactions.end(), [](const std::pair<uint64_t, lrc::api::interaction::Info>& inter) {
295 return inter.second.type == lrc::api::interaction::Type::TEXT;
296 });
297 return count;
298 }
299 return 0;
300
301#if 0
302 // TODO: Replace above code by the following one when every interaction types implemented
303 if (conv_) {
304 return conv_->interactions.size();
305 }
306#endif
307}
308
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400309#pragma mark - Text formatting
310
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400311- (NSMutableDictionary*) timestampAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400312{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400313 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400314 attrs[NSForegroundColorAttributeName] = [NSColor grayColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400315 NSFont* systemFont = [NSFont systemFontOfSize:12.0f];
316 attrs[NSFontAttributeName] = systemFont;
317 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
318
319 return attrs;
320}
321
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400322- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400323{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400324 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400325 attrs[NSForegroundColorAttributeName] = [NSColor blackColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400326 NSFont* systemFont = [NSFont systemFontOfSize:14.0f];
327 attrs[NSFontAttributeName] = systemFont;
328 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
329
330 return attrs;
331}
332
333- (NSParagraphStyle*) paragraphStyle
334{
335 /*
336 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
337 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
338 to copy, we use the default one.
339
340 The default values supplied by the default NSParagraphStyle are:
341 Alignment NSNaturalTextAlignment
342 Tab stops 12 left-aligned tabs, spaced by 28.0 points
343 Line break mode NSLineBreakByWordWrapping
344 All others 0.0
345 */
346 NSMutableParagraphStyle* aMutableParagraphStyle =
347 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
348
349 // Now adjust our NSMutableParagraphStyle formatting to be whatever we want.
350 // The numeric values below are in points (72 points per inch)
351 [aMutableParagraphStyle setLineSpacing:1.5];
352 [aMutableParagraphStyle setParagraphSpacing:5.0];
353 [aMutableParagraphStyle setHeadIndent:5.0];
354 [aMutableParagraphStyle setTailIndent:-5.0];
355 [aMutableParagraphStyle setFirstLineHeadIndent:5.0];
356 return aMutableParagraphStyle;
357}
358
359@end