NCChatMessage.m 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. /**
  2. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. #import "NCChatMessage.h"
  6. #import "NCAPIController.h"
  7. #import "NCAppBranding.h"
  8. #import "NextcloudTalk-Swift.h"
  9. NSInteger const kChatMessageGroupTimeDifference = 30;
  10. NSString * const kMessageTypeComment = @"comment";
  11. NSString * const kMessageTypeCommentDeleted = @"comment_deleted";
  12. NSString * const kMessageTypeSystem = @"system";
  13. NSString * const kMessageTypeCommand = @"command";
  14. NSString * const kMessageTypeVoiceMessage = @"voice-message";
  15. NSString * const kSharedItemTypeAudio = @"audio";
  16. NSString * const kSharedItemTypeDeckcard = @"deckcard";
  17. NSString * const kSharedItemTypeFile = @"file";
  18. NSString * const kSharedItemTypeLocation = @"location";
  19. NSString * const kSharedItemTypeMedia = @"media";
  20. NSString * const kSharedItemTypeOther = @"other";
  21. NSString * const kSharedItemTypeVoice = @"voice";
  22. NSString * const kSharedItemTypePoll = @"poll";
  23. NSString * const kSharedItemTypeRecording = @"recording";
  24. @interface NCChatMessage ()
  25. {
  26. NCMessageFileParameter *_fileParameter;
  27. NCMessageLocationParameter *_locationParameter;
  28. NCDeckCardParameter *_deckCardParameter;
  29. NSString *_objectShareLink;
  30. NSMutableArray *_temporaryReactions;
  31. BOOL _urlDetectionDone;
  32. NSString *_urlDetected;
  33. BOOL _referenceDataDone;
  34. NSDictionary *_referenceData;
  35. }
  36. @end
  37. @implementation NCChatMessage
  38. + (instancetype)messageWithDictionary:(NSDictionary *)messageDict
  39. {
  40. if (!messageDict || ![messageDict isKindOfClass:[NSDictionary class]]) {
  41. return nil;
  42. }
  43. NCChatMessage *message = [[NCChatMessage alloc] init];
  44. message.actorId = [messageDict objectForKey:@"actorId"];
  45. message.actorType = [messageDict objectForKey:@"actorType"];
  46. message.messageId = [[messageDict objectForKey:@"id"] integerValue];
  47. message.message = [messageDict objectForKey:@"message"];
  48. message.timestamp = [[messageDict objectForKey:@"timestamp"] integerValue];
  49. message.token = [messageDict objectForKey:@"token"];
  50. message.systemMessage = [messageDict objectForKey:@"systemMessage"];
  51. message.isReplyable = [[messageDict objectForKey:@"isReplyable"] boolValue];
  52. message.referenceId = [messageDict objectForKey:@"referenceId"];
  53. message.messageType = [messageDict objectForKey:@"messageType"];
  54. message.expirationTimestamp = [[messageDict objectForKey:@"expirationTimestamp"] integerValue];
  55. message.isMarkdownMessage = [[messageDict objectForKey:@"markdown"] boolValue];
  56. message.lastEditActorId = [messageDict objectForKey:@"lastEditActorId"];
  57. message.lastEditActorType = [messageDict objectForKey:@"lastEditActorType"];
  58. message.lastEditActorDisplayName = [messageDict objectForKey:@"lastEditActorDisplayName"];
  59. message.lastEditTimestamp = [[messageDict objectForKey:@"lastEditTimestamp"] integerValue];
  60. message.isSilent = [[messageDict objectForKey:@"silent"] boolValue];
  61. id actorDisplayName = [messageDict objectForKey:@"actorDisplayName"];
  62. if (!actorDisplayName) {
  63. message.actorDisplayName = @"";
  64. } else {
  65. if ([actorDisplayName isKindOfClass:[NSString class]]) {
  66. message.actorDisplayName = actorDisplayName;
  67. } else {
  68. message.actorDisplayName = [actorDisplayName stringValue];
  69. }
  70. }
  71. id messageParameters = [messageDict objectForKey:@"messageParameters"];
  72. if ([messageParameters isKindOfClass:[NSDictionary class]]) {
  73. NSError *error;
  74. NSData *jsonData = [NSJSONSerialization dataWithJSONObject:messageParameters
  75. options:0
  76. error:&error];
  77. if (jsonData) {
  78. message.messageParametersJSONString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
  79. } else {
  80. NSLog(@"Error generating message parameters JSON string: %@", error);
  81. }
  82. }
  83. id reactions = [messageDict objectForKey:@"reactions"];
  84. if ([reactions isKindOfClass:[NSDictionary class]]) {
  85. NSError *error;
  86. NSData *jsonData = [NSJSONSerialization dataWithJSONObject:reactions
  87. options:0
  88. error:&error];
  89. if (jsonData) {
  90. message.reactionsJSONString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
  91. } else {
  92. NSLog(@"Error generating reactions JSON string: %@", error);
  93. }
  94. }
  95. id reactionsSelf = [messageDict objectForKey:@"reactionsSelf"];
  96. if ([reactionsSelf isKindOfClass:[NSArray class]]) {
  97. NSError *error;
  98. NSData *jsonData = [NSJSONSerialization dataWithJSONObject:reactionsSelf
  99. options:0
  100. error:&error];
  101. if (jsonData) {
  102. message.reactionsSelfJSONString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
  103. } else {
  104. NSLog(@"Error generating reactionsSelf JSON string: %@", error);
  105. }
  106. }
  107. return message;
  108. }
  109. + (instancetype)messageWithDictionary:(NSDictionary *)messageDict andAccountId:(NSString *)accountId
  110. {
  111. NCChatMessage *message = [NCChatMessage messageWithDictionary:messageDict];
  112. if (message) {
  113. message.accountId = accountId;
  114. message.internalId = [NSString stringWithFormat:@"%@@%@@%ld", accountId, message.token, (long)message.messageId];
  115. NCChatMessage *parent = [NCChatMessage messageWithDictionary:[messageDict objectForKey:@"parent"] andAccountId:accountId];
  116. message.parentId = parent.internalId;
  117. }
  118. return message;
  119. }
  120. + (void)updateChatMessage:(NCChatMessage *)managedChatMessage withChatMessage:(NCChatMessage *)chatMessage isRoomLastMessage:(BOOL)isRoomLastMessage
  121. {
  122. int previewImageHeight = 0;
  123. // Try to keep our locally saved previewImageHeight when updating this messages with the server message
  124. // This happens when updating the last message of a room for example
  125. if (managedChatMessage.file && chatMessage.file) {
  126. // Only do this, if the new message does not include a height, to prevent an infinite recursion
  127. if (managedChatMessage.file.previewImageHeight > 0 && chatMessage.file.previewImageHeight == 0) {
  128. previewImageHeight = managedChatMessage.file.previewImageHeight;
  129. }
  130. }
  131. managedChatMessage.actorDisplayName = chatMessage.actorDisplayName;
  132. managedChatMessage.actorId = chatMessage.actorId;
  133. managedChatMessage.actorType = chatMessage.actorType;
  134. managedChatMessage.message = chatMessage.message;
  135. managedChatMessage.messageParametersJSONString = chatMessage.messageParametersJSONString;
  136. managedChatMessage.timestamp = chatMessage.timestamp;
  137. managedChatMessage.systemMessage = chatMessage.systemMessage;
  138. managedChatMessage.isReplyable = chatMessage.isReplyable;
  139. managedChatMessage.messageType = chatMessage.messageType;
  140. managedChatMessage.reactionsJSONString = chatMessage.reactionsJSONString;
  141. managedChatMessage.expirationTimestamp = chatMessage.expirationTimestamp;
  142. managedChatMessage.isMarkdownMessage = chatMessage.isMarkdownMessage;
  143. managedChatMessage.lastEditActorId = chatMessage.lastEditActorId;
  144. managedChatMessage.lastEditActorType = chatMessage.lastEditActorType;
  145. managedChatMessage.lastEditActorDisplayName = chatMessage.lastEditActorDisplayName;
  146. managedChatMessage.lastEditTimestamp = chatMessage.lastEditTimestamp;
  147. if (!isRoomLastMessage) {
  148. managedChatMessage.reactionsSelfJSONString = chatMessage.reactionsSelfJSONString;
  149. }
  150. if (!managedChatMessage.parentId && chatMessage.parentId) {
  151. managedChatMessage.parentId = chatMessage.parentId;
  152. }
  153. if (previewImageHeight > 0) {
  154. [managedChatMessage setPreviewImageHeight:previewImageHeight];
  155. }
  156. }
  157. + (NSString *)primaryKey {
  158. return @"internalId";
  159. }
  160. - (id)copyWithZone:(NSZone *)zone
  161. {
  162. NCChatMessage *messageCopy = [[NCChatMessage alloc] init];
  163. messageCopy.internalId = [_internalId copyWithZone:zone];
  164. messageCopy.accountId = [_accountId copyWithZone:zone];
  165. messageCopy.actorDisplayName = [_actorDisplayName copyWithZone:zone];
  166. messageCopy.actorId = [_actorId copyWithZone:zone];
  167. messageCopy.actorType = [_actorType copyWithZone:zone];
  168. messageCopy.messageId = _messageId;
  169. messageCopy.message = [_message copyWithZone:zone];
  170. messageCopy.messageParametersJSONString = [_messageParametersJSONString copyWithZone:zone];
  171. messageCopy.timestamp = _timestamp;
  172. messageCopy.token = [_token copyWithZone:zone];
  173. messageCopy.systemMessage = [_systemMessage copyWithZone:zone];
  174. messageCopy.isReplyable = _isReplyable;
  175. messageCopy.parentId = [_parentId copyWithZone:zone];
  176. messageCopy.referenceId = [_referenceId copyWithZone:zone];
  177. messageCopy.messageType = [_messageType copyWithZone:zone];
  178. messageCopy.reactionsJSONString = [_reactionsJSONString copyWithZone:zone];
  179. messageCopy.reactionsSelfJSONString = [_reactionsSelfJSONString copyWithZone:zone];
  180. messageCopy.expirationTimestamp = _expirationTimestamp;
  181. messageCopy.isTemporary = _isTemporary;
  182. messageCopy.sendingFailed = _sendingFailed;
  183. messageCopy.isGroupMessage = _isGroupMessage;
  184. messageCopy.isDeleting = _isDeleting;
  185. messageCopy.isOfflineMessage = _isOfflineMessage;
  186. messageCopy.isSilent = _isSilent;
  187. messageCopy.isMarkdownMessage = _isMarkdownMessage;
  188. messageCopy.lastEditActorId = _lastEditActorId;
  189. messageCopy.lastEditActorType = _lastEditActorType;
  190. messageCopy.lastEditActorDisplayName = _lastEditActorDisplayName;
  191. messageCopy.lastEditTimestamp = _lastEditTimestamp;
  192. return messageCopy;
  193. }
  194. - (NCMessageParameter *)file
  195. {
  196. if (!_fileParameter) {
  197. for (NSDictionary *parameterDict in [[self messageParameters] allValues]) {
  198. NCMessageFileParameter *parameter = [[NCMessageFileParameter alloc] initWithDictionary:parameterDict];
  199. if (![parameter.type isEqualToString:@"file"]) {
  200. continue;
  201. }
  202. if (!_fileParameter) {
  203. _fileParameter = parameter;
  204. } else {
  205. // If there is more than one file in the message,
  206. // we don't display any preview.
  207. _fileParameter = nil;
  208. return nil;
  209. }
  210. }
  211. }
  212. return _fileParameter;
  213. }
  214. - (NCMessageLocationParameter *)geoLocation
  215. {
  216. if (!_locationParameter) {
  217. for (NSDictionary *parameterDict in [[self messageParameters] allValues]) {
  218. NCMessageLocationParameter *parameter = [[NCMessageLocationParameter alloc] initWithDictionary:parameterDict] ;
  219. if ([parameter.type isEqualToString:@"geo-location"]) {
  220. _locationParameter = parameter;
  221. break;
  222. }
  223. }
  224. }
  225. return _locationParameter;
  226. }
  227. - (NCDeckCardParameter *)deckCard
  228. {
  229. if (!_deckCardParameter) {
  230. for (NSDictionary *parameterDict in [[self messageParameters] allValues]) {
  231. NCDeckCardParameter *parameter = [[NCDeckCardParameter alloc] initWithDictionary:parameterDict] ;
  232. if ([parameter.type isEqualToString:@"deck-card"]) {
  233. _deckCardParameter = parameter;
  234. break;
  235. }
  236. }
  237. }
  238. return _deckCardParameter;
  239. }
  240. - (NSString *)objectShareLink;
  241. {
  242. if (!_objectShareLink && [self isObjectShare]) {
  243. _objectShareLink = [[self.messageParameters objectForKey:@"object"] objectForKey:@"link"];
  244. }
  245. return _objectShareLink;
  246. }
  247. - (NSMutableAttributedString *)parsedMessage
  248. {
  249. if (!self.message) {
  250. return nil;
  251. }
  252. NSString *originalMessage = self.file.contactName ? self.file.contactName : self.message;
  253. if (self.collapsedMessage && self.isCollapsed) {
  254. originalMessage = self.collapsedMessage;
  255. }
  256. NSString *parsedMessage = originalMessage;
  257. NSError *error = nil;
  258. NSRegularExpression *parameterRegex = [NSRegularExpression regularExpressionWithPattern:@"\\{([^}]+)\\}" options:NSRegularExpressionCaseInsensitive error:&error];
  259. NSArray *matches = [parameterRegex matchesInString:originalMessage
  260. options:0
  261. range:NSMakeRange(0, [originalMessage length])];
  262. // Find message parameters
  263. NSMutableArray *parameters = [NSMutableArray new];
  264. for (NSTextCheckingResult *match in matches) {
  265. NSString* parameter = [originalMessage substringWithRange:match.range];
  266. NSString *parameterKey = [[parameter stringByReplacingOccurrencesOfString:@"{" withString:@""]
  267. stringByReplacingOccurrencesOfString:@"}" withString:@""];
  268. NSDictionary *parameterDict = [[self messageParameters] objectForKey:parameterKey];
  269. if (self.collapsedMessage && self.isCollapsed) {
  270. parameterDict = [[self collapsedMessageParameters] objectForKey:parameterKey];
  271. }
  272. if (parameterDict) {
  273. NCMessageParameter *messageParameter = [[NCMessageParameter alloc] initWithDictionary:parameterDict] ;
  274. // Default replacement string is the parameter name
  275. NSString *replaceString = messageParameter.name;
  276. // Format user and call mentions
  277. if ([messageParameter.type isEqualToString:@"user"] || [messageParameter.type isEqualToString:@"guest"] ||
  278. [messageParameter.type isEqualToString:@"user-group"] || [messageParameter.type isEqualToString:@"call"]) {
  279. replaceString = [NSString stringWithFormat:@"@%@", [parameterDict objectForKey:@"name"]];
  280. }
  281. parsedMessage = [parsedMessage stringByReplacingOccurrencesOfString:parameter withString:replaceString];
  282. // Calculate parameter range
  283. NSRange searchRange = NSMakeRange(0,parsedMessage.length);
  284. if (parameters.count > 0) {
  285. NCMessageParameter *lastParameter = [parameters objectAtIndex:parameters.count - 1];
  286. NSInteger newRangeLocation = lastParameter.range.location + lastParameter.range.length;
  287. searchRange = NSMakeRange(newRangeLocation, parsedMessage.length - newRangeLocation);
  288. }
  289. messageParameter.range = [parsedMessage rangeOfString:replaceString options:0 range:searchRange];
  290. [parameters addObject:messageParameter];
  291. }
  292. }
  293. UIColor *defaultColor = [NCAppBranding chatForegroundColor];
  294. NSMutableAttributedString *attributedMessage = [[NSMutableAttributedString alloc] initWithString:parsedMessage];
  295. [attributedMessage addAttribute:NSForegroundColorAttributeName value:defaultColor range:NSMakeRange(0, parsedMessage.length)];
  296. if (self.isEmojiMessage) {
  297. [attributedMessage addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:36.0f] range:NSMakeRange(0, parsedMessage.length)];
  298. } else {
  299. [attributedMessage addAttribute:NSFontAttributeName value:[UIFont preferredFontForTextStyle:UIFontTextStyleBody] range:NSMakeRange(0, parsedMessage.length)];
  300. }
  301. UIColor *highlightedColor = nil;
  302. for (NCMessageParameter *param in parameters) {
  303. //Set color for mentions
  304. if ([param.type isEqualToString:@"user"] || [param.type isEqualToString:@"guest"] ||
  305. [param.type isEqualToString:@"user-group"] || [param.type isEqualToString:@"call"]) {
  306. if (param.shouldBeHighlighted) {
  307. if (!highlightedColor) {
  308. // Only get the elementColor if we really need it to reduce realm queries
  309. highlightedColor = [NCAppBranding elementColor];
  310. }
  311. [attributedMessage addAttribute:NSForegroundColorAttributeName value:highlightedColor range:param.range];
  312. } else {
  313. [attributedMessage addAttribute:NSForegroundColorAttributeName value:defaultColor range:param.range];
  314. }
  315. [attributedMessage addAttribute:NSFontAttributeName value:[UIFont preferredFontFor:UIFontTextStyleBody weight:UIFontWeightBold] range:param.range];
  316. }
  317. //Create a link if parameter contains a link
  318. else if (param.link) {
  319. // Do not create links for files. File preview images will redirect to files client or browser.
  320. if ([param.type isEqualToString:@"file"]) {
  321. [attributedMessage addAttribute:NSFontAttributeName value:[UIFont preferredFontFor:UIFontTextStyleBody weight:UIFontWeightBold] range:param.range];
  322. } else {
  323. [attributedMessage addAttribute:NSLinkAttributeName value:param.link range:param.range];
  324. }
  325. }
  326. }
  327. return attributedMessage;
  328. }
  329. - (NSMutableAttributedString *)parsedMarkdown
  330. {
  331. NSMutableAttributedString *parsedMessage = self.parsedMessage;
  332. if (!parsedMessage) {
  333. return nil;
  334. }
  335. if (!_isMarkdownMessage) {
  336. return parsedMessage;
  337. }
  338. return [SwiftMarkdownObjCBridge parseMarkdownWithMarkdownString:parsedMessage];
  339. }
  340. - (NSMutableAttributedString *)parsedMarkdownForChat
  341. {
  342. // In some circumstances we want/need to hide the message in the chat, but still want to show it in other parts like the conversation list
  343. if ([self getDeckCardUrlForReferenceProvider]) {
  344. return nil;
  345. }
  346. NSMutableAttributedString *parsedMessage = self.parsedMessage;
  347. if (!parsedMessage) {
  348. return nil;
  349. }
  350. if (!_isMarkdownMessage) {
  351. return parsedMessage;
  352. }
  353. return [SwiftMarkdownObjCBridge parseMarkdownWithMarkdownString:parsedMessage];
  354. }
  355. - (NSMutableArray *)temporaryReactions
  356. {
  357. if (!_temporaryReactions) {
  358. _temporaryReactions = [NSMutableArray new];
  359. }
  360. return _temporaryReactions;
  361. }
  362. - (void)mergeTemporaryReactionsWithReactions:(NSMutableArray *)reactions
  363. {
  364. for (NCChatReaction *temporaryReaction in [self temporaryReactions]) {
  365. if (temporaryReaction.state == NCChatReactionStateAdding) {
  366. [self addTemporaryReaction:temporaryReaction.reaction inReactions:reactions];
  367. } else if (temporaryReaction.state == NCChatReactionStateRemoving) {
  368. [self removeReactionTemporarily:temporaryReaction.reaction inReactions:reactions];
  369. }
  370. }
  371. }
  372. - (void)addTemporaryReaction:(NSString *)reaction inReactions:(NSMutableArray *)reactions
  373. {
  374. BOOL includedReaction = NO;
  375. for (NCChatReaction *currentReaction in reactions) {
  376. if ([currentReaction.reaction isEqualToString:reaction]) {
  377. currentReaction.count += 1;
  378. currentReaction.userReacted = YES;
  379. includedReaction = YES;
  380. }
  381. }
  382. if (!includedReaction) {
  383. NCChatReaction *newReaction = [[NCChatReaction alloc] init];
  384. newReaction.reaction = reaction;
  385. newReaction.count = 1;
  386. newReaction.userReacted = YES;
  387. [reactions addObject:newReaction];
  388. }
  389. }
  390. - (void)removeReactionTemporarily:(NSString *)reaction inReactions:(NSMutableArray *)reactions
  391. {
  392. NCChatReaction *removeReaction = nil;
  393. for (NCChatReaction *currentReaction in reactions) {
  394. if ([currentReaction.reaction isEqualToString:reaction]) {
  395. currentReaction.state = NCChatReactionStateRemoving;
  396. if (currentReaction.count > 1) {
  397. currentReaction.count -= 1;
  398. currentReaction.userReacted = NO;
  399. } else {
  400. removeReaction = currentReaction;
  401. }
  402. }
  403. }
  404. if (removeReaction) {
  405. [reactions removeObject:removeReaction];
  406. }
  407. }
  408. - (NSDictionary *)reactionsDictionary
  409. {
  410. NSDictionary *reactionsDictionary = @{};
  411. NSData *data = [self.reactionsJSONString dataUsingEncoding:NSUTF8StringEncoding];
  412. if (data) {
  413. NSError* error;
  414. NSDictionary* jsonData = [NSJSONSerialization JSONObjectWithData:data
  415. options:0
  416. error:&error];
  417. if (jsonData) {
  418. reactionsDictionary = jsonData;
  419. } else {
  420. NSLog(@"Error retrieving reactions JSON data: %@", error);
  421. }
  422. }
  423. return reactionsDictionary;
  424. }
  425. - (NSArray *)reactionsSelfArray
  426. {
  427. NSArray *reactionsSelfArray = @[];
  428. NSData *data = [self.reactionsSelfJSONString dataUsingEncoding:NSUTF8StringEncoding];
  429. if (data) {
  430. NSError* error;
  431. NSArray* jsonData = [NSJSONSerialization JSONObjectWithData:data
  432. options:0
  433. error:&error];
  434. if (jsonData) {
  435. reactionsSelfArray = jsonData;
  436. } else {
  437. NSLog(@"Error retrieving reactionsSelf JSON data: %@", error);
  438. }
  439. }
  440. return reactionsSelfArray;
  441. }
  442. - (NSMutableArray<NCChatReaction *> *)reactionsArray
  443. {
  444. NSMutableArray *reactionsArray = [NSMutableArray new];
  445. // Grab message reactions
  446. NSDictionary *reactionsDict = [self reactionsDictionary];
  447. for (NSString *reactionKey in reactionsDict.allKeys) {
  448. // We need to keep this check for users who installed v14.0 (beta 1)
  449. if ([reactionKey isEqualToString:@"self"]) {continue;}
  450. NCChatReaction *reaction = [NCChatReaction initWithReaction:reactionKey andCount:[[reactionsDict objectForKey:reactionKey] integerValue]];
  451. [reactionsArray addObject:reaction];
  452. }
  453. // Set flag for own reactions
  454. for (NSString *ownReaction in [self reactionsSelfArray]) {
  455. for (NCChatReaction *reaction in reactionsArray) {
  456. if ([reaction.reaction isEqualToString:ownReaction]) {
  457. reaction.userReacted = YES;
  458. }
  459. }
  460. }
  461. // Merge with temporary reactions
  462. [self mergeTemporaryReactionsWithReactions:reactionsArray];
  463. // Sort by reactions count
  464. NSSortDescriptor *valueDescriptor = [[NSSortDescriptor alloc] initWithKey:@"count" ascending:NO];
  465. NSArray *descriptors = [NSArray arrayWithObject:valueDescriptor];
  466. [reactionsArray sortUsingDescriptors:descriptors];
  467. return reactionsArray;
  468. }
  469. - (NSString *)getDeckCardUrlForReferenceProvider
  470. {
  471. // Check if the message is a shared deck card and a reference provider can be used to retrieve details
  472. if (self.deckCard != nil && self.deckCard.link != nil && [self.deckCard.link length] > 0) {
  473. if ([self isReferenceApiSupported]) {
  474. return _deckCardParameter.link;
  475. }
  476. }
  477. return nil;
  478. }
  479. - (BOOL)containsURL
  480. {
  481. if (!self.message) {
  482. return NO;
  483. }
  484. if (_urlDetectionDone) {
  485. return ([_urlDetected length] != 0);
  486. }
  487. if (![self isReferenceApiSupported]) {
  488. _urlDetectionDone = YES;
  489. return NO;
  490. }
  491. NSString *deckCardUrl = [self getDeckCardUrlForReferenceProvider];
  492. if (deckCardUrl != nil) {
  493. _urlDetectionDone = YES;
  494. _urlDetected = deckCardUrl;
  495. return YES;
  496. }
  497. NSDataDetector *dataDetector = [[NSDataDetector alloc] initWithTypes:NSTextCheckingTypeLink error:nil];
  498. NSArray *urlMatches = [dataDetector matchesInString:self.message options:0 range:NSMakeRange(0, [self.message length])];
  499. _urlDetectionDone = YES;
  500. for (NSTextCheckingResult *match in urlMatches) {
  501. NSURL *url = [match URL];
  502. NSString *scheme = [url scheme];
  503. // Check that the scheme is either https or http, because other schemes (like mailto) would be recognized as well
  504. if ([[scheme lowercaseString] isEqualToString:@"http"] || [[scheme lowercaseString] isEqualToString:@"https"]) {
  505. _urlDetected = [url absoluteString];
  506. return true;
  507. }
  508. }
  509. return false;
  510. }
  511. - (void)getReferenceDataWithCompletionBlock:(GetReferenceDataCompletionBlock)block
  512. {
  513. if (_referenceDataDone) {
  514. if (block) {
  515. block(self, _referenceData, _urlDetected);
  516. }
  517. } else {
  518. TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:_accountId];
  519. [[NCAPIController sharedInstance] getReferenceForUrlString:_urlDetected forAccount:account withCompletionBlock:^(NSDictionary *references, NSError *error) {
  520. if (block) {
  521. block(self, references, self->_urlDetected);
  522. }
  523. self->_referenceData = references;
  524. self->_referenceDataDone = YES;
  525. }];
  526. }
  527. }
  528. - (void)setPreviewImageHeight:(CGFloat)height
  529. {
  530. // Since the messageParameters property is a non-mutable dictionary, we create a mutable copy
  531. NSMutableDictionary *messageParameterDict = [[NSMutableDictionary alloc] initWithDictionary:self.messageParameters];
  532. NSMutableDictionary *fileParameterDict = [[NSMutableDictionary alloc] initWithDictionary:[messageParameterDict objectForKey:@"file"]];
  533. if (!fileParameterDict) {
  534. return;
  535. }
  536. [messageParameterDict setObject:fileParameterDict forKey:@"file"];
  537. [fileParameterDict setObject:@(height) forKey:@"preview-image-height"];
  538. NSData *jsonData = [NSJSONSerialization dataWithJSONObject:messageParameterDict
  539. options:0
  540. error:nil];
  541. if (jsonData) {
  542. NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
  543. // Only the JSON String is stored inside of the database
  544. self.messageParametersJSONString = jsonString;
  545. // Since we previously accessed the 'file' property, it would not be created from the JSON String again
  546. // Manually set it for the lifetime of this message
  547. self.file.previewImageHeight = height;
  548. // Save our changes to the database
  549. RLMRealm *realm = [RLMRealm defaultRealm];
  550. void (^update)(void) = ^void(){
  551. NCChatMessage *managedMessage = [NCChatMessage objectsWhere:@"internalId = %@", self.internalId].firstObject;
  552. [NCChatMessage updateChatMessage:managedMessage withChatMessage:self isRoomLastMessage:NO];
  553. };
  554. if ([realm inWriteTransaction]) {
  555. update();
  556. } else {
  557. [realm transactionWithBlock:^{
  558. update();
  559. }];
  560. }
  561. }
  562. }
  563. @end