blob: 035c4f8decd3f350673e53ad29c0161af5d3cc93 [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"
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040034
Anthony Léonard2382b562017-12-13 15:51:28 -050035@interface MessagesVC () <NSTableViewDelegate, NSTableViewDataSource> {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040036
Anthony Léonard2382b562017-12-13 15:51:28 -050037 __unsafe_unretained IBOutlet NSTableView* conversationView;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040038
Anthony Léonard2382b562017-12-13 15:51:28 -050039 std::string convUid_;
40 const lrc::api::ConversationModel* convModel_;
41 const lrc::api::conversation::Info* cachedConv_;
42
43 QMetaObject::Connection newMessageSignal_;
44
45 // Both are needed to invalidate cached conversation as pointer
46 // may not be referencing the same conversation anymore
47 QMetaObject::Connection modelSortedSignal_;
48 QMetaObject::Connection filterChangedSignal_;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040049}
50
51@property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager;
52
53@end
54
55@implementation MessagesVC
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040056
Anthony Léonard2382b562017-12-13 15:51:28 -050057-(const lrc::api::conversation::Info*) getCurrentConversation
58{
59 if (convModel_ == nil || convUid_.empty())
60 return nil;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040061
Anthony Léonard2382b562017-12-13 15:51:28 -050062 if (cachedConv_ != nil)
63 return cachedConv_;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040064
Anthony Léonard2382b562017-12-13 15:51:28 -050065 auto& convQueue = convModel_->allFilteredConversations();
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -040066
Anthony Léonard2382b562017-12-13 15:51:28 -050067 auto it = std::find_if(convQueue.begin(), convQueue.end(), [self](const lrc::api::conversation::Info& conv) {return conv.uid == convUid_;});
68
69 if (it != convQueue.end())
70 cachedConv_ = &(*it);
71
72 return cachedConv_;
73}
74
75-(void)setConversationUid:(const std::string)convUid model:(const lrc::api::ConversationModel *)model
76{
77 if (convUid_ == convUid && convModel_ == model)
78 return;
79
80 cachedConv_ = nil;
81 convUid_ = convUid;
82 convModel_ = model;
83
84 // Signal triggered when messages are received
85 QObject::disconnect(newMessageSignal_);
86 newMessageSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::newUnreadMessage,
87 [self](const std::string& uid, uint64_t msgId, const lrc::api::interaction::Info& msg){
88 if (uid != convUid_)
89 return;
90 [conversationView reloadData];
91 [conversationView scrollToEndOfDocument:nil];
92 });
93
94 // Signals tracking changes in conversation list, we need them as cached conversation can be invalid
95 // after a reordering.
96 QObject::disconnect(modelSortedSignal_);
97 QObject::disconnect(filterChangedSignal_);
98 modelSortedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::modelSorted,
99 [self](){
100 cachedConv_ = nil;
101 });
102 filterChangedSignal_ = QObject::connect(convModel_, &lrc::api::ConversationModel::filterChanged,
103 [self](){
104 cachedConv_ = nil;
105 });
106
107
108
109 [conversationView reloadData];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400110 [conversationView scrollToEndOfDocument:nil];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400111}
112
Anthony Léonard2382b562017-12-13 15:51:28 -0500113-(void)newMessageSent
114{
115 [conversationView reloadData];
116 [conversationView scrollToEndOfDocument:nil];
117}
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400118
Anthony Léonard2382b562017-12-13 15:51:28 -0500119#pragma mark - NSTableViewDelegate methods
120- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400121{
122 return YES;
123}
124
Anthony Léonard2382b562017-12-13 15:51:28 -0500125- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400126{
Anthony Léonard2382b562017-12-13 15:51:28 -0500127 return NO;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400128}
129
Anthony Léonard2382b562017-12-13 15:51:28 -0500130- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400131{
Anthony Léonard2382b562017-12-13 15:51:28 -0500132 auto* conv = [self getCurrentConversation];
133
134 if (conv == nil)
135 return nil;
136
137 // HACK HACK HACK HACK HACK
138 // The following code has to be replaced when every views are implemented for every interaction types
139 // This is an iterator which "jumps over" any interaction which is not a text one.
140 // It behaves as if interaction list was only containing text interactions.
141 std::map<uint64_t, lrc::api::interaction::Info>::const_iterator it;
142
143 {
144 int msgCount = 0;
145 it = std::find_if(conv->interactions.begin(), conv->interactions.end(), [&msgCount, row](const std::pair<uint64_t, lrc::api::interaction::Info>& inter) {
146 if (inter.second.type == lrc::api::interaction::Type::TEXT) {
147 if (msgCount == row) {
148 return true;
149 } else {
150 msgCount++;
151 return false;
152 }
153 }
154 return false;
155 });
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400156 }
Anthony Léonard2382b562017-12-13 15:51:28 -0500157
158 if (it == conv->interactions.end())
159 return nil;
160
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400161 IMTableCellView* result;
162
Anthony Léonard2382b562017-12-13 15:51:28 -0500163 auto& interaction = it->second;
164
165 // TODO Implement interactions other than messages
166 if(interaction.type != lrc::api::interaction::Type::TEXT) {
167 return nil;
168 }
169
170 bool isOutgoing = lrc::api::interaction::isOutgoing(interaction);
171
172 if (isOutgoing) {
173 result = [tableView makeViewWithIdentifier:@"RightMessageView" owner:self];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400174 } else {
Anthony Léonard2382b562017-12-13 15:51:28 -0500175 result = [tableView makeViewWithIdentifier:@"LeftMessageView" owner:self];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400176 }
177
178 // check if the message first in incoming or outgoing messages sequence
179 Boolean isFirstInSequence = true;
Anthony Léonard2382b562017-12-13 15:51:28 -0500180 if (it != conv->interactions.begin()) {
181 auto previousIt = it;
182 previousIt--;
183 auto& previousInteraction = previousIt->second;
184 if (previousInteraction.type == lrc::api::interaction::Type::TEXT && (isOutgoing == lrc::api::interaction::isOutgoing(previousInteraction)))
185 isFirstInSequence = false;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400186 }
187 [result.photoView setHidden:!isFirstInSequence];
188 result.msgBackground.needPointer = isFirstInSequence;
189 [result setup];
190
191 NSMutableAttributedString* msgAttString =
Anthony Léonard2382b562017-12-13 15:51:28 -0500192 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",@(interaction.body.c_str())]
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400193 attributes:[self messageAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400194
Anthony Léonard2382b562017-12-13 15:51:28 -0500195 NSDate *msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400196 NSAttributedString* timestampAttrString =
Anthony Léonard2382b562017-12-13 15:51:28 -0500197 [[NSAttributedString alloc] initWithString:[NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle]
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400198 attributes:[self timestampAttributes]];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400199
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400200 CGFloat finalWidth = MAX(msgAttString.size.width, timestampAttrString.size.width);
201
Anthony Léonard2382b562017-12-13 15:51:28 -0500202 finalWidth = MIN(finalWidth + 30, tableView.frame.size.width * 0.7);
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400203
204 [msgAttString appendAttributedString:timestampAttrString];
205 [[result.msgView textStorage] appendAttributedString:msgAttString];
206 [result.msgView checkTextInDocument:nil];
207 [result updateWidthConstraint:finalWidth];
Anthony Léonard2382b562017-12-13 15:51:28 -0500208
209 auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
210 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(*conv, convModel_->owner)))];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400211 return result;
212}
213
Anthony Léonard2382b562017-12-13 15:51:28 -0500214- (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400215{
Anthony Léonard2382b562017-12-13 15:51:28 -0500216 if (IMTableCellView* cellView = [tableView viewAtColumn:0 row:row makeIfNecessary:NO]) {
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400217 [self.selectionManager registerTextView:cellView.msgView withUniqueIdentifier:@(row).stringValue];
218 }
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400219}
220
Anthony Léonard2382b562017-12-13 15:51:28 -0500221- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400222{
Anthony Léonard2382b562017-12-13 15:51:28 -0500223 double someWidth = tableView.frame.size.width * 0.7;
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400224
Anthony Léonard2382b562017-12-13 15:51:28 -0500225 auto* conv = [self getCurrentConversation];
226
227 if (conv == nil)
228 return 0;
229
230 // HACK HACK HACK HACK HACK
231 // The following code has to be replaced when every views are implemented for every interaction types
232 // This is an iterator which "jumps over" any interaction which is not a text one.
233 // It behaves as if interaction list was only containing text interactions.
234 std::map<uint64_t, lrc::api::interaction::Info>::const_iterator it;
235
236 {
237 int msgCount = 0;
238 it = std::find_if(conv->interactions.begin(), conv->interactions.end(), [&msgCount, row](const std::pair<uint64_t, lrc::api::interaction::Info>& inter) {
239 if (inter.second.type == lrc::api::interaction::Type::TEXT) {
240 if (msgCount == row) {
241 return true;
242 } else {
243 msgCount++;
244 return false;
245 }
246 }
247 return false;
248 });
249 }
250
251 if (it == conv->interactions.end())
252 return 0;
253
254 auto& interaction = it->second;
255
256 // TODO Implement interactions other than messages
257 if(interaction.type != lrc::api::interaction::Type::TEXT) {
258 return 0;
259 }
260
261 NSMutableAttributedString* msgAttString =
262 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",@(interaction.body.c_str())]
263 attributes:[self messageAttributes]];
264
265 NSDate *msgTime = [NSDate dateWithTimeIntervalSince1970:interaction.timestamp];
266 NSAttributedString* timestampAttrString =
267 [[NSAttributedString alloc] initWithString:[NSDateFormatter localizedStringFromDate:msgTime dateStyle:NSDateFormatterMediumStyle timeStyle:NSDateFormatterMediumStyle]
268 attributes:[self timestampAttributes]];
269
270 [msgAttString appendAttributedString:timestampAttrString];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400271
272 [msgAttString appendAttributedString:timestampAttrString];
273
274 NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT);
275 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
276 [tv setEnabledTextCheckingTypes:NSTextCheckingTypeLink];
277 [tv setAutomaticLinkDetectionEnabled:YES];
278 [[tv textStorage] setAttributedString:msgAttString];
279 [tv sizeToFit];
280
281 double height = tv.frame.size.height + 10;
282 return MAX(height, 50.0f);
283}
284
Anthony Léonard2382b562017-12-13 15:51:28 -0500285#pragma mark - NSTableViewDataSource
286
287- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
288{
289 auto* conv = [self getCurrentConversation];
290
291 if (conv) {
292 int count;
293 count = std::count_if(conv->interactions.begin(), conv->interactions.end(), [](const std::pair<uint64_t, lrc::api::interaction::Info>& inter) {
294 return inter.second.type == lrc::api::interaction::Type::TEXT;
295 });
296 return count;
297 }
298 return 0;
299
300#if 0
301 // TODO: Replace above code by the following one when every interaction types implemented
302 if (conv_) {
303 return conv_->interactions.size();
304 }
305#endif
306}
307
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400308#pragma mark - Text formatting
309
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400310- (NSMutableDictionary*) timestampAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400311{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400312 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400313 attrs[NSForegroundColorAttributeName] = [NSColor grayColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400314 NSFont* systemFont = [NSFont systemFontOfSize:12.0f];
315 attrs[NSFontAttributeName] = systemFont;
316 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
317
318 return attrs;
319}
320
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400321- (NSMutableDictionary*) messageAttributes
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400322{
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400323 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
Kateryna Kostiuk33089872017-07-14 16:43:59 -0400324 attrs[NSForegroundColorAttributeName] = [NSColor blackColor];
Kateryna Kostiuk58276bc2017-06-07 08:50:48 -0400325 NSFont* systemFont = [NSFont systemFontOfSize:14.0f];
326 attrs[NSFontAttributeName] = systemFont;
327 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
328
329 return attrs;
330}
331
332- (NSParagraphStyle*) paragraphStyle
333{
334 /*
335 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
336 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
337 to copy, we use the default one.
338
339 The default values supplied by the default NSParagraphStyle are:
340 Alignment NSNaturalTextAlignment
341 Tab stops 12 left-aligned tabs, spaced by 28.0 points
342 Line break mode NSLineBreakByWordWrapping
343 All others 0.0
344 */
345 NSMutableParagraphStyle* aMutableParagraphStyle =
346 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
347
348 // Now adjust our NSMutableParagraphStyle formatting to be whatever we want.
349 // The numeric values below are in points (72 points per inch)
350 [aMutableParagraphStyle setLineSpacing:1.5];
351 [aMutableParagraphStyle setParagraphSpacing:5.0];
352 [aMutableParagraphStyle setHeadIndent:5.0];
353 [aMutableParagraphStyle setTailIndent:-5.0];
354 [aMutableParagraphStyle setFirstLineHeadIndent:5.0];
355 return aMutableParagraphStyle;
356}
357
358@end