blob: d30e3ae1824d0265a9536bb59602a81a9e8e9966 [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());
Anthony Léonard64e19672018-01-18 16:40:34 -0500211 if (isOutgoing) {
212 [result.photoView setImage:[NSImage imageNamed:@"default_user_icon"]];
213 } else {
214 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
215 }
216
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400217 return result;
218}
219
Anthony Léonard2382b562017-12-13 15:51:28 -0500220- (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400221{
Anthony Léonard2382b562017-12-13 15:51:28 -0500222 if (IMTableCellView* cellView = [tableView viewAtColumn:0 row:row makeIfNecessary:NO]) {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400223 [self.selectionManager registerTextView:cellView.msgView withUniqueIdentifier:@(row).stringValue];
224 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400225}
226
Anthony Léonard2382b562017-12-13 15:51:28 -0500227- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400228{
Anthony Léonard2382b562017-12-13 15:51:28 -0500229 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400230
Anthony Léonard2382b562017-12-13 15:51:28 -0500231 auto* conv = [self getCurrentConversation];
232
233 if (conv == nil)
234 return 0;
235
236 // HACK HACK HACK HACK HACK
237 // The following code has to be replaced when every views are implemented for every interaction types
238 // This is an iterator which "jumps over" any interaction which is not a text one.
239 // It behaves as if interaction list was only containing text interactions.
240 std::map<uint64_t, lrc::api::interaction::Info>::const_iterator it;
241
242 {
243 int msgCount = 0;
244 it = std::find_if(conv->interactions.begin(), conv->interactions.end(), [&msgCount, row](const std::pair<uint64_t, lrc::api::interaction::Info>& inter) {
245 if (inter.second.type == lrc::api::interaction::Type::TEXT) {
246 if (msgCount == row) {
247 return true;
248 } else {
249 msgCount++;
250 return false;
251 }
252 }
253 return false;
254 });
255 }
256
257 if (it == conv->interactions.end())
258 return 0;
259
260 auto& interaction = it->second;
261
262 // TODO Implement interactions other than messages
263 if(interaction.type != lrc::api::interaction::Type::TEXT) {
264 return 0;
265 }
266
267 NSMutableAttributedString* msgAttString =
268 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",@(interaction.body.c_str())]
269 attributes:[self messageAttributes]];
270
271 NSDate *msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
272 NSAttributedString* timestampAttrString =
273 [[NSAttributedString alloc] initWithString:[NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle]
274 attributes:[self timestampAttributes]];
275
276 [msgAttString appendAttributedString:timestampAttrString];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400277
278 [msgAttString appendAttributedString:timestampAttrString];
279
280 NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT);
281 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
282 [tv setEnabledTextCheckingTypes:NSTextCheckingTypeLink];
283 [tv setAutomaticLinkDetectionEnabled:YES];
284 [[tv textStorage] setAttributedString:msgAttString];
285 [tv sizeToFit];
286
287 double height = tv.frame.size.height + 10;
288 return MAX(height, 50.0f);
289}
290
Anthony Léonard2382b562017-12-13 15:51:28 -0500291#pragma mark - NSTableViewDataSource
292
293- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
294{
295 auto* conv = [self getCurrentConversation];
296
297 if (conv) {
298 int count;
299 count = std::count_if(conv->interactions.begin(), conv->interactions.end(), [](const std::pair<uint64_t, lrc::api::interaction::Info>& inter) {
300 return inter.second.type == lrc::api::interaction::Type::TEXT;
301 });
302 return count;
303 }
304 return 0;
305
306#if 0
307 // TODO: Replace above code by the following one when every interaction types implemented
308 if (conv_) {
309 return conv_->interactions.size();
310 }
311#endif
312}
313
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400314#pragma mark - Text formatting
315
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400316- (NSMutableDictionary*) timestampAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400317{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400318 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400319 attrs[NSForegroundColorAttributeName] = [NSColor grayColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400320 NSFont* systemFont = [NSFont systemFontOfSize:12.0f];
321 attrs[NSFontAttributeName] = systemFont;
322 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
323
324 return attrs;
325}
326
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400327- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400328{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400329 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400330 attrs[NSForegroundColorAttributeName] = [NSColor blackColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400331 NSFont* systemFont = [NSFont systemFontOfSize:14.0f];
332 attrs[NSFontAttributeName] = systemFont;
333 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
334
335 return attrs;
336}
337
338- (NSParagraphStyle*) paragraphStyle
339{
340 /*
341 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
342 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
343 to copy, we use the default one.
344
345 The default values supplied by the default NSParagraphStyle are:
346 Alignment NSNaturalTextAlignment
347 Tab stops 12 left-aligned tabs, spaced by 28.0 points
348 Line break mode NSLineBreakByWordWrapping
349 All others 0.0
350 */
351 NSMutableParagraphStyle* aMutableParagraphStyle =
352 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
353
354 // Now adjust our NSMutableParagraphStyle formatting to be whatever we want.
355 // The numeric values below are in points (72 points per inch)
356 [aMutableParagraphStyle setLineSpacing:1.5];
357 [aMutableParagraphStyle setParagraphSpacing:5.0];
358 [aMutableParagraphStyle setHeadIndent:5.0];
359 [aMutableParagraphStyle setTailIndent:-5.0];
360 [aMutableParagraphStyle setFirstLineHeadIndent:5.0];
361 return aMutableParagraphStyle;
362}
363
364@end