chatsecureios / ChatSecure / Classes / Model / Yap Storage / OTRMediaItem.m @ ce32510a
History | View | Annotate | Download (14.2 KB)
1 |
// |
---|---|
2 |
// OTRMediaItem.m |
3 |
// ChatSecure |
4 |
// |
5 |
// Created by David Chiles on 1/19/15. |
6 |
// Copyright (c) 2015 Chris Ballinger. All rights reserved. |
7 |
// |
8 |
|
9 |
#import "OTRMediaItem.h" |
10 |
#import "OTRImages.h" |
11 |
#import "OTRFileItem.h" |
12 |
#import "OTRLog.h" |
13 |
#import "OTRTextItem.h" |
14 |
#import "OTRHTMLItem.h" |
15 |
@import JSQMessagesViewController; |
16 |
@import YapDatabase; |
17 |
@import OTRKit; |
18 |
@import MobileCoreServices; |
19 |
@import OTRAssets; |
20 |
#import "OTRDatabaseManager.h" |
21 |
#import <ChatSecureCore/ChatSecureCore-Swift.h> |
22 |
|
23 |
static NSString* GetExtensionForMimeType(NSString* mimeType) { |
24 |
NSCParameterAssert(mimeType.length > 0); |
25 |
if (!mimeType.length) { return @""; } |
26 |
NSString *extension = @""; |
27 |
CFStringRef cfMimeType = (__bridge CFStringRef)mimeType; |
28 |
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, cfMimeType, NULL); |
29 |
if (uti) { |
30 |
extension = CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension)); |
31 |
CFRelease(uti); |
32 |
} |
33 |
return extension; |
34 |
} |
35 |
|
36 |
|
37 |
@implementation OTRMediaItem |
38 |
@synthesize mimeType = _mimeType; |
39 |
@synthesize parentObjectKey = _parentObjectKey; |
40 |
@synthesize parentObjectCollection = _parentObjectCollection; |
41 |
@dynamic displayText; |
42 |
|
43 |
- (instancetype) initWithFilename:(NSString *)filename mimeType:(NSString*)mimeType isIncoming:(BOOL)isIncoming { |
44 |
NSParameterAssert(filename); |
45 |
if (self = [super init]) { |
46 |
if (!filename.length) { |
47 |
filename = @"file"; |
48 |
} |
49 |
_filename = [filename copy]; |
50 |
_isIncoming = isIncoming; |
51 |
_transferProgress = 0.0f; |
52 |
if (!mimeType.length) { |
53 |
_mimeType = [self.class mimeTypeForFilename:filename]; |
54 |
} else { |
55 |
NSString *extension = GetExtensionForMimeType(mimeType); |
56 |
if (![filename.pathExtension isEqualToString:extension]) { |
57 |
DDLogWarn(@"Given file extension does not match expected extension from mime type: %@ %@", filename.pathExtension, extension); |
58 |
if (!filename.pathExtension.length && extension.length > 0) { |
59 |
_filename = [filename stringByAppendingPathExtension:extension]; |
60 |
DDLogInfo(@"Created new filename with best guess for file extension: %@", _filename); |
61 |
} |
62 |
} |
63 |
_mimeType = [mimeType copy]; |
64 |
} |
65 |
} |
66 |
return self; |
67 |
} |
68 |
|
69 |
+ (NSString*) mimeTypeForFilename:(NSString*)filename { |
70 |
// This is to handle the case where we're storing temporary items with the full URL as the filename |
71 |
NSURL *url = [NSURL URLWithString:filename]; |
72 |
NSString *pathExtension = filename.pathExtension; |
73 |
if (url) { |
74 |
pathExtension = url.pathExtension; |
75 |
} |
76 |
if (!pathExtension) { |
77 |
pathExtension = @""; |
78 |
} |
79 |
NSString *mimeType = OTRKitGetMimeTypeForExtension(pathExtension); |
80 |
return mimeType; |
81 |
} |
82 |
|
83 |
/** Returns the appropriate subclass (OTRImageItem, etc) for incoming file */ |
84 |
+ (instancetype) incomingItemWithFilename:(NSString*)filename |
85 |
mimeType:(nullable NSString*)mimeType { |
86 |
if (!mimeType) { |
87 |
mimeType = [self mimeTypeForFilename:filename]; |
88 |
} |
89 |
NSRange imageRange = [mimeType rangeOfString:@"image"]; |
90 |
NSRange audioRange = [mimeType rangeOfString:@"audio"]; |
91 |
NSRange videoRange = [mimeType rangeOfString:@"video"]; |
92 |
NSRange htmlRange = [mimeType rangeOfString:@"text/html"]; |
93 |
NSRange textRange = [mimeType rangeOfString:@"text"]; |
94 |
|
95 |
OTRMediaItem *mediaItem = nil; |
96 |
Class mediaClass = nil; |
97 |
if(audioRange.location == 0) { |
98 |
mediaClass = [OTRAudioItem class]; |
99 |
} else if (imageRange.location == 0) { |
100 |
mediaClass = [OTRImageItem class]; |
101 |
} else if (videoRange.location == 0) { |
102 |
mediaClass = [OTRVideoItem class]; |
103 |
} else if (textRange.location == 0) { |
104 |
if (htmlRange.location == 0) { |
105 |
mediaClass = [OTRHTMLItem class]; |
106 |
} else { |
107 |
mediaClass = [OTRTextItem class]; |
108 |
} |
109 |
} else { |
110 |
mediaClass = [OTRFileItem class]; |
111 |
} |
112 |
|
113 |
if (mediaClass) { |
114 |
mediaItem = [[mediaClass alloc] initWithFilename:filename mimeType:mimeType isIncoming:YES]; |
115 |
} |
116 |
return mediaItem; |
117 |
} |
118 |
|
119 |
- (NSString*) mimeType { |
120 |
if (_mimeType) { |
121 |
return _mimeType; |
122 |
} else { |
123 |
return [self.class mimeTypeForFilename:self.filename]; |
124 |
} |
125 |
} |
126 |
|
127 |
- (void)touchParentMessageWithTransaction:(YapDatabaseReadWriteTransaction *)transaction |
128 |
{ |
129 |
[self touchParentObjectWithTransaction:transaction]; |
130 |
NSString *extensionName = [YapDatabaseConstants extensionName:DatabaseExtensionNameRelationshipExtensionName]; |
131 |
NSString *edgeName = [YapDatabaseConstants edgeName:RelationshipEdgeNameMessageMediaEdgeName]; |
132 |
[[transaction ext:extensionName] enumerateEdgesWithName:edgeName destinationKey:self.uniqueId collection:[[self class] collection] usingBlock:^(YapDatabaseRelationshipEdge *edge, BOOL *stop) { |
133 |
[transaction touchObjectForKey:edge.sourceKey inCollection:edge.sourceCollection]; |
134 |
}]; |
135 |
} |
136 |
|
137 |
- (nullable id) parentObjectWithTransaction:(YapDatabaseReadTransaction*)transaction { |
138 |
if (!self.parentObjectKey || !self.parentObjectCollection) { |
139 |
return nil; |
140 |
} |
141 |
id parent = [transaction objectForKey:self.parentObjectKey inCollection:self.parentObjectCollection]; |
142 |
return parent; |
143 |
} |
144 |
|
145 |
- (void) touchParentObjectWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { |
146 |
if (!self.parentObjectKey || !self.parentObjectCollection) { |
147 |
return; |
148 |
} |
149 |
[transaction touchObjectForKey:self.parentObjectKey inCollection:self.parentObjectCollection]; |
150 |
} |
151 |
|
152 |
- (id<OTRMessageProtocol>)parentMessageWithTransaction:(YapDatabaseReadTransaction *)readTransaction |
153 |
{ |
154 |
id parent = [self parentObjectWithTransaction:readTransaction]; |
155 |
if (parent && [parent conformsToProtocol:@protocol(OTRMessageProtocol)]) { |
156 |
return parent; |
157 |
} |
158 |
__block id<OTRMessageProtocol> message = nil; |
159 |
NSString *extensionName = [YapDatabaseConstants extensionName:DatabaseExtensionNameRelationshipExtensionName]; |
160 |
NSString *edgeName = [YapDatabaseConstants edgeName:RelationshipEdgeNameMessageMediaEdgeName]; |
161 |
[[readTransaction ext:extensionName] enumerateEdgesWithName:edgeName destinationKey:self.uniqueId collection:[[self class] collection] usingBlock:^(YapDatabaseRelationshipEdge *edge, BOOL *stop) { |
162 |
message = [OTRBaseMessage fetchObjectWithUniqueID:edge.sourceKey transaction:readTransaction]; |
163 |
*stop = YES; |
164 |
}]; |
165 |
return message; |
166 |
} |
167 |
|
168 |
#pragma - mark JSQMessageMediaData Methods |
169 |
|
170 |
- (NSUInteger)mediaHash |
171 |
{ |
172 |
return [self hash]; |
173 |
} |
174 |
|
175 |
- (UIView *)mediaView |
176 |
{ |
177 |
[self fetchMediaData]; |
178 |
UIView *errorView = [self errorView]; |
179 |
if (errorView) { return errorView; } |
180 |
return nil; |
181 |
} |
182 |
|
183 |
- (CGSize)mediaViewDisplaySize |
184 |
{ |
185 |
// This is an absolutely terrible way of doing this |
186 |
if ([self downloadMessage].messageError) { |
187 |
return CGSizeMake(210.0f, 100.0f); |
188 |
} |
189 |
//Taken from JSQMediaItem Example project |
190 |
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { |
191 |
return CGSizeMake(315.0f, 225.0f); |
192 |
} |
193 |
return CGSizeMake(210.0f, 150.0f); |
194 |
} |
195 |
|
196 |
- (UIView *)mediaPlaceholderView |
197 |
{ |
198 |
CGSize size = [self mediaViewDisplaySize]; |
199 |
UIView *view = [JSQMessagesMediaPlaceholderView viewWithActivityIndicator]; |
200 |
view.frame = CGRectMake(0.0f, 0.0f, size.width, size.height); |
201 |
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:view isOutgoing:!self.isIncoming]; |
202 |
return view; |
203 |
} |
204 |
|
205 |
- (NSUInteger)hash |
206 |
{ |
207 |
return self.filename.hash; |
208 |
} |
209 |
|
210 |
- (nullable NSURL*) mediaServerURLWithTransaction:(YapDatabaseReadTransaction*)transaction { |
211 |
id<OTRMessageProtocol> message = [self parentMessageWithTransaction:transaction]; |
212 |
id<OTRThreadOwner> threadOwner = [message threadOwnerWithTransaction:transaction]; |
213 |
NSString *buddyUniqueId = [threadOwner threadIdentifier]; |
214 |
if (!buddyUniqueId) { |
215 |
return nil; |
216 |
} |
217 |
NSURL *url = [[OTRMediaServer sharedInstance] urlForMediaItem:self buddyUniqueId:buddyUniqueId]; |
218 |
return url; |
219 |
} |
220 |
|
221 |
+ (nullable instancetype) mediaItemForMessage:(id<OTRMessageProtocol>)message transaction:(YapDatabaseReadTransaction*)transaction { |
222 |
if (!message.messageMediaItemKey.length) { |
223 |
return nil; |
224 |
} |
225 |
OTRMediaItem *item = [OTRMediaItem fetchObjectWithUniqueID:message.messageMediaItemKey transaction:transaction]; |
226 |
return item; |
227 |
} |
228 |
|
229 |
- (BOOL) shouldFetchMediaData { |
230 |
return YES; |
231 |
} |
232 |
|
233 |
- (void) fetchMediaData { |
234 |
if (![self shouldFetchMediaData]) { |
235 |
return; |
236 |
} |
237 |
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
238 |
if (![self shouldFetchMediaData]) { |
239 |
return; |
240 |
} |
241 |
// The superview should handle creating the actual imageview |
242 |
// this code is used to fetch the image from the data store and then cache in ram |
243 |
__block id<OTRThreadOwner> thread = nil; |
244 |
__block id<OTRMessageProtocol> message = nil; |
245 |
[OTRDatabaseManager.shared.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { |
246 |
message = [self parentMessageWithTransaction:transaction]; |
247 |
thread = [message threadOwnerWithTransaction:transaction]; |
248 |
}]; |
249 |
if (!message || !thread) { |
250 |
DDLogError(@"Missing parent message or thread for media message!"); |
251 |
return; |
252 |
} |
253 |
NSError *error = nil; |
254 |
NSData *data = [OTRMediaFileManager.shared dataForItem:self buddyUniqueId:thread.threadIdentifier error:&error]; |
255 |
if(!data.length) { |
256 |
DDLogError(@"No data found for media item: %@", error); |
257 |
} else if (![self handleMediaData:data message:message]) { |
258 |
DDLogError(@"Could not handle display for media item %@", self); |
259 |
} else { |
260 |
// Success, touch parent message to display it. |
261 |
[[OTRDatabaseManager sharedInstance].readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) { |
262 |
[self touchParentMessageWithTransaction:transaction]; |
263 |
}]; |
264 |
} |
265 |
}); |
266 |
} |
267 |
|
268 |
/** Overrideable in subclasses. This is called after data is fetched from db, but before display */ |
269 |
- (BOOL) handleMediaData:(NSData*)mediaData message:(id<OTRMessageProtocol>)message { |
270 |
NSParameterAssert(mediaData.length > 0); |
271 |
if (!mediaData.length) { return NO; } |
272 |
return NO; |
273 |
} |
274 |
|
275 |
/** โ ๏ธ Do not call from within an existing database transaction */ |
276 |
- (nullable id<OTRDownloadMessage>) downloadMessage { |
277 |
__block id<OTRMessageProtocol> message = nil; |
278 |
[OTRDatabaseManager.shared.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { |
279 |
message = [self parentMessageWithTransaction:transaction]; |
280 |
}]; |
281 |
if ([message conformsToProtocol:@protocol(OTRDownloadMessage)]) { |
282 |
return (id<OTRDownloadMessage>)message; |
283 |
} |
284 |
return nil; |
285 |
} |
286 |
|
287 |
- (UIView*) errorView { |
288 |
id<OTRDownloadMessage> message = [self downloadMessage]; |
289 |
if (message.messageError) { |
290 |
MediaDownloadView *downloadView = [MediaDownloadView otr_viewFromNib]; |
291 |
if (!downloadView) { |
292 |
return nil; |
293 |
} |
294 |
[downloadView setMediaItem:self message:message]; |
295 |
downloadView.backgroundColor = [UIColor jsq_messageBubbleLightGrayColor]; |
296 |
CGSize size = [self mediaViewDisplaySize]; |
297 |
downloadView.frame = CGRectMake(0, 0, size.width, size.height); |
298 |
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:downloadView isOutgoing:!self.isIncoming]; |
299 |
return downloadView; |
300 |
} |
301 |
return nil; |
302 |
} |
303 |
|
304 |
- (NSString*) displayText { |
305 |
NSString *item = [NSString stringWithFormat:@"๐ %@", FILE_MESSAGE_STRING()]; |
306 |
if ([self isKindOfClass:[OTRImageItem class]]) { |
307 |
item = [NSString stringWithFormat:@"๐ท %@", PICTURE_MESSAGE_STRING()]; |
308 |
} else if ([self isKindOfClass:[OTRVideoItem class]]) { |
309 |
item = [NSString stringWithFormat:@"๐ฅ %@", VIDEO_MESSAGE_STRING()]; |
310 |
} else if ([self isKindOfClass:[OTRAudioItem class]]) { |
311 |
item = [NSString stringWithFormat:@"๐ %@", AUDIO_MESSAGE_STRING()]; |
312 |
} else { |
313 |
NSURL *url = [NSURL URLWithString:self.filename]; |
314 |
if (url) { |
315 |
item = [NSString stringWithFormat:@"๐ %@", url.absoluteString]; |
316 |
} |
317 |
} |
318 |
return item; |
319 |
} |
320 |
|
321 |
+ (NSDictionary *)encodingBehaviorsByPropertyKey { |
322 |
NSMutableDictionary *behaviors = [NSMutableDictionary dictionaryWithDictionary:[super encodingBehaviorsByPropertyKey]]; |
323 |
[behaviors setObject:@(MTLModelEncodingBehaviorExcluded) forKey:NSStringFromSelector(@selector(displayText))]; |
324 |
return behaviors; |
325 |
} |
326 |
|
327 |
|
328 |
#pragma - mark YapDatabaseRelationshipNode Methods |
329 |
|
330 |
- (id)yapDatabaseRelationshipEdgeDeleted:(YapDatabaseRelationshipEdge *)edge withReason:(YDB_NotifyReason)reason |
331 |
{ |
332 |
//#865 Delete File because the parent OTRMessage was deleted |
333 |
__block id<OTRThreadOwner> thread = nil; |
334 |
__block id<OTRMessageProtocol> message = nil; |
335 |
[OTRDatabaseManager.shared.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { |
336 |
message = [self parentMessageWithTransaction:transaction]; |
337 |
thread = [message threadOwnerWithTransaction:transaction]; |
338 |
}]; |
339 |
if (!message || !thread) { |
340 |
DDLogError(@"Missing parent message or thread for media message!"); |
341 |
return nil; |
342 |
} |
343 |
|
344 |
NSString *buddyUniqueId = [thread threadIdentifier]; |
345 |
if (!buddyUniqueId) { |
346 |
return nil; |
347 |
} |
348 |
|
349 |
[[OTRMediaFileManager sharedInstance] deleteDataForItem:self buddyUniqueId:buddyUniqueId completion:^(BOOL success, NSError *error) { |
350 |
if (error) { |
351 |
DDLogError(@"ERROR in deleting data for media item"); |
352 |
} |
353 |
} completionQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; |
354 |
|
355 |
return nil; |
356 |
} |
357 |
|
358 |
#pragma - mark Class Methods |
359 |
|
360 |
+ (CGSize)normalizeWidth:(CGFloat)width height:(CGFloat)height |
361 |
{ |
362 |
CGFloat maxWidth = 210; |
363 |
CGFloat maxHeight = 150; |
364 |
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { |
365 |
maxWidth = 315; |
366 |
maxHeight = 225; |
367 |
} |
368 |
|
369 |
float aspectRatio = width / height; |
370 |
|
371 |
if (aspectRatio < 1) { |
372 |
//Taller then wider then use max height and resize width |
373 |
CGFloat newWidth = floorf(maxHeight * aspectRatio); |
374 |
return CGSizeMake(newWidth, maxHeight); |
375 |
} |
376 |
else { |
377 |
//Wider than taller then use max width and resize height |
378 |
CGFloat newHeight = floorf(maxWidth * 1/aspectRatio); |
379 |
return CGSizeMake(maxWidth, newHeight); |
380 |
} |
381 |
} |
382 |
|
383 |
@end |