1
0
mirror of https://github.com/danog/Telegram.git synced 2024-12-02 09:27:55 +01:00
Telegram/Telegraph/TGNotificationController.m
2016-02-25 01:03:51 +01:00

1271 lines
46 KiB
Objective-C

#import "TGNotificationController.h"
#import "TGOverlayControllerWindow.h"
#import <AVFoundation/AVFoundation.h>
#import "TGOverlayController.h"
#import "TGMenuSheetController.h"
#import "TGPickerSheet.h"
#import "TGSingleStickerPreviewWindow.h"
#import "TGModernConversationController.h"
#import "TGGenericModernConversationCompanion.h"
#import "TGNotificationOverlayView.h"
#import "TGNotificationView.h"
#import "ActionStage.h"
#import "TGAppDelegate.h"
#import "TGTelegraph.h"
#import "TGConversation.h"
#import "TGDatabase.h"
#import "TGPeerIdAdapter.h"
#import "TGRemoteImageView.h"
#import "TGDownloadManager.h"
#import "TGSendMessageSignals.h"
#import "TGChatMessageListSignal.h"
#import "TGRecentHashtagsSignal.h"
#import "TGConversationSignals.h"
#import "TGChatMessageListSignal.h"
#import "TGStickersSignals.h"
#import "TGStickerAssociation.h"
#import "TGMediaStoreContext.h"
#import "TGPreparedRemoteImageMessage.h"
#import "TGPreparedLocalDocumentMessage.h"
#import "TGModernConversationAudioPlayer.h"
#import "TGMessageViewedContentProperty.h"
#import "TGGenericPeerPlaylistSignals.h"
const NSTimeInterval TGNotificationTimerInterval = 0.5;
const NSUInteger TGNotificationInterItemDelay = 2;
const NSUInteger TGNotificationInterItemDelayAfterHide = 1;
const NSUInteger TGNotificationExpandedTimeout = 60;
@interface TGNotificationWindow : UIWindow
@property (nonatomic, copy) bool (^pointInside)(CGPoint);
@end
@interface TGNotificationWindowViewController : TGOverlayWindowViewController
@end
@interface TGNotificationItem : NSObject
@property (nonatomic, readonly) int32_t identifier;
@property (nonatomic, readonly) int64_t conversationId;
@property (nonatomic, readonly) int32_t replyToMid;
@property (nonatomic, readonly) NSTimeInterval duration;
@property (nonatomic, readonly) bool isChannelGroup;
@property (nonatomic, copy) void (^configure)(TGNotificationContentView *, bool *);
- (instancetype)initWithConversation:(TGConversation *)conversation identifier:(int32_t)identifier replyToMid:(int32_t)replyToMid duration:(NSTimeInterval)duration configure:(void (^)(TGNotificationContentView *, bool *))configure;
@end
@interface TGNotificationController () <ASWatcher, TGModernConversationAudioPlayerDelegate>
{
TGNotificationItem *_currentItem;
NSMutableArray *_queue;
bool _ignoringStartupNotifications;
bool _ignoringCompleted;
STimer *_timer;
NSUInteger _ticksToTransition;
TGModernConversationAudioPlayer *_currentAudioPlayer;
int32_t _currentAudioPlayerMessageId;
TGNotificationWindow *_window;
TGNotificationOverlayView *_overlayView;
}
@property (nonatomic, readonly) TGNotificationView *notificationView;
@property (nonatomic, strong) ASHandle *actionHandle;
@end
@implementation TGNotificationController
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_actionHandle = [[ASHandle alloc] initWithDelegate:self];
self.autoManageStatusBarBackground = false;
_window = [[TGNotificationWindow alloc] initWithFrame:TGAppDelegateInstance.rootController.applicationBounds];
[_window.rootViewController addChildViewController:self];
[_window.rootViewController.view addSubview:self.view];
_queue = [[NSMutableArray alloc] init];
[ActionStageInstance() watchForPaths:@
[
@"downloadManagerStateChanged",
@"/as/media/imageThumbnailUpdated"
] watcher:self];
}
return self;
}
- (void)dealloc
{
[_actionHandle reset];
[ActionStageInstance() removeWatcher:self];
}
- (void)loadView
{
[super loadView];
self.view.frame = TGAppDelegateInstance.rootController.applicationBounds;
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
CGFloat side = MAX(self.view.bounds.size.width, self.view.bounds.size.height) * 2;
_overlayView = [[TGNotificationOverlayView alloc] initWithFrame:CGRectMake((self.view.frame.size.width - side) / 2, (self.view.frame.size.height - side) / 2, side, side)];
_overlayView.hidden = true;
[_overlayView addTarget:self action:@selector(overlayPressed) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_overlayView];
__weak TGNotificationController *weakSelf = self;
_notificationView = [[TGNotificationView alloc] initWithFrame:CGRectMake(0, 0, 0, TGNotificationDefaultHeight)];
_notificationView.sendTextMessage = ^(NSString *text)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[TGRecentHashtagsSignal addRecentHashtagsFromText:text space:TGHashtagSpaceEntered];
[[[TGSendMessageSignals sendTextMessageWithPeerId:strongSelf->_currentItem.conversationId text:text replyToMid:strongSelf->_currentItem.replyToMid] then:[TGChatMessageListSignal readChatMessageListWithPeerId:strongSelf->_currentItem.conversationId]] startWithNext:nil];
};
_notificationView.sendSticker = ^(TGDocumentMediaAttachment *sticker)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[[[TGSendMessageSignals sendRemoteDocumentWithPeerId:strongSelf->_currentItem.conversationId replyToMid:strongSelf->_currentItem.replyToMid documentAttachment:sticker] then:[TGChatMessageListSignal readChatMessageListWithPeerId:strongSelf->_currentItem.conversationId]] startWithNext:nil];
};
_notificationView.onTap = ^
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf != nil && strongSelf->_navigateToConversation != nil)
strongSelf->_navigateToConversation(strongSelf->_currentItem.conversationId);
};
_notificationView.onExpand = ^
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_ticksToTransition = TGNotificationExpandedTimeout;
};
_notificationView.onExpandProgress = ^(CGFloat progress)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_overlayView.hidden = (progress < FLT_EPSILON);
strongSelf->_overlayView.alpha = progress;
};
_notificationView.shouldExpandOnTap = ^bool
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return false;
return [strongSelf shouldExpandOnTap];
};
_notificationView.hide = ^(bool animated)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf hideAnimated:animated];
};
_notificationView.parentController = ^TGViewController *
{
__strong TGNotificationController *strongSelf = weakSelf;
return strongSelf;
};
_notificationView.userListSignal = ^SSignal *(NSString *mention)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return nil;
return [strongSelf userListForConversationId:strongSelf->_currentItem.conversationId channelGroup:strongSelf->_currentItem.isChannelGroup mention:mention];
};
_notificationView.hashtagListSignal = ^SSignal *(NSString *hashtag)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return nil;
return [strongSelf hashtagListForHashtag:hashtag];
};
_notificationView.stickersSignal = ^SSignal *(NSString *emoji)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return nil;
return [strongSelf stickersListForEmoji:emoji];
};
_notificationView.requestMedia = ^id (TGMediaAttachment *attachment, int64_t cid, int32_t mid)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return nil;
return [strongSelf _downloadMediaWithAttachment:attachment conversationId:cid messageId:mid];
};
_notificationView.cancelMedia = ^(id mediaId)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf _cancelMediaWithId:mediaId];
};
_notificationView.playMedia = ^(TGMediaAttachment *attachment, int64_t cid, int32_t mid)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf _playAudioWithAttachment:attachment peerId:cid messageId:mid];
};
_notificationView.isMediaAvailable = ^bool(TGMediaAttachment *attachment)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return false;
return [strongSelf _isMediaAvailable:attachment];
};
_notificationView.mediaContext = ^TGModernViewInlineMediaContext *(__unused int64_t cid, int32_t mid)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return nil;
return [strongSelf _inlineMediaContext:mid];
};
[self.view addSubview:_notificationView];
_window.pointInside = ^bool(CGPoint point)
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return false;
bool pointInsideView = CGRectContainsPoint(strongSelf->_notificationView.frame, point);
bool pointInsideOverlay = CGRectContainsPoint(strongSelf->_overlayView.frame, point) && !strongSelf->_overlayView.hidden;
return pointInsideView || pointInsideOverlay;
};
}
- (void)displayNotificationForConversation:(TGConversation *)conversation identifier:(int32_t)identifier replyToMid:(int32_t)replyToMid duration:(NSTimeInterval)duration configure:(void (^)(TGNotificationContentView *view, bool *isRepliable))configure
{
if (!_ignoringCompleted && !_ignoringStartupNotifications)
{
if (CFAbsoluteTimeGetCurrent() - mainLaunchTimestamp < 2.0)
{
_ignoringStartupNotifications = true;
TGDispatchAfter(2.0, dispatch_get_main_queue(), ^
{
_ignoringStartupNotifications = false;
_ignoringCompleted = true;
});
return;
}
else
{
_ignoringCompleted = true;
}
}
if (_ignoringStartupNotifications)
return;
if (_currentItem.identifier == identifier)
return;
for (TGNotificationItem *item in _queue)
{
if (item.identifier == identifier)
return;
}
TGNotificationItem *item = [[TGNotificationItem alloc] initWithConversation:conversation identifier:identifier replyToMid:replyToMid duration:duration configure:configure];
[_queue addObject:item];
if (_notificationView.isPresented)
{
if (!_notificationView.isExpanded)
_ticksToTransition = MIN(_ticksToTransition, TGNotificationInterItemDelay);
}
else
{
_ticksToTransition = (NSInteger)(duration / TGNotificationTimerInterval);
[self showNextItem];
}
}
- (void)dismissNotificationsForConversationId:(int64_t)conversationId
{
NSMutableIndexSet *itemsToRemove = [[NSMutableIndexSet alloc] init];
[_queue enumerateObjectsUsingBlock:^(TGNotificationItem *item, NSUInteger index, __unused BOOL *stop)
{
if (item.conversationId == conversationId)
[itemsToRemove addIndex:index];
}];
[_queue removeObjectsAtIndexes:itemsToRemove];
if (_currentItem.conversationId == conversationId)
[self showNextItem];
}
- (void)dismissAllNotifications
{
[_queue removeAllObjects];
[self showNextItem];
}
#pragma mark -
- (bool)shouldExpandOnTap
{
bool hasModalController = (TGAppDelegateInstance.rootController.presentedViewController != nil);
if (hasModalController)
return true;
bool hasOverlayController = false;
bool hasPlayerController = false;
for (UIWindow *window in [UIApplication sharedApplication].windows)
{
if ([window isKindOfClass:[TGOverlayControllerWindow class]] && window != _window)
{
TGOverlayController *overlayController = (TGOverlayController *)window.rootViewController;
if (overlayController.isImportant)
{
hasOverlayController = true;
break;
}
else
{
for (TGViewController *viewController in overlayController.childViewControllers)
{
if ([viewController isKindOfClass:[TGOverlayController class]])
{
TGOverlayController *overlayController = (TGOverlayController *)viewController;
if (overlayController.isImportant)
{
hasOverlayController = true;
break;
}
}
}
}
}
else if (iosMajorVersion() >= 8 && [window isMemberOfClass:[UIWindow class]])
{
for (UIView *view in window.rootViewController.view.subviews)
{
if ([NSStringFromClass([view class]) hasPrefix:@"AVPlayer"])
{
hasPlayerController = true;
break;
}
}
}
else if (iosMajorVersion() <= 7)
{
for (UIView *view in window.subviews)
{
if ([NSStringFromClass([view class]) hasPrefix:@"MP"])
{
hasPlayerController = true;
break;
}
}
}
}
if (hasOverlayController || hasPlayerController)
return true;
return false;
}
- (bool)shouldDisplayNotificationForConversation:(TGConversation *)conversation
{
bool shouldDisplay = true;
bool hasExistingConversationController = false;
TGModernConversationController *existingConversationController = nil;
TGGenericModernConversationCompanion *existingConversationCompanion = nil;
for (UIViewController *viewController in TGAppDelegateInstance.rootController.viewControllers)
{
if ([viewController isKindOfClass:[TGModernConversationController class]])
{
existingConversationController = (TGModernConversationController *)viewController;
existingConversationCompanion = (TGGenericModernConversationCompanion *)existingConversationController.companion;
NSArray *viewControllers = TGAppDelegateInstance.rootController.viewControllers;
TGViewController *lastController = viewControllers.lastObject;
if ([lastController isKindOfClass:[TGMenuSheetController class]] && viewControllers.count > 2)
lastController = TGAppDelegateInstance.rootController.viewControllers[viewControllers.count - 2];
if (existingConversationCompanion.conversationId == conversation.conversationId && lastController == viewController)
{
hasExistingConversationController = true;
break;
}
}
}
bool hasModalController = (TGAppDelegateInstance.rootController.presentedViewController != nil && TGAppDelegateInstance.rootController.currentSizeClass == UIUserInterfaceSizeClassCompact);
bool hasOverlayController = false;
bool hasPlayerController = false;
for (UIWindow *window in [UIApplication sharedApplication].windows)
{
if ([window isKindOfClass:[TGOverlayControllerWindow class]] && ![window isKindOfClass:[TGSingleStickerPreviewWindow class]] && window.rootViewController != nil && window != _window)
{
if (![window.rootViewController isKindOfClass:[TGPickerSheetOverlayController class]])
{
hasOverlayController = true;
break;
}
}
else if (iosMajorVersion() >= 8 && [window isMemberOfClass:[UIWindow class]])
{
for (UIView *view in window.rootViewController.view.subviews)
{
if ([NSStringFromClass([view class]) hasPrefix:@"AVPlayer"])
{
hasPlayerController = true;
break;
}
}
}
else if (iosMajorVersion() <= 7)
{
for (UIView *view in window.subviews)
{
if ([NSStringFromClass([view class]) hasPrefix:@"MP"])
{
hasPlayerController = true;
break;
}
}
}
}
if (hasExistingConversationController && (!hasModalController && !hasOverlayController && !hasPlayerController))
shouldDisplay = false;
return shouldDisplay;
}
- (void)expandCurrentNotification
{
}
#pragma mark -
- (void)overlayPressed
{
if (!_notificationView.isPresented || !_notificationView.isExpanded || _notificationView.hasUnsavedData || _notificationView.isInteracting)
return;
[self hideAnimated:true];
}
#pragma mark -
- (void)showNextItem
{
if (_queue.count == 0)
{
[self hideAnimated:true];
return;
}
TGNotificationItem *item = _queue.firstObject;
_currentItem = item;
[_queue removeObjectAtIndex:0];
bool isRepliable = false;
bool isPresented = _notificationView.isPresented;
if (!isPresented)
{
item.configure(_notificationView.contentView, &isRepliable);
[self _presentNotificationView];
}
else
{
[_notificationView prepareInterItemTransitionView];
[_notificationView.contentView reset];
item.configure(_notificationView.contentView, &isRepliable);
[_notificationView playInterItemTransition];
}
_notificationView.isRepliable = isRepliable;
_overlayView.isTransparent = !isRepliable;
[_notificationView updateHandleViewAnimated:isPresented];
}
- (void)hideAnimated:(bool)animated
{
[self hideAnimated:animated completion:nil];
}
- (void)hideAnimated:(bool)animated completion:(void (^)(void))completion
{
_currentItem = nil;
_notificationView.isHiding = true;
[_notificationView prepareForHide];
[self _updateStatusBarHiding:false];
void (^changeBlock)(void) = ^
{
_notificationView.frame = CGRectOffset(_notificationView.frame, 0, -_notificationView.frame.size.height);
_overlayView.alpha = 0.0f;
};
void (^finishBlock)(BOOL) = ^(__unused BOOL finished)
{
_notificationView.isPresented = false;
_window.hidden = true;
_overlayView.hidden = true;
[_notificationView reset];
if (_queue.count == 0)
[self _stopTimer];
else
_ticksToTransition = TGNotificationInterItemDelayAfterHide;
if (completion != nil)
completion();
};
if (animated)
{
[UIView animateWithDuration:0.25 delay:0.0 options:(6 << 16 | UIViewAnimationOptionLayoutSubviews) animations:changeBlock completion:finishBlock];
}
else
{
changeBlock();
finishBlock(true);
}
}
- (void)_presentNotificationView
{
_notificationView.isHiding = false;
if (_window.hidden)
_window.hidden = false;
[self _startTimer];
_notificationView.isPresented = true;
_notificationView.frame = CGRectMake(0, -TGNotificationDefaultHeight, self.view.frame.size.width, TGNotificationDefaultHeight);
[UIView animateWithDuration:0.35 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^
{
_notificationView.frame = CGRectMake(0, 0, _notificationView.frame.size.width, _notificationView.frame.size.height);
} completion:^(__unused BOOL finished)
{
[_window makeKeyWindow];
[self _updateStatusBarHiding:true];
}];
}
#pragma mark -
- (void)_startTimer
{
[self _stopTimer];
__weak TGNotificationController *weakSelf = self;
_timer = [[STimer alloc] initWithTimeout:TGNotificationTimerInterval repeat:true completion:^
{
__strong TGNotificationController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
if (strongSelf->_notificationView.isExpanded)
{
if (!strongSelf->_notificationView.isIdle)
{
strongSelf->_ticksToTransition = TGNotificationExpandedTimeout;
return;
}
}
else if (strongSelf->_notificationView.isInteracting)
{
TGNotificationItem *currentItem = strongSelf->_currentItem;
strongSelf->_ticksToTransition = (NSInteger)(currentItem.duration / TGNotificationTimerInterval);
return;
}
strongSelf->_ticksToTransition--;
if (strongSelf->_ticksToTransition > 0)
return;
if (strongSelf->_notificationView.isExpanded)
{
[strongSelf hideAnimated:true];
}
else
{
if (strongSelf->_queue.count == 1)
{
TGNotificationItem *lastItem = strongSelf->_queue.firstObject;
strongSelf->_ticksToTransition = (NSInteger)(lastItem.duration / TGNotificationTimerInterval);
}
else
{
strongSelf->_ticksToTransition = TGNotificationInterItemDelay;
}
[strongSelf showNextItem];
}
} queue:[SQueue mainQueue]];
[_timer start];
}
- (void)_stopTimer
{
[_timer invalidate];
_timer = nil;
}
- (void)localizationUpdated
{
[_notificationView localizationUpdated];
}
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
bool shouldShrink = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone && UIInterfaceOrientationIsLandscape(toInterfaceOrientation));
CGFloat height = _notificationView.isExpanded && !shouldShrink ? [_notificationView expandedHeight] : [_notificationView shrinkedHeight];
_notificationView.frame = CGRectMake(0, _notificationView.frame.origin.y, self.view.frame.size.width, height);
[_notificationView setShrinked:shouldShrink];
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
_overlayView.center = self.view.center;
_notificationView.frame = CGRectMake(0, _notificationView.frame.origin.y, self.view.frame.size.width, _notificationView.frame.size.height);
}
- (UIWindow *)window
{
return _window;
}
- (void)_updateStatusBarHiding:(bool)__unused hiding
{
if (iosMajorVersion() >= 7)
[[UIApplication sharedApplication].keyWindow.rootViewController setNeedsStatusBarAppearanceUpdate];
}
#pragma mark -
- (SSignal *)userListForConversationId:(int64_t)conversationId channelGroup:(bool)channelGroup mention:(NSString *)mention
{
if (!TGPeerIdIsGroup(conversationId) && !channelGroup)
return [SSignal single:@[]];
NSString *normalizedMention = [mention lowercaseString];
SSignal *participantsSignal = channelGroup ? [[TGDatabaseInstance() channelCachedData:conversationId] map:^id(TGCachedConversationData *data)
{
NSMutableArray *chatParticipantsUids = [[NSMutableArray alloc] init];
for (TGCachedConversationMember *member in data.generalMembers)
[chatParticipantsUids addObject:@(member.uid)];
return chatParticipantsUids;
}] : [[[TGConversationSignals conversationWithPeerId:conversationId] filter:^bool(TGConversation *conversation)
{
return (conversation.chatParticipants != nil);
}] map:^NSArray *(TGConversation *conversation)
{
return conversation.chatParticipants.chatParticipantUids;
}];
return [[[participantsSignal take:1] mapToSignal:^SSignal *(NSArray *chatParticipantUids)
{
NSMutableDictionary *userDict = [[NSMutableDictionary alloc] init];
for (NSNumber *nUid in chatParticipantUids)
{
TGUser *user = [TGDatabaseInstance() loadUser:[nUid intValue]];
if (user != nil && user.uid != TGTelegraphInstance.clientUserId && user.userName.length != 0 && (normalizedMention.length == 0 || [[user.userName lowercaseString] hasPrefix:normalizedMention]))
{
userDict[@(user.uid)] = user;
}
}
return [[[TGChatMessageListSignal chatMessageListViewWithPeerId:conversationId atMessageId:0 rangeMessageCount:16] take:1] map:^NSArray *(TGChatMessageListView *messageListView)
{
NSMutableArray *sortedUserList = [[NSMutableArray alloc] init];
for (TGMessage *message in messageListView.messages)
{
int32_t uid = (int32_t)message.fromUid;
TGUser *user = userDict[@(uid)];
if (user != nil)
{
[sortedUserList addObject:user];
[userDict removeObjectForKey:@(uid)];
if (userDict.count == 0)
break;
}
}
NSArray *sortedRemainingUsers = [[userDict allValues] sortedArrayUsingComparator:^NSComparisonResult(TGUser *user1, TGUser *user2)
{
return [user1.displayName compare:user2.displayName];
}];
[sortedUserList addObjectsFromArray:sortedRemainingUsers];
return sortedUserList;
}];
}] deliverOn:[SQueue mainQueue]];
}
- (SSignal *)hashtagListForHashtag:(NSString *)hashtag
{
return [[TGRecentHashtagsSignal recentHashtagsFromSpaces:TGHashtagSpaceEntered | TGHashtagSpaceSearchedBy] map:^id (NSArray *recentHashtags)
{
if (hashtag.length == 0)
return recentHashtags;
NSMutableArray *filteredHashtags = [[NSMutableArray alloc] init];
for (NSString *listHashtag in recentHashtags)
{
if ([listHashtag hasPrefix:hashtag])
[filteredHashtags addObject:listHashtag];
}
return filteredHashtags;
}];
}
- (SSignal *)stickersListForEmoji:(NSString *)emoji
{
return [[[[[TGStickersSignals stickerPacks] filter:^bool(NSDictionary *dict)
{
return ((NSArray *)dict[@"packs"]).count != 0;
}] take:1] mapToSignal:^SSignal *(NSDictionary *dict)
{
NSMutableArray *matchedDocuments = [[NSMutableArray alloc] init];
NSArray *sortedStickerPacks = dict[@"packs"];
for (TGStickerPack *stickerPack in sortedStickerPacks)
{
NSMutableArray *documentIds = [[NSMutableArray alloc] init];
for (TGStickerAssociation *association in stickerPack.stickerAssociations)
{
if ([association.key isEqual:emoji])
[documentIds addObjectsFromArray:association.documentIds];
}
for (NSNumber *nDocumentId in documentIds)
{
for (TGDocumentMediaAttachment *document in stickerPack.documents)
{
if (document.documentId == [nDocumentId longLongValue])
{
[matchedDocuments addObject:document];
break;
}
}
}
}
return [TGStickersSignals preloadedStickerPreviews:matchedDocuments count:6];
}] deliverOn:[SQueue mainQueue]];
}
#pragma mark - Media
- (id)_downloadMediaWithAttachment:(TGMediaAttachment *)attachment conversationId:(int64_t)conversationId messageId:(int32_t)messageId
{
switch (attachment.type)
{
case TGImageMediaAttachmentType:
{
TGImageMediaAttachment *imageAttachment = (TGImageMediaAttachment *)attachment;
id mediaId = [[TGMediaId alloc] initWithType:2 itemId:imageAttachment.imageId];
NSString *url = [[imageAttachment imageInfo] closestImageUrlWithSize:CGSizeMake(1136, 1136) resultingSize:NULL pickLargest:true];
if (url != nil)
{
NSInteger contentHints = TGRemoteImageContentHintLargeFile;
NSDictionary *options = @
{
@"cancelTimeout": @0,
@"cache": [TGRemoteImageView sharedCache],
@"useCache": @false,
@"allowThumbnailCache": @false,
@"contentHints": @(contentHints),
@"userProperties": @
{
@"messageId": @(messageId),
@"conversationId": @(conversationId),
@"forceSave": @(true),
@"mediaId": mediaId,
@"imageInfo": imageAttachment.imageInfo
}
};
[[TGDownloadManager instance] requestItem:[NSString stringWithFormat:@"/img/(download:{filter:%@}%@)", @"maybeScale", url] options:options changePriority:true messageId:messageId itemId:mediaId groupId:conversationId itemClass:TGDownloadItemClassImage];
return mediaId;
}
}
break;
case TGAudioMediaAttachmentType:
{
TGAudioMediaAttachment *audioAttachment = (TGAudioMediaAttachment *)attachment;
if (audioAttachment.audioId != 0 || audioAttachment.audioUri.length != 0)
{
id mediaId = [[TGMediaId alloc] initWithType:4 itemId:audioAttachment.audioId != 0 ? audioAttachment.audioId : audioAttachment.localAudioId];
NSDictionary *options = @{ @"audioAttachment": audioAttachment };
[[TGDownloadManager instance] requestItem:[NSString stringWithFormat:@"/tg/media/audio/(%" PRId32 ":%" PRId64 ":%@)", audioAttachment.datacenterId, audioAttachment.audioId, audioAttachment.audioUri.length != 0 ? audioAttachment.audioUri : @""] options:options changePriority:true messageId:messageId itemId:mediaId groupId:conversationId itemClass:TGDownloadItemClassAudio];
return mediaId;
}
break;
}
case TGDocumentMediaAttachmentType:
{
TGDocumentMediaAttachment *documentAttachment = (TGDocumentMediaAttachment *)attachment;
if (documentAttachment.documentId != 0 || documentAttachment.documentUri.length != 0) {
id mediaId = nil;
if (documentAttachment.documentId != 0) {
mediaId = [[TGMediaId alloc] initWithType:3 itemId:documentAttachment.documentId];
} else if (documentAttachment.localDocumentId != 0 && documentAttachment.documentUri.length != 0) {
mediaId = [[TGMediaId alloc] initWithType:3 itemId:documentAttachment.localDocumentId];
}
if (mediaId != nil) {
NSString *downloadUri = documentAttachment.documentUri;
[[TGDownloadManager instance] requestItem:[NSString stringWithFormat:@"/tg/media/document/(%d:%" PRId64 ":%@)", documentAttachment.datacenterId, documentAttachment.documentId, downloadUri.length != 0 ? downloadUri : @""] options:[[NSDictionary alloc] initWithObjectsAndKeys:documentAttachment, @"documentAttachment", nil] changePriority:true messageId:messageId itemId:mediaId groupId:conversationId itemClass:TGDownloadItemClassDocument];
return mediaId;
}
}
break;
}
default:
break;
}
return nil;
}
- (void)_cancelMediaWithId:(id)mediaId
{
[[TGDownloadManager instance] cancelItem:mediaId];
}
- (TGModernViewInlineMediaContext *)_inlineMediaContext:(int32_t)messageId
{
if (_currentAudioPlayerMessageId == messageId && _currentAudioPlayer != nil)
return [_currentAudioPlayer inlineMediaContext];
return nil;
}
- (void)_updateInlineMediaContext
{
[_notificationView.contentView.previewView updateInlineMediaContext];
}
- (void)_playAudioWithAttachment:(TGMediaAttachment *)attachment peerId:(int64_t)peerId messageId:(int32_t)messageId
{
bool isVoice = false;
if ([attachment isKindOfClass:[TGAudioMediaAttachment class]]) {
isVoice = true;
} else if ([attachment isKindOfClass:[TGDocumentMediaAttachment class]]) {
for (id attribute in ((TGDocumentMediaAttachment *)attachment).attributes) {
if ([attribute isKindOfClass:[TGDocumentAttributeAudio class]]) {
isVoice = ((TGDocumentAttributeAudio *)attribute).isVoice;
}
}
}
[TGTelegraphInstance.musicPlayer setPlaylist:[TGGenericPeerPlaylistSignals playlistForPeerId:peerId important:true atMessageId:messageId voice:isVoice] initialItemKey:@(messageId) metadata:@{@"peerId": @(peerId), @"voice": @(isVoice)}];
[self hideAnimated:true];
}
static id mediaIdForAttachment(TGMediaAttachment *attachment)
{
if (attachment.type == TGVideoMediaAttachmentType)
{
if (((TGVideoMediaAttachment *)attachment).videoId == 0)
return nil;
return [[TGMediaId alloc] initWithType:1 itemId:((TGVideoMediaAttachment *)attachment).videoId];
}
else if (attachment.type == TGImageMediaAttachmentType)
{
if (((TGImageMediaAttachment *)attachment).imageId == 0)
return nil;
return [[TGMediaId alloc] initWithType:2 itemId:((TGImageMediaAttachment *)attachment).imageId];
}
else if (attachment.type == TGDocumentMediaAttachmentType)
{
if (((TGDocumentMediaAttachment *)attachment).documentId != 0)
return [[TGMediaId alloc] initWithType:3 itemId:((TGDocumentMediaAttachment *)attachment).documentId];
else if (((TGDocumentMediaAttachment *)attachment).localDocumentId != 0 && ((TGDocumentMediaAttachment *)attachment).documentUri.length != 0)
return [[TGMediaId alloc] initWithType:3 itemId:((TGDocumentMediaAttachment *)attachment).localDocumentId];
return nil;
}
else if (attachment.type == TGAudioMediaAttachmentType)
{
if (((TGAudioMediaAttachment *)attachment).audioId != 0)
return [[TGMediaId alloc] initWithType:4 itemId:((TGAudioMediaAttachment *)attachment).audioId];
else if (((TGAudioMediaAttachment *)attachment).localAudioId != 0)
return [[TGMediaId alloc] initWithType:4 itemId:((TGAudioMediaAttachment *)attachment).localAudioId];
return nil;
}
return nil;
}
- (void)_updateMediaAccessTimeWithAttachment:(TGMediaAttachment *)attachment peerId:(int64_t)peerId messageId:(int32_t)messageId
{
TGMediaId *mediaId = mediaIdForAttachment(attachment);
if (mediaId != 0)
[TGDatabaseInstance() updateLastUseDateForMediaType:mediaId.type mediaId:mediaId.itemId messageId:messageId];
bool maybeReadContents = ([attachment isKindOfClass:[TGAudioMediaAttachment class]] || [attachment isKindOfClass:[TGDocumentMediaAttachment class]]);
if (maybeReadContents)// && [self allowMessageForwarding] && !TGPeerIdIsChannel(_conversationId))
{
TGMessage *message = [TGDatabaseInstance() loadMessageWithMid:messageId peerId:peerId];
if (message == nil)
return;
bool found = (message.contentProperties[@"contentsRead"] != nil);
if (!found)
{
NSMutableDictionary *contentProperties = [[NSMutableDictionary alloc] initWithDictionary:message.contentProperties];
contentProperties[@"contentsRead"] = [[TGMessageViewedContentProperty alloc] init];
TGMessage *updatedMessage = [message copy];
updatedMessage.contentProperties = contentProperties;
TGDatabaseAction action = { .type = TGDatabaseActionReadMessageContents, .subject = message.mid, .arg0 = 0, .arg1 = 0};
[TGDatabaseInstance() storeQueuedActions:[NSArray arrayWithObject:[[NSValue alloc] initWithBytes:&action objCType:@encode(TGDatabaseAction)]]];
[ActionStageInstance() requestActor:@"/tg/service/synchronizeactionqueue/(global)" options:nil watcher:TGTelegraphInstance];
[TGDatabaseInstance() updateMessage:message.mid peerId:peerId withMessage:updatedMessage];
}
}
}
- (void)_stopInlineMediaIfPlaying
{
if (_currentAudioPlayer != nil)
{
[_currentAudioPlayer stop];
_currentAudioPlayer = nil;
_currentAudioPlayerMessageId = 0;
[self _updateInlineMediaContext];
}
}
- (void)audioPlayerDidFinish
{
[self _stopInlineMediaIfPlaying];
}
- (bool)_isMediaAvailable:(TGMediaAttachment *)attachment
{
static NSFileManager *fileManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
fileManager = [[NSFileManager alloc] init];
});
switch (attachment.type)
{
case TGImageMediaAttachmentType:
{
static TGCache *cache = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
cache = [TGRemoteImageView sharedCache];
});
TGImageMediaAttachment *imageAttachment = (TGImageMediaAttachment *)attachment;
NSString *url = [imageAttachment.imageInfo closestImageUrlWithSize:(CGSizeMake(1136, 1136)) resultingSize:NULL pickLargest:true];
bool imageDownloaded = false;
if ([url hasPrefix:@"http://"] || [url hasPrefix:@"https://"])
{
imageDownloaded = [[[TGMediaStoreContext instance] temporaryFilesCache] containsValueForKey:[url dataUsingEncoding:NSUTF8StringEncoding]];
}
else
{
if (imageAttachment.imageId != 0)
{
NSString *path = [TGPreparedRemoteImageMessage filePathForRemoteImageId:imageAttachment.imageId];
imageDownloaded = [fileManager fileExistsAtPath:path];
}
if (!imageDownloaded)
{
NSString *path = [cache pathForCachedData:url];
if (path != nil)
{
imageDownloaded = ([url hasPrefix:@"upload/"] || [url hasPrefix:@"file://"]) ? true : [fileManager fileExistsAtPath:path];
}
}
}
return imageDownloaded;
}
case TGDocumentMediaAttachmentType:
{
TGDocumentMediaAttachment *documentAttachment = (TGDocumentMediaAttachment *)attachment;
bool documentDownloaded = false;
if (documentAttachment.localDocumentId != 0)
{
NSString *documentPath = [[TGPreparedLocalDocumentMessage localDocumentDirectoryForLocalDocumentId:documentAttachment.localDocumentId] stringByAppendingPathComponent:[documentAttachment safeFileName]];
documentDownloaded = [[NSFileManager defaultManager] fileExistsAtPath:documentPath];
}
else
{
NSString *documentPath = [[TGPreparedLocalDocumentMessage localDocumentDirectoryForDocumentId:documentAttachment.documentId] stringByAppendingPathComponent:[documentAttachment safeFileName]];
documentDownloaded = [[NSFileManager defaultManager] fileExistsAtPath:documentPath];
}
return documentDownloaded;
}
case TGAudioMediaAttachmentType:
{
TGAudioMediaAttachment *audioAttachment = (TGAudioMediaAttachment *)attachment;
bool audioDownloaded = false;
if (audioAttachment.localAudioId != 0)
{
NSString *audioPath = [TGAudioMediaAttachment localAudioFilePathForLocalAudioId:audioAttachment.localAudioId];
audioDownloaded = [[NSFileManager defaultManager] fileExistsAtPath:audioPath];
}
else
{
NSString *audioPath = [TGAudioMediaAttachment localAudioFilePathForRemoteAudioId:audioAttachment.audioId];
audioDownloaded = [[NSFileManager defaultManager] fileExistsAtPath:audioPath];
}
return audioDownloaded;
}
default:
break;
}
return false;
}
- (void)actionStageResourceDispatched:(NSString *)path resource:(id)resource arguments:(id)__unused arguments
{
if ([path isEqualToString:@"/as/media/imageThumbnailUpdated"])
{
NSString *imageUrl = resource;
TGDispatchOnMainThread(^
{
[_notificationView.contentView.previewView imageDataInvalidated:imageUrl];
});
}
else if ([path isEqualToString:@"downloadManagerStateChanged"])
{
bool animated = ![arguments[@"requested"] boolValue];
NSDictionary *mediaList = resource;
NSMutableDictionary *messageDownloadProgress = [[NSMutableDictionary alloc] init];
if (mediaList == nil || mediaList.count == 0)
{
[messageDownloadProgress removeAllObjects];
}
else
{
[mediaList enumerateKeysAndObjectsUsingBlock:^(__unused NSString *path, TGDownloadItem *item, __unused BOOL *stop)
{
if (item.itemId != nil)
[messageDownloadProgress setObject:@(item.progress) forKey:item.itemId];
}];
}
TGDispatchOnMainThread(^
{
id activeMediaId = _notificationView.contentView.previewView.activeRequestMediaId;
if (activeMediaId == nil)
return;
NSNumber *nProgress = messageDownloadProgress[activeMediaId];
if (nProgress != nil)
{
float progress = nProgress.floatValue;
[_notificationView.contentView.previewView updateProgress:(progress > -FLT_EPSILON) progress:progress animated:animated];
}
if (arguments != nil)
{
NSMutableDictionary *completedItemStatuses = [[NSMutableDictionary alloc] init];
for (id mediaId in [arguments objectForKey:@"completedItemIds"])
[completedItemStatuses setObject:@(true) forKey:mediaId];
for (id mediaId in [arguments objectForKey:@"failedItemIds"])
[completedItemStatuses setObject:@(false) forKey:mediaId];
NSNumber *nResult = completedItemStatuses[activeMediaId];
if (nResult != nil)
{
bool availability = nResult.boolValue;
[_notificationView.contentView.previewView updateMediaAvailability:availability];
}
}
});
}
}
@end
@implementation TGNotificationWindowViewController
- (BOOL)prefersStatusBarHidden
{
TGNotificationController *controller = self.childViewControllers.firstObject;
bool viewPresented = controller.notificationView.isPresented;
bool isHiding = controller.notificationView.isHiding;
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
return (iosMajorVersion() >=7 && viewPresented && !isHiding && (statusBarHeight - 20.0f) < FLT_EPSILON);
}
- (void)viewDidLayoutSubviews
{
self.view.frame = TGAppDelegateInstance.rootController.applicationBounds;
}
@end
@implementation TGNotificationWindow
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self != nil)
{
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.backgroundColor = [UIColor clearColor];
self.windowLevel = UIWindowLevelStatusBar + 0.1f;
self.rootViewController = [[TGNotificationWindowViewController alloc] init];
self.rootViewController.view.userInteractionEnabled = true;
if (iosMajorVersion() < 7)
self.rootViewController.wantsFullScreenLayout = true;
}
return self;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint localPoint = [[((TGOverlayWindowViewController *)self.rootViewController).childViewControllers.firstObject view] convertPoint:point fromView:self];
UIView *result = [[((TGOverlayWindowViewController *)self.rootViewController).childViewControllers.firstObject view] hitTest:localPoint withEvent:event];
if (result == [((TGOverlayWindowViewController *)self.rootViewController).childViewControllers.firstObject view] || result == self.rootViewController.view)
return nil;
return result;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.pointInside != nil)
return self.pointInside(point);
return [super pointInside:point withEvent:event];
}
@end
@implementation TGNotificationItem
- (instancetype)initWithConversation:(TGConversation *)conversation identifier:(int32_t)identifier replyToMid:(int32_t)replyToMid duration:(NSTimeInterval)duration configure:(void (^)(TGNotificationContentView *, bool *))configure
{
self = [super init];
if (self != nil)
{
_conversationId = conversation.conversationId;
_identifier = identifier;
_replyToMid = replyToMid;
_isChannelGroup = conversation.isChannelGroup;
_duration = duration;
self.configure = configure;
}
return self;
}
@end