TextStyleUtils.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2018-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "TextStyleUtils.h"
  21. #import "ContactStore.h"
  22. #import "Contact.h"
  23. #import "MyIdentityStore.h"
  24. #import "UILabel+Markup.h"
  25. #import "BundleUtil.h"
  26. static NSString *regex = @"@\\[[0-9A-Z*@]{8}\\]";
  27. @implementation TextStyleUtils
  28. + (NSAttributedString*)makeAttributedStringFromString:(NSString*)string withFont:(UIFont*)font textColor:(UIColor *)textColor isOwn:(BOOL)isOwn application:(UIApplication *)application {
  29. if (string == nil)
  30. return nil;
  31. NSTextCheckingTypes textCheckingTypes = NSTextCheckingTypeLink;
  32. if (application) {
  33. static dispatch_once_t onceToken;
  34. static BOOL canOpenPhoneLinks;
  35. dispatch_once(&onceToken, ^{
  36. canOpenPhoneLinks = [application canOpenURL:[NSURL URLWithString:@"tel:0"]];
  37. });
  38. if (canOpenPhoneLinks)
  39. textCheckingTypes |= NSTextCheckingTypePhoneNumber;
  40. }
  41. NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:textCheckingTypes error:NULL];
  42. NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string attributes:@{NSFontAttributeName: font, NSForegroundColorAttributeName: textColor ? textColor : [Colors fontNormal]}];
  43. [detector enumerateMatchesInString:string options:0 range:NSMakeRange(0, string.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
  44. NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
  45. attributes[@"ZSWTappableLabelTappableRegionAttributeName"] = @YES;
  46. attributes[@"ZSWTappableLabelHighlightedForegroundAttributeName"] = isOwn == true ? [Colors fontLink] : [Colors fontLinkReceived];
  47. attributes[NSForegroundColorAttributeName] = isOwn == true ? [Colors fontLink] : [Colors fontLinkReceived];
  48. attributes[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
  49. attributes[@"NSTextCheckingResult"] = result;
  50. [attributedString addAttributes:attributes range:result.range];
  51. }];
  52. return attributedString;
  53. }
  54. + (NSString *)makeMentionsStringForText:(NSString *)text {
  55. if (!text) {
  56. return text;
  57. }
  58. NSRegularExpression *mentionsRegex = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:nil];
  59. BOOL finished = NO;
  60. int lastNotFoundIndex = -1;
  61. while (!finished) {
  62. NSArray *mentionResults = [mentionsRegex matchesInString:text options:NSMatchingReportCompletion range:NSMakeRange(0, [text length])];
  63. NSTextCheckingResult *result = nil;
  64. if (lastNotFoundIndex == -1) {
  65. result = mentionResults.firstObject;
  66. } else {
  67. if (mentionResults.count >= lastNotFoundIndex+2) {
  68. result = mentionResults[lastNotFoundIndex+1];
  69. }
  70. }
  71. if (!result) {
  72. finished = YES;
  73. break;
  74. }
  75. NSString *mentionTag = [text substringWithRange:result.range];
  76. NSString *identity = [mentionTag substringWithRange:NSMakeRange(2, 8)].uppercaseString;
  77. Contact *contact = [[ContactStore sharedContactStore] contactForIdentity:identity];
  78. if (contact || [identity isEqualToString:[[MyIdentityStore sharedMyIdentityStore] identity]] || [identity isEqualToString:@"@@@@@@@@"]) {
  79. NSString *displayName = [BundleUtil localizedStringForKey:@"me"];
  80. if ([MyIdentityStore sharedMyIdentityStore].pushFromName.length > 0) {
  81. displayName = [MyIdentityStore sharedMyIdentityStore].pushFromName;
  82. }
  83. if (contact) {
  84. displayName = contact.mentionName;
  85. } else if ([identity isEqualToString:@"@@@@@@@@"]) {
  86. displayName = [BundleUtil localizedStringForKey:@"mentions_all"];
  87. }
  88. text = [text stringByReplacingCharactersInRange:result.range withString:[NSString stringWithFormat:@"@%@", displayName]];
  89. } else {
  90. text = [text stringByReplacingCharactersInRange:result.range withString:[NSString stringWithFormat:@"@%@", identity]];
  91. if (lastNotFoundIndex == -1) {
  92. lastNotFoundIndex = 0;
  93. } else {
  94. lastNotFoundIndex++;
  95. }
  96. }
  97. }
  98. return text;
  99. }
  100. + (NSAttributedString *)makeMentionsAttributedStringForAttributedString:(NSMutableAttributedString *)text textFont:(UIFont *)textFont atColor:(UIColor *)atColor messageInfo:(int)messageInfo application:(UIApplication *)application {
  101. NSRegularExpression *mentionsRegex = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:nil];
  102. NSMutableAttributedString *origTextAttributed = text;
  103. BOOL finished = NO;
  104. int lastNotFoundIndex = -1;
  105. while (!finished) {
  106. NSArray *mentionResults = [mentionsRegex matchesInString:origTextAttributed.string options:NSMatchingReportCompletion range:NSMakeRange(0, [origTextAttributed.string length])];
  107. NSTextCheckingResult *result = nil;
  108. if (lastNotFoundIndex == -1) {
  109. result = mentionResults.firstObject;
  110. } else {
  111. if (mentionResults.count >= lastNotFoundIndex+2) {
  112. result = mentionResults[lastNotFoundIndex+1];
  113. }
  114. }
  115. if (!result) {
  116. finished = YES;
  117. break;
  118. }
  119. NSString *mentionTag = [origTextAttributed.string substringWithRange:result.range];
  120. NSString *identity = [mentionTag substringWithRange:NSMakeRange(2, 8)].uppercaseString;
  121. Contact *contact = [[ContactStore sharedContactStore] contactForIdentity:identity];
  122. NSMutableDictionary *paddingAttributeLeft = [[NSMutableDictionary alloc] initWithDictionary:@{@"TTTBackgroundFillColor": [Colors mentionBackground:messageInfo],
  123. NSForegroundColorAttributeName: [Colors mentionBackground:messageInfo],
  124. NSTextEffectAttributeName: NSTextEffectLetterpressStyle,
  125. @"TTTBackgroundCornerRadius": [NSNumber numberWithFloat:3.0f],
  126. @"ZSWTappableLabelTappableRegionAttributeName": @NO,
  127. @"TTTBackgroundFillPadding": [NSNumber valueWithUIEdgeInsets:UIEdgeInsetsMake(-1, 0, -1, 5)] }];
  128. NSMutableDictionary *paddingAttributeRight = [[NSMutableDictionary alloc] initWithDictionary:@{@"TTTBackgroundFillColor": [Colors mentionBackground:messageInfo],
  129. NSForegroundColorAttributeName: [Colors mentionBackground:messageInfo],
  130. NSTextEffectAttributeName: NSTextEffectLetterpressStyle,
  131. @"TTTBackgroundCornerRadius": [NSNumber numberWithFloat:3.0f],
  132. @"ZSWTappableLabelTappableRegionAttributeName": @NO,
  133. @"TTTBackgroundFillPadding": [NSNumber valueWithUIEdgeInsets:UIEdgeInsetsMake(-1, 5, -1, 0)] }];
  134. NSMutableDictionary *attributeAt = [[NSMutableDictionary alloc] initWithDictionary:@{@"TTTBackgroundFillColor": [Colors mentionBackground:messageInfo],
  135. NSForegroundColorAttributeName: atColor,
  136. NSTextEffectAttributeName: NSTextEffectLetterpressStyle,
  137. @"TTTBackgroundCornerRadius": [NSNumber numberWithFloat:0.0f],
  138. @"ZSWTappableLabelTappableRegionAttributeName": @NO,
  139. NSBaselineOffsetAttributeName: @1,
  140. @"TTTBackgroundFillPadding": [NSNumber valueWithUIEdgeInsets:UIEdgeInsetsMake(-1, 0, -1, 0)]
  141. }];
  142. if (contact || [identity isEqualToString:[[MyIdentityStore sharedMyIdentityStore] identity]] || [identity isEqualToString:@"@@@@@@@@"]) {
  143. NSMutableDictionary *attributes = [[NSMutableDictionary alloc] initWithDictionary:@{@"TTTBackgroundFillColor": [Colors mentionBackground:messageInfo],
  144. NSTextEffectAttributeName: NSTextEffectLetterpressStyle,
  145. @"TTTBackgroundCornerRadius": [NSNumber numberWithFloat:0.0f],
  146. @"TTTBackgroundFillPadding": [NSNumber valueWithUIEdgeInsets:UIEdgeInsetsMake(-1, 0, -1, 0)]
  147. }];
  148. NSString *displayName = [BundleUtil localizedStringForKey:@"me"];
  149. if (contact) {
  150. displayName = contact.mentionName;
  151. [attributes setObject:@YES forKey:@"ZSWTappableLabelTappableRegionAttributeName"];
  152. [attributes setObject:contact forKey:@"NSTextCheckingResult"];
  153. [attributes setObject:[Colors fontNormal] forKey:NSForegroundColorAttributeName];
  154. [paddingAttributeLeft setObject:@YES forKey:@"ZSWTappableLabelTappableRegionAttributeName"];
  155. [paddingAttributeLeft setObject:contact forKey:@"NSTextCheckingResult"];
  156. [paddingAttributeRight setObject:@YES forKey:@"ZSWTappableLabelTappableRegionAttributeName"];
  157. [paddingAttributeRight setObject:contact forKey:@"NSTextCheckingResult"];
  158. [attributeAt setObject:@YES forKey:@"ZSWTappableLabelTappableRegionAttributeName"];
  159. [attributeAt setObject:contact forKey:@"NSTextCheckingResult"];
  160. } else if ([identity isEqualToString:@"@@@@@@@@"]) {
  161. displayName = [BundleUtil localizedStringForKey:@"mentions_all"];
  162. [paddingAttributeLeft setObject:[Colors mentionBackgroundMe:messageInfo] forKey:@"TTTBackgroundFillColor"];
  163. [paddingAttributeLeft setObject:[Colors mentionBackgroundMe:messageInfo] forKey:NSForegroundColorAttributeName];
  164. [paddingAttributeRight setObject:[Colors mentionBackgroundMe:messageInfo] forKey:@"TTTBackgroundFillColor"];
  165. [paddingAttributeRight setObject:[Colors mentionBackgroundMe:messageInfo] forKey:NSForegroundColorAttributeName];
  166. [attributeAt setObject:[Colors mentionBackgroundMe:messageInfo] forKey:@"TTTBackgroundFillColor"];
  167. [attributeAt setObject:[[Colors mentionTextMe:messageInfo] colorWithAlphaComponent:0.6] forKey:NSForegroundColorAttributeName];
  168. [attributes setObject:[Colors mentionBackgroundMe:messageInfo] forKey:@"TTTBackgroundFillColor"];
  169. [attributes setObject:[Colors mentionTextMe:messageInfo] forKey:NSForegroundColorAttributeName];
  170. } else {
  171. // me
  172. if ([MyIdentityStore sharedMyIdentityStore].pushFromName.length > 0) {
  173. displayName = [MyIdentityStore sharedMyIdentityStore].pushFromName;
  174. }
  175. [paddingAttributeLeft setObject:@YES forKey:@"ZSWTappableLabelTappableRegionAttributeName"];
  176. [paddingAttributeLeft setObject:@"meContact" forKey:@"NSTextCheckingResult"];
  177. [paddingAttributeRight setObject:@YES forKey:@"ZSWTappableLabelTappableRegionAttributeName"];
  178. [paddingAttributeRight setObject:@"meContact" forKey:@"NSTextCheckingResult"];
  179. [attributeAt setObject:@YES forKey:@"ZSWTappableLabelTappableRegionAttributeName"];
  180. [attributeAt setObject:@"meContact" forKey:@"NSTextCheckingResult"];
  181. [attributes setObject:@YES forKey:@"ZSWTappableLabelTappableRegionAttributeName"];
  182. [attributes setObject:@"meContact" forKey:@"NSTextCheckingResult"];
  183. [paddingAttributeLeft setObject:[Colors mentionBackgroundMe:messageInfo] forKey:@"TTTBackgroundFillColor"];
  184. [paddingAttributeLeft setObject:[Colors mentionBackgroundMe:messageInfo] forKey:NSForegroundColorAttributeName];
  185. [paddingAttributeRight setObject:[Colors mentionBackgroundMe:messageInfo] forKey:@"TTTBackgroundFillColor"];
  186. [paddingAttributeRight setObject:[Colors mentionBackgroundMe:messageInfo] forKey:NSForegroundColorAttributeName];
  187. [attributeAt setObject:[Colors mentionBackgroundMe:messageInfo] forKey:@"TTTBackgroundFillColor"];
  188. [attributeAt setObject:[[Colors mentionTextMe:messageInfo] colorWithAlphaComponent:0.6] forKey:NSForegroundColorAttributeName];
  189. [attributes setObject:[Colors mentionBackgroundMe:messageInfo] forKey:@"TTTBackgroundFillColor"];
  190. [attributes setObject:[Colors mentionTextMe:messageInfo] forKey:NSForegroundColorAttributeName];
  191. }
  192. displayName = [NSString stringWithFormat:@".@%@.", displayName];
  193. [origTextAttributed replaceCharactersInRange:result.range withString:displayName];
  194. [origTextAttributed addAttributes:attributes range:NSMakeRange(result.range.location, displayName.length)];
  195. [origTextAttributed addAttributes:attributeAt range:NSMakeRange(result.range.location+1, 1)];
  196. [origTextAttributed addAttributes:paddingAttributeLeft range:NSMakeRange(result.range.location, 1)];
  197. [origTextAttributed addAttributes:paddingAttributeRight range:NSMakeRange(result.range.location+displayName.length-1, 1)];
  198. } else {
  199. NSMutableDictionary *attributes = [[NSMutableDictionary alloc] initWithDictionary:@{@"TTTBackgroundFillColor": [Colors mentionBackground:messageInfo],
  200. NSTextEffectAttributeName: NSTextEffectLetterpressStyle,
  201. @"TTTBackgroundCornerRadius": [NSNumber numberWithFloat:1.0f],
  202. @"TTTBackgroundFillPadding": [NSNumber valueWithUIEdgeInsets:UIEdgeInsetsMake(-1, 0, -1, 0)]
  203. }];
  204. identity = [NSString stringWithFormat:@".@%@.", identity];
  205. [origTextAttributed replaceCharactersInRange:result.range withString:identity];
  206. [origTextAttributed addAttributes:attributes range:NSMakeRange(result.range.location, identity.length)];
  207. [origTextAttributed addAttributes:attributeAt range:NSMakeRange(result.range.location+1, 1)];
  208. [origTextAttributed addAttributes:paddingAttributeLeft range:NSMakeRange(result.range.location, 1)];
  209. [origTextAttributed addAttributes:paddingAttributeRight range:NSMakeRange(result.range.location+identity.length-1, 1)];
  210. if (lastNotFoundIndex == -1) {
  211. lastNotFoundIndex = 0;
  212. } else {
  213. lastNotFoundIndex++;
  214. }
  215. }
  216. }
  217. return origTextAttributed;
  218. }
  219. + (NSAttributedString *)makeMentionsAttributedStringForText:(NSString *)text textFont:(UIFont *)textFont textColor:(UIColor *)textColor isOwn:(BOOL)isOwn messageInfo:(int)messageInfo application:(UIApplication *)application {
  220. NSMutableAttributedString *origTextAttributed = [[NSMutableAttributedString alloc] initWithAttributedString:[TextStyleUtils makeAttributedStringFromString:text withFont:textFont textColor:textColor isOwn:isOwn application:application]];
  221. return [TextStyleUtils makeMentionsAttributedStringForAttributedString:origTextAttributed textFont:textFont atColor:[textColor colorWithAlphaComponent:0.4] messageInfo:messageInfo application:application];
  222. }
  223. + (BOOL)isMeOrAllMentionInText:(NSString *)text {
  224. if (!text) {
  225. return NO;
  226. }
  227. NSRegularExpression *mentionsRegex = [NSRegularExpression regularExpressionWithPattern:regex options:NSRegularExpressionCaseInsensitive error:nil];
  228. BOOL found = NO;
  229. BOOL finished = NO;
  230. int lastNotFoundIndex = -1;
  231. while (!finished || !found) {
  232. NSArray *mentionResults = [mentionsRegex matchesInString:text options:NSMatchingReportCompletion range:NSMakeRange(0, [text length])];
  233. NSTextCheckingResult *result = nil;
  234. if (lastNotFoundIndex == -1) {
  235. result = mentionResults.firstObject;
  236. } else {
  237. if (mentionResults.count >= lastNotFoundIndex+2) {
  238. result = mentionResults[lastNotFoundIndex+1];
  239. }
  240. }
  241. if (!result) {
  242. finished = YES;
  243. break;
  244. }
  245. NSString *mentionTag = [text substringWithRange:result.range];
  246. NSString *identity = [mentionTag substringWithRange:NSMakeRange(2, 8)].uppercaseString;
  247. if ([identity isEqualToString:[[MyIdentityStore sharedMyIdentityStore] identity]] || [identity isEqualToString:@"@@@@@@@@"]) {
  248. found = YES;
  249. break;
  250. } else {
  251. lastNotFoundIndex += 1;
  252. }
  253. }
  254. return found;
  255. }
  256. @end