Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ShareItems",
module_name = "ShareItems",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SSignalKit",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/MtProtoKit:MtProtoKit",
"//submodules/Display:Display",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/LocalMediaResources:LocalMediaResources",
"//submodules/Pdf:Pdf",
"//submodules/AccountContext:AccountContext",
"//submodules/ShareItems/Impl:ShareItemsImpl",
],
visibility = [
"//visibility:public",
],
)
+34
View File
@@ -0,0 +1,34 @@
objc_library(
name = "ShareItemsImpl",
enable_modules = True,
module_name = "ShareItemsImpl",
srcs = glob([
"Sources/**/*.m",
"Sources/**/*.h",
], allow_empty=True),
hdrs = glob([
"PublicHeaders/**/*.h",
]),
includes = [
"PublicHeaders",
],
deps = [
"//submodules/MtProtoKit:MtProtoKit",
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/MimeTypes:MimeTypes",
],
sdk_frameworks = [
"Foundation",
"UIKit",
"MobileCoreServices",
"AddressBook",
"AVFoundation",
],
weak_sdk_frameworks = [
"PassKit",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,6 @@
#import <UIKit/UIKit.h>
#import <ShareItemsImpl/TGItemProviderSignals.h>
#import <ShareItemsImpl/TGShareLocationSignals.h>
@@ -0,0 +1,10 @@
#import <Foundation/Foundation.h>
@class MTSignal;
@interface TGItemProviderSignals : NSObject
+ (NSArray<MTSignal *> *)itemSignalsForInputItems:(NSArray *)inputItems;
+ (NSData *)audioWaveform:(NSURL *)url;
@end
@@ -0,0 +1,24 @@
#import <Foundation/Foundation.h>
@class MTSignal;
@interface TGShareLocationResult : NSObject
@property (nonatomic, readonly) double latitude;
@property (nonatomic, readonly) double longitude;
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) NSString *address;
@property (nonatomic, readonly) NSString *provider;
@property (nonatomic, readonly) NSString *venueId;
@property (nonatomic, readonly) NSString *venueType;
- (instancetype)initWithLatitude:(double)latitude longitude:(double)longitude title:(NSString *)title address:(NSString *)address provider:(NSString *)provider venueId:(NSString *)venueId venueType:(NSString *)venueType;
@end
@interface TGShareLocationSignals : NSObject
+ (MTSignal *)locationMessageContentForURL:(NSURL *)url;
+ (bool)isLocationURL:(NSURL *)url;
@end
@@ -0,0 +1,708 @@
#import <ShareItemsImpl/TGItemProviderSignals.h>
#import <MtProtoKit/MtProtoKit.h>
#import <UIKit/UIKit.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <AddressBook/AddressBook.h>
#import <AVFoundation/AVFoundation.h>
#import <PassKit/PassKit.h>
#import <MimeTypes/MimeTypes.h>
@implementation TGItemProviderSignals
+ (NSArray<MTSignal *> *)itemSignalsForInputItems:(NSArray *)inputItems
{
NSMutableArray *itemSignals = [[NSMutableArray alloc] init];
NSMutableArray *providers = [[NSMutableArray alloc] init];
for (NSExtensionItem *item in inputItems)
{
for (NSItemProvider *provider in item.attachments)
{
if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeAudio])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeFileURL])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL]) {
[providers removeAllObjects];
[providers addObject:provider];
break;
}
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeVCard])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeText])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeData])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:@"com.apple.pkpass"])
[providers addObject:provider];
}
}
__unused NSInteger providerIndex = -1;
for (NSItemProvider *provider in providers)
{
providerIndex++;
MTSignal *dataSignal = nil;
if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeAudio])
dataSignal = [self signalForAudioItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie])
dataSignal = [self signalForVideoItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeGIF])
dataSignal = [self signalForDataItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage])
dataSignal = [self signalForImageItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeFileURL])
{
dataSignal = [[self signalForUrlItemProvider:provider] mapToSignal:^MTSignal *(NSURL *url)
{
NSData *data = [[NSData alloc] initWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:nil];
if (data == nil)
return [MTSignal fail:nil];
NSString *fileName = [[url pathComponents] lastObject];
if (fileName.length == 0)
fileName = @"file.bin";
NSString *extension = [fileName pathExtension];
NSString *mimeType = [TGMimeTypeMap mimeTypeForExtension:[extension lowercaseString]];
if (mimeType == nil)
mimeType = @"application/octet-stream";
return [MTSignal single:@{@"data": data, @"fileName": fileName, @"mimeType": mimeType}];
}];
}
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeVCard])
dataSignal = [self signalForVCardItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeText])
dataSignal = [self signalForTextItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL])
dataSignal = [self signalForTextUrlItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeData])
{
dataSignal = [[self signalForDataItemProvider:provider] map:^id(NSDictionary *dict)
{
if (dict[@"fileName"] == nil)
{
NSMutableDictionary *updatedDict = [[NSMutableDictionary alloc] initWithDictionary:dict];
for (NSString *typeIdentifier in provider.registeredTypeIdentifiers)
{
NSString *extension = [TGMimeTypeMap extensionForMimeType:typeIdentifier];
if (extension == nil)
extension = [TGMimeTypeMap extensionForMimeType:[@"application/" stringByAppendingString:typeIdentifier]];
if (extension != nil) {
updatedDict[@"fileName"] = [@"file" stringByAppendingPathExtension:extension];
updatedDict[@"mimeType"] = [TGMimeTypeMap mimeTypeForExtension:extension];
}
}
return updatedDict;
}
else
{
return dict;
}
}];
}
else if ([provider hasItemConformingToTypeIdentifier:@"com.apple.pkpass"])
{
dataSignal = [self signalForPassKitItemProvider:provider];
}
if (dataSignal != nil)
[itemSignals addObject:dataSignal];
}
return itemSignals;
}
+ (MTSignal *)signalForDataItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeData options:nil completionHandler:^(NSData *data, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"data": data}];
[subscriber putCompletion];
}
}];
return nil;
}];
}
__unused static UIImage *TGScaleImageToPixelSize(UIImage *image, CGSize size) {
UIGraphicsBeginImageContextWithOptions(size, true, 1.0f);
[image drawInRect:CGRectMake(0, 0, size.width, size.height) blendMode:kCGBlendModeCopy alpha:1.0f];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
__unused static CGSize TGFitSize(CGSize size, CGSize maxSize) {
if (size.width < 1)
size.width = 1;
if (size.height < 1)
size.height = 1;
if (size.width > maxSize.width)
{
size.height = floor((size.height * maxSize.width / size.width));
size.width = maxSize.width;
}
if (size.height > maxSize.height)
{
size.width = floor((size.width * maxSize.height / size.height));
size.height = maxSize.height;
}
return size;
}
+ (MTSignal *)signalForImageItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
bool preferAsFile = false;
CGSize maxSize = CGSizeMake(1280.0, 1280.0);
NSDictionary *imageOptions = @{
NSItemProviderPreferredImageSizeKey: [NSValue valueWithCGSize:maxSize]
};
if (preferAsFile) {
imageOptions = nil;
}
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) {
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:imageOptions completionHandler:^(id<NSSecureCoding> _Nullable item, NSError * _Null_unspecified error) {
if (error != nil && ![(NSObject *)item respondsToSelector:@selector(CGImage)] && ![(NSObject *)item respondsToSelector:@selector(absoluteString)]) {
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeData options:nil completionHandler:^(UIImage *image, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"image": image}];
[subscriber putCompletion];
}
}];
} else {
if ([(NSObject *)item respondsToSelector:@selector(absoluteString)]) {
NSURL *url = (NSURL *)item;
if (preferAsFile) {
NSData *data = [[NSData alloc] initWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:nil];
if (data == nil) {
[subscriber putError:nil];
return;
}
NSString *fileName = [[url pathComponents] lastObject];
if (fileName.length == 0) {
fileName = @"file.bin";
}
NSString *extension = [fileName pathExtension];
NSString *mimeType = [TGMimeTypeMap mimeTypeForExtension:[extension lowercaseString]];
if (mimeType == nil) {
mimeType = @"application/octet-stream";
}
[subscriber putNext:@{@"data": data, @"fileName": fileName, @"mimeType": mimeType, @"treatAsFile": @true}];
[subscriber putCompletion];
} else {
CGImageSourceRef src = CGImageSourceCreateWithURL((__bridge CFURLRef) url, NULL);
CFDictionaryRef options = (__bridge CFDictionaryRef) @{
(id) kCGImageSourceCreateThumbnailWithTransform : @YES,
(id) kCGImageSourceCreateThumbnailFromImageAlways : @YES,
(id) kCGImageSourceThumbnailMaxPixelSize : @(maxSize.width)
};
CGImageRef image = CGImageSourceCreateThumbnailAtIndex(src, 0, options);
CFRelease(src);
if (image == nil) {
[subscriber putError:nil];
return;
}
NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"img%d", (int)arc4random()]];
CFURLRef tempUrl = (__bridge CFURLRef)[NSURL fileURLWithPath:tempPath];
CGImageDestinationRef destination = CGImageDestinationCreateWithURL(tempUrl, kUTTypeJPEG, 1, NULL);
NSDictionary *properties = @{ (__bridge NSString *)kCGImageDestinationLossyCompressionQuality: @(0.52)};
CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)properties);
CGImageDestinationAddImage(destination, image, nil);
if (!CGImageDestinationFinalize(destination)) {
CFRelease(destination);
[subscriber putError:nil];
return;
}
CFRelease(destination);
NSData *resultData = [[NSData alloc] initWithContentsOfFile:tempPath options:NSDataReadingMappedIfSafe error:nil];
if (resultData != nil) {
[subscriber putNext:@{@"scaledImageData": resultData, @"scaledImageDimensions": [NSValue valueWithCGSize:CGSizeMake(CGImageGetWidth(image), CGImageGetHeight(image))]}];
[subscriber putCompletion];
} else {
[subscriber putError:nil];
}
}
} else {
[subscriber putNext:@{@"image": item}];
[subscriber putCompletion];
}
}
}];
} else {
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeData options:nil completionHandler:^(UIImage *image, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"image": image}];
[subscriber putCompletion];
}
}];
}
return nil;
}];
}
+ (MTSignal *)signalForAudioItemProvider:(NSItemProvider *)itemProvider
{
MTSignal *itemSignal = [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeAudio options:nil completionHandler:^(NSURL *url, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:url];
[subscriber putCompletion];
}
}];
return nil;
}];
return [itemSignal map:^id(NSURL *url)
{
AVAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
if (asset == nil)
return [MTSignal fail:nil];
NSString *extension = url.pathExtension;
NSString *mimeType = [TGMimeTypeMap mimeTypeForExtension:[extension lowercaseString]];
if (mimeType == nil)
mimeType = @"application/octet-stream";
NSString *title = (NSString *)[[AVMetadataItem metadataItemsFromArray:asset.commonMetadata withKey:AVMetadataCommonKeyTitle keySpace:AVMetadataKeySpaceCommon] firstObject];
NSString *artist = (NSString *)[[AVMetadataItem metadataItemsFromArray:asset.commonMetadata withKey:AVMetadataCommonKeyArtist keySpace:AVMetadataKeySpaceCommon] firstObject];
NSString *software = nil;
AVMetadataItem *softwareItem = [[AVMetadataItem metadataItemsFromArray:asset.commonMetadata withKey:AVMetadataCommonKeySoftware keySpace:AVMetadataKeySpaceCommon] firstObject];
if ([softwareItem isKindOfClass:[AVMetadataItem class]] && ([softwareItem.value isKindOfClass:[NSString class]]))
software = (NSString *)[softwareItem value];
bool isVoice = [software hasPrefix:@"com.apple.VoiceMemos"];
NSTimeInterval duration = CMTimeGetSeconds(asset.duration);
NSMutableDictionary *result = [[NSMutableDictionary alloc] init];
result[@"audio"] = url;
result[@"mimeType"] = mimeType;
result[@"duration"] = @(duration);
result[@"isVoice"] = @(isVoice);
NSString *artistString = @"";
if ([artist respondsToSelector:@selector(characterAtIndex:)]) {
artistString = artist;
} else if ([artist isKindOfClass:[AVMetadataItem class]]) {
artistString = [(AVMetadataItem *)artist stringValue];
}
NSString *titleString = @"";
if ([artist respondsToSelector:@selector(characterAtIndex:)]) {
titleString = title;
} else if ([title isKindOfClass:[AVMetadataItem class]]) {
titleString = [(AVMetadataItem *)title stringValue];
}
if (artistString.length > 0)
result[@"artist"] = artistString;
if (titleString.length > 0)
result[@"title"] = titleString;
return result;
}];
}
+ (MTSignal *)detectRoundVideo:(AVAsset *)asset
{
MTSignal *imageSignal = [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subsriber)
{
AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:asset];
imageGenerator.appliesPreferredTrackTransform = true;
[imageGenerator generateCGImagesAsynchronouslyForTimes:@[ [NSValue valueWithCMTime:kCMTimeZero] ] completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error)
{
if (error != nil)
{
[subsriber putError:nil];
}
else
{
[subsriber putNext:[UIImage imageWithCGImage:image]];
[subsriber putCompletion];
}
}];
return [[MTBlockDisposable alloc] initWithBlock:^
{
[imageGenerator cancelAllCGImageGeneration];
}];
}];
return [imageSignal map:^NSNumber *(UIImage *image)
{
CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
const UInt8 *data = CFDataGetBytePtr(pixelData);
bool (^isWhitePixel)(NSInteger, NSInteger) = ^bool(NSInteger x, NSInteger y)
{
int pixelInfo = ((image.size.width * y) + x ) * 4;
UInt8 red = data[pixelInfo];
UInt8 green = data[(pixelInfo + 1)];
UInt8 blue = data[pixelInfo + 2];
return (red > 250 && green > 250 && blue > 250);
};
CFRelease(pixelData);
return @(isWhitePixel(0, 0) && isWhitePixel(image.size.width - 1, 0) && isWhitePixel(0, image.size.height - 1) && isWhitePixel(image.size.width - 1, image.size.height - 1));
}];
}
+ (MTSignal *)signalForVideoItemProvider:(NSItemProvider *)itemProvider
{
MTSignal *assetSignal = [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeMovie options:nil completionHandler:^(NSURL *url, NSError *error)
{
if (error != nil)
{
[subscriber putError:nil];
}
else
{
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];
[subscriber putNext:asset];
[subscriber putCompletion];
}
}];
return nil;
}];
return [assetSignal mapToSignal:^MTSignal *(AVURLAsset *asset)
{
AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
if (videoTrack == nil)
{
return [MTSignal fail:nil];
}
else
{
CGSize dimensions = CGRectApplyAffineTransform((CGRect){CGPointZero, videoTrack.naturalSize}, videoTrack.preferredTransform).size;
NSString *extension = asset.URL.pathExtension;
NSString *mimeType = [TGMimeTypeMap mimeTypeForExtension:[extension lowercaseString]];
if (mimeType == nil)
mimeType = @"application/octet-stream";
NSString *software = nil;
AVMetadataItem *softwareItem = [[AVMetadataItem metadataItemsFromArray:asset.metadata withKey:AVMetadataCommonKeySoftware keySpace:AVMetadataKeySpaceCommon] firstObject];
if ([softwareItem isKindOfClass:[AVMetadataItem class]] && ([softwareItem.value isKindOfClass:[NSString class]]))
software = (NSString *)[softwareItem value];
bool isAnimation = false;
if ([software hasPrefix:@"Boomerang"])
isAnimation = true;
if (isAnimation || fabs(dimensions.width - dimensions.height) > FLT_EPSILON)
{
return [MTSignal single:@{@"video": asset, @"mimeType": mimeType, @"isAnimation": @(isAnimation), @"width": @(dimensions.width), @"height": @(dimensions.height)}];
}
else
{
return [[self detectRoundVideo:asset] mapToSignal:^MTSignal *(NSNumber *isRoundVideo)
{
return [MTSignal single:@{@"video": asset, @"mimeType": mimeType, @"isAnimation": @(isAnimation), @"width": @(dimensions.width), @"height": @(dimensions.height), @"isRoundMessage": isRoundVideo}];
}];
}
}
}];
}
+ (MTSignal *)signalForUrlItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeFileURL options:nil completionHandler:^(NSURL *url, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:url];
[subscriber putCompletion];
}
}];
return nil;
}];
}
+ (MTSignal *)signalForTextItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeText options:nil completionHandler:^(NSString *text, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"text": text}];
[subscriber putCompletion];
}
}];
return nil;
}];
}
+ (MTSignal *)signalForTextUrlItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeURL options:nil completionHandler:^(NSURL *url, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"url": url}];
[subscriber putCompletion];
}
}];
return nil;
}];
}
+ (MTSignal *)signalForVCardItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeVCard options:nil completionHandler:^(NSData *vcard, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"contact": vcard}];
[subscriber putCompletion];
}
}];
return nil;
}];
}
+ (MTSignal *)signalForPassKitItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:@"com.apple.pkpass" options:nil completionHandler:^(id data, NSError *error)
{
if (error != nil)
{
[subscriber putError:nil];
}
else
{
NSError *parseError;
PKPass *pass = [[PKPass alloc] initWithData:data error:&parseError];
if (parseError != nil)
{
[subscriber putError:nil];
}
else
{
NSString *fileName = [NSString stringWithFormat:@"%@.pkpass", pass.serialNumber];
[subscriber putNext:@{@"data": data, @"fileName": fileName, @"mimeType": @"application/vnd.apple.pkpass"}];
[subscriber putCompletion];
}
}
}];
return nil;
}];
}
static void set_bits(uint8_t *bytes, int32_t bitOffset, int32_t numBits, int32_t value) {
numBits = (unsigned int)pow(2, numBits) - 1; //this will only work up to 32 bits, of course
uint8_t *data = bytes;
data += bitOffset / 8;
bitOffset %= 8;
*((int32_t *)data) |= ((value) << bitOffset);
}
+ (NSData *)audioWaveform:(NSURL *)url {
NSDictionary *outputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
[NSNumber numberWithFloat:44100.0], AVSampleRateKey,
[NSNumber numberWithInt:16], AVLinearPCMBitDepthKey,
[NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
[NSNumber numberWithBool:NO], AVLinearPCMIsFloatKey,
[NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey,
nil];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
if (asset == nil) {
NSLog(@"asset is not defined!");
return nil;
}
NSError *assetError = nil;
AVAssetReader *iPodAssetReader = [AVAssetReader assetReaderWithAsset:asset error:&assetError];
if (assetError) {
NSLog (@"error: %@", assetError);
return nil;
}
AVAssetReaderOutput *readerOutput = [AVAssetReaderAudioMixOutput assetReaderAudioMixOutputWithAudioTracks:asset.tracks audioSettings:outputSettings];
if (! [iPodAssetReader canAddOutput: readerOutput]) {
NSLog (@"can't add reader output... die!");
return nil;
}
// add output reader to reader
[iPodAssetReader addOutput: readerOutput];
if (![iPodAssetReader startReading]) {
NSLog(@"Unable to start reading!");
return nil;
}
NSMutableData *_waveformSamples = [[NSMutableData alloc] init];
int16_t _waveformPeak = 0;
int _waveformPeakCount = 0;
while (iPodAssetReader.status == AVAssetReaderStatusReading) {
CMSampleBufferRef nextBuffer = [readerOutput copyNextSampleBuffer];
if (nextBuffer) {
AudioBufferList abl;
CMBlockBufferRef blockBuffer = NULL;
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(nextBuffer, NULL, &abl, sizeof(abl), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer);
UInt64 size = CMSampleBufferGetTotalSampleSize(nextBuffer);
if (size != 0) {
int16_t *samples = (int16_t *)(abl.mBuffers[0].mData);
int count = (int)size / 2;
for (int i = 0; i < count; i++) {
int16_t sample = samples[i];
if (sample < 0) {
sample = -sample;
}
if (_waveformPeak < sample) {
_waveformPeak = sample;
}
_waveformPeakCount++;
if (_waveformPeakCount >= 100) {
[_waveformSamples appendBytes:&_waveformPeak length:2];
_waveformPeak = 0;
_waveformPeakCount = 0;
}
}
}
CFRelease(nextBuffer);
if (blockBuffer) {
CFRelease(blockBuffer);
}
}
else {
break;
}
}
int16_t scaledSamples[100];
memset(scaledSamples, 0, 100 * 2);
int16_t *samples = _waveformSamples.mutableBytes;
int count = (int)_waveformSamples.length / 2;
for (int i = 0; i < count; i++) {
int16_t sample = samples[i];
int index = i * 100 / count;
if (scaledSamples[index] < sample) {
scaledSamples[index] = sample;
}
}
int16_t peak = 0;
int64_t sumSamples = 0;
for (int i = 0; i < 100; i++) {
int16_t sample = scaledSamples[i];
if (peak < sample) {
peak = sample;
}
sumSamples += sample;
}
uint16_t calculatedPeak = 0;
calculatedPeak = (uint16_t)(sumSamples * 1.8f / 100);
if (calculatedPeak < 2500) {
calculatedPeak = 2500;
}
for (int i = 0; i < 100; i++) {
uint16_t sample = (uint16_t)((int64_t)samples[i]);
if (sample > calculatedPeak) {
scaledSamples[i] = calculatedPeak;
}
}
int numSamples = 100;
int bitstreamLength = (numSamples * 5) / 8 + (((numSamples * 5) % 8) == 0 ? 0 : 1);
NSMutableData *result = [[NSMutableData alloc] initWithLength:bitstreamLength];
{
int32_t maxSample = peak;
uint16_t const *samples = (uint16_t *)scaledSamples;
uint8_t *bytes = result.mutableBytes;
for (int i = 0; i < numSamples; i++) {
int32_t value = MIN(31, ABS((int32_t)samples[i]) * 31 / maxSample);
set_bits(bytes, i * 5, 5, value & 31);
}
}
return result;
}
@end
@@ -0,0 +1,373 @@
#import <ShareItemsImpl/TGShareLocationSignals.h>
#import <MtProtoKit/MtProtoKit.h>
NSString *const TGShareAppleMapsHost = @"maps.apple.com";
NSString *const TGShareAppleMapsPath = @"/maps";
NSString *const TGShareAppleMapsLatLonKey = @"ll";
NSString *const TGShareAppleMapsNameKey = @"q";
NSString *const TGShareAppleMapsAddressKey = @"address";
NSString *const TGShareAppleMapsIdKey = @"auid";
NSString *const TGShareAppleMapsProvider = @"apple";
NSString *const TGShareFoursquareHost = @"foursquare.com";
NSString *const TGShareFoursquareVenuePath = @"/v";
NSString *const TGShareFoursquareVenueEndpointUrl = @"https://api.foursquare.com/v2/venues/";
NSString *const TGShareFoursquareClientId = @"BN3GWQF1OLMLKKQTFL0OADWD1X1WCDNISPPOT1EMMUYZTQV1";
NSString *const TGShareFoursquareClientSecret = @"WEEZHCKI040UVW2KWW5ZXFAZ0FMMHKQ4HQBWXVSX4WXWBWYN";
NSString *const TGShareFoursquareVersion = @"20150326";
NSString *const TGShareFoursquareVenuesCountLimit = @"25";
NSString *const TGShareFoursquareLocale = @"en";
NSString *const TGShareFoursquareProvider = @"foursquare";
NSString *const TGShareGoogleShortenerEndpointUrl = @"https://www.googleapis.com/urlshortener/v1/url";
NSString *const TGShareGoogleAPIKey = @"AIzaSyBCTH4aAdvi0MgDGlGNmQAaFS8GTNBrfj4";
NSString *const TGShareGoogleMapsShortHost = @"goo.gl";
NSString *const TGShareGoogleMapsShortPath = @"/maps";
NSString *const TGShareGoogleMapsHost = @"google.com";
NSString *const TGShareGoogleMapsSearchPath = @"maps/search";
NSString *const TGShareGoogleMapsPlacePath = @"maps/place";
NSString *const TGShareGoogleProvider = @"google";
@implementation TGShareLocationResult
- (instancetype)initWithLatitude:(double)latitude longitude:(double)longitude title:(NSString *)title address:(NSString *)address provider:(NSString *)provider venueId:(NSString *)venueId venueType:(NSString *)venueType {
self = [super init];
if (self != nil) {
_latitude = latitude;
_longitude = longitude;
_title = title;
_address = address;
_provider = provider;
_venueId = venueId;
_venueType = venueType;
}
return self;
}
@end
@interface TGQueryStringComponent : NSObject {
@private
NSString *_key;
NSString *_value;
}
@property (readwrite, nonatomic, retain) id key;
@property (readwrite, nonatomic, retain) id value;
- (id)initWithKey:(id)key value:(id)value;
- (NSString *)URLEncodedStringValueWithEncoding:(NSStringEncoding)stringEncoding;
@end
NSString * TGURLEncodedStringFromStringWithEncoding(NSString *string, NSStringEncoding encoding) {
static NSString * const kAFLegalCharactersToBeEscaped = @"?!@#$^&%*+=,:;'\"`<>()[]{}/\\|~ ";
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSString *unescapedString = [string stringByReplacingPercentEscapesUsingEncoding:encoding];
if (unescapedString) {
string = unescapedString;
}
return (__bridge NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)string, NULL, (__bridge CFStringRef)kAFLegalCharactersToBeEscaped, CFStringConvertNSStringEncodingToEncoding(encoding));
#pragma clang diagnostic pop
}
@implementation TGQueryStringComponent
@synthesize key = _key;
@synthesize value = _value;
- (id)initWithKey:(id)key value:(id)value {
self = [super init];
if (!self) {
return nil;
}
self.key = key;
self.value = value;
return self;
}
- (NSString *)URLEncodedStringValueWithEncoding:(NSStringEncoding)stringEncoding {
return [NSString stringWithFormat:@"%@=%@", self.key, TGURLEncodedStringFromStringWithEncoding([self.value description], stringEncoding)];
}
@end
static NSString * TGQueryStringFromParametersWithEncoding(NSDictionary *parameters, NSStringEncoding stringEncoding);
static NSArray * TGQueryStringComponentsFromKeyAndValue(NSString *key, id value);
NSArray * TGQueryStringComponentsFromKeyAndDictionaryValue(NSString *key, NSDictionary *value);
NSArray * TGQueryStringComponentsFromKeyAndArrayValue(NSString *key, NSArray *value);
static NSString * TGQueryStringFromParametersWithEncoding(NSDictionary *parameters, NSStringEncoding stringEncoding) {
NSMutableArray *mutableComponents = [NSMutableArray array];
for (TGQueryStringComponent *component in TGQueryStringComponentsFromKeyAndValue(nil, parameters)) {
[mutableComponents addObject:[component URLEncodedStringValueWithEncoding:stringEncoding]];
}
return [mutableComponents componentsJoinedByString:@"&"];
}
static NSArray * TGQueryStringComponentsFromKeyAndValue(NSString *key, id value) {
NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
if([value isKindOfClass:[NSDictionary class]]) {
[mutableQueryStringComponents addObjectsFromArray:TGQueryStringComponentsFromKeyAndDictionaryValue(key, value)];
} else if([value isKindOfClass:[NSArray class]]) {
[mutableQueryStringComponents addObjectsFromArray:TGQueryStringComponentsFromKeyAndArrayValue(key, value)];
} else {
[mutableQueryStringComponents addObject:[[TGQueryStringComponent alloc] initWithKey:key value:value]];
}
return mutableQueryStringComponents;
}
NSArray * TGQueryStringComponentsFromKeyAndDictionaryValue(NSString *key, NSDictionary *value){
NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
[value enumerateKeysAndObjectsUsingBlock:^(id nestedKey, id nestedValue, __unused BOOL *stop) {
[mutableQueryStringComponents addObjectsFromArray:TGQueryStringComponentsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
}];
return mutableQueryStringComponents;
}
NSArray * TGQueryStringComponentsFromKeyAndArrayValue(NSString *key, NSArray *value) {
NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
[value enumerateObjectsUsingBlock:^(id nestedValue, __unused NSUInteger idx, __unused BOOL *stop) {
[mutableQueryStringComponents addObjectsFromArray:TGQueryStringComponentsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
}];
return mutableQueryStringComponents;
}
@implementation TGShareLocationSignals
+ (MTSignal *)locationMessageContentForURL:(NSURL *)url
{
if ([self isAppleMapsURL:url])
return [self _appleMapsLocationContentForURL:url];
else if ([self isFoursquareURL:url])
return [self _foursquareLocationForURL:url];
return [MTSignal single:nil];
}
+ (MTSignal *)_appleMapsLocationContentForURL:(NSURL *)url
{
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:false];
NSArray *queryItems = urlComponents.queryItems;
NSString *latLon = nil;
NSString *name = nil;
NSString *address = nil;
NSString *venueId = nil;
for (NSURLQueryItem *queryItem in queryItems)
{
if ([queryItem.name isEqualToString:TGShareAppleMapsLatLonKey])
{
latLon = queryItem.value;
}
else if ([queryItem.name isEqualToString:TGShareAppleMapsNameKey])
{
if (![queryItem.value isEqualToString:latLon])
name = queryItem.value;
}
else if ([queryItem.name isEqualToString:TGShareAppleMapsAddressKey])
{
address = queryItem.value;
}
else if ([queryItem.name isEqualToString:TGShareAppleMapsIdKey])
{
venueId = queryItem.value;
}
}
if (latLon == nil)
return [MTSignal fail:nil];
NSArray *coordComponents = [latLon componentsSeparatedByString:@","];
if (coordComponents.count != 2)
return [MTSignal fail:nil];
double latitude = [coordComponents.firstObject floatValue];
double longitude = [coordComponents.lastObject floatValue];
return [MTSignal single:[[TGShareLocationResult alloc] initWithLatitude:latitude longitude:longitude title:name address:address provider:TGShareAppleMapsProvider venueId:venueId venueType:@""]];
}
+ (MTSignal *)_foursquareLocationForURL:(NSURL *)url
{
NSArray *pathComponents = url.pathComponents;
NSString *venueId = nil;
for (NSString *component in pathComponents)
{
if (component.length == 24)
{
venueId = component;
break;
}
}
if (venueId == nil)
return [MTSignal fail:nil];
NSString *urlString = [NSString stringWithFormat:@"%@?%@", [TGShareFoursquareVenueEndpointUrl stringByAppendingPathComponent:venueId], TGQueryStringFromParametersWithEncoding([self _defaultParametersForFoursquare], NSUTF8StringEncoding)];
return [[MTHttpRequestOperation dataForHttpUrl:[NSURL URLWithString:urlString]] mapToSignal:^id(MTHttpResponse *response)
{
NSData *data = response.data;
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (![json respondsToSelector:@selector(objectForKey:)])
return nil;
NSDictionary *venue = json[@"response"][@"venue"];
if (![venue respondsToSelector:@selector(objectForKey:)])
return nil;
NSString *name = venue[@"name"];
NSDictionary *location = venue[@"location"];
NSString *address = location[@"address"];
if (address.length == 0)
address = location[@"crossStreet"];
if (address.length == 0)
address = location[@"city"];
if (address.length == 0)
address = location[@"country"];
if (address.length == 0)
address = @"";
double latitude = [location[@"lat"] doubleValue];
double longitude = [location[@"lng"] doubleValue];
if (name.length == 0)
return [MTSignal fail:nil];
return [MTSignal single:[[TGShareLocationResult alloc] initWithLatitude:latitude longitude:longitude title:name address:address provider:TGShareFoursquareProvider venueId:venueId venueType:@""]];
}];
}
+ (MTSignal *)_googleMapsLocationForURL:(NSURL *)url
{
NSString *shortenerUrl = [NSString stringWithFormat:@"%@?fields=longUrl,status&shortUrl=%@&key=%@", TGShareGoogleShortenerEndpointUrl, TGURLEncodedStringFromStringWithEncoding(url.absoluteString, NSUTF8StringEncoding), TGShareGoogleAPIKey];
MTSignal *shortenerSignal = [[MTHttpRequestOperation dataForHttpUrl:[NSURL URLWithString:shortenerUrl]] mapToSignal:^MTSignal *(MTHttpResponse *response)
{
NSData *data = response.data;
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (![json respondsToSelector:@selector(objectForKey:)])
return [MTSignal fail:nil];
NSString *status = json[@"status"];
if (![status isEqualToString:@"OK"])
return [MTSignal fail:nil];
return [MTSignal single:[NSURL URLWithString:json[@"longUrl"]]];
}];
MTSignal *(^processLongUrl)(NSURL *) = ^MTSignal *(NSURL *longUrl)
{
NSArray *pathComponents = longUrl.pathComponents;
bool isSearch = false;
double latitude = 0.0;
double longitude = 0.0;
for (NSString *component in pathComponents)
{
if ([component isEqualToString:@"search"])
{
isSearch = true;
}
else if ([component isEqualToString:@"place"])
{
return [MTSignal fail:nil];
}
else if (isSearch && [component containsString:@","])
{
NSArray *coordinates = [component componentsSeparatedByString:@","];
if (coordinates.count == 2)
{
latitude = [coordinates.firstObject doubleValue];
longitude = [coordinates.lastObject doubleValue];
break;
}
}
}
if (fabs(latitude) < DBL_EPSILON && fabs(longitude) < DBL_EPSILON)
return [MTSignal fail:nil];
return [MTSignal single:[[TGShareLocationResult alloc] initWithLatitude:latitude longitude:longitude title:nil address:nil provider:nil venueId:nil venueType:nil]];
};
MTSignal *signal = nil;
if ([self _isShortGoogleMapsURL:url])
{
signal = [shortenerSignal mapToSignal:^MTSignal *(NSURL *longUrl)
{
return processLongUrl(longUrl);
}];
}
else
{
signal = processLongUrl(url);
}
return [signal catch:^MTSignal *(id error)
{
return [MTSignal single:url.absoluteString];
}];
}
+ (NSDictionary *)_defaultParametersForFoursquare
{
return @
{
@"v": TGShareFoursquareVersion,
@"locale": TGShareFoursquareLocale,
@"client_id": TGShareFoursquareClientId,
@"client_secret" :TGShareFoursquareClientSecret
};
}
+ (bool)isLocationURL:(NSURL *)url
{
return [self isAppleMapsURL:url] || [self isFoursquareURL:url];
}
+ (bool)isAppleMapsURL:(NSURL *)url
{
return ([url.host isEqualToString:TGShareAppleMapsHost] && [url.path isEqualToString:TGShareAppleMapsPath]);
}
+ (bool)isFoursquareURL:(NSURL *)url
{
return ([url.host isEqualToString:TGShareFoursquareHost] && [url.path hasPrefix:TGShareFoursquareVenuePath]);
}
+ (bool)_isShortGoogleMapsURL:(NSURL *)url
{
return ([url.host isEqualToString:TGShareGoogleMapsShortHost] && [url.path hasPrefix:TGShareGoogleMapsShortPath]);
}
+ (bool)_isLongGoogleMapsURL:(NSURL *)url
{
return ([url.host isEqualToString:TGShareGoogleMapsHost] && ([url.path hasPrefix:TGShareGoogleMapsSearchPath] || [url.path hasPrefix:TGShareGoogleMapsPlacePath]));
}
+ (bool)isGoogleMapsURL:(NSURL *)url
{
return [self _isShortGoogleMapsURL:url] || [self _isLongGoogleMapsURL:url];
}
@end
@@ -0,0 +1,567 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
import MtProtoKit
import Display
import AccountContext
import Pdf
import LocalMediaResources
import AVFoundation
import LegacyComponents
import ShareItemsImpl
import UIKit
import SSignalKit
public enum UnpreparedShareItemContent {
case contact(DeviceContactExtendedData)
}
public enum PreparedShareItemContent {
case text(String)
case media(StandaloneUploadMediaResult)
}
public enum PreparedShareItem {
case preparing(Bool)
case progress(Float)
case userInteractionRequired(UnpreparedShareItemContent)
case done(PreparedShareItemContent)
}
public enum PreparedShareItems {
case preparing(Bool)
case progress(Float)
case userInteractionRequired([UnpreparedShareItemContent])
case done([PreparedShareItemContent])
}
private func scalePhotoImage(_ image: UIImage, dimensions: CGSize) -> UIImage? {
let format = UIGraphicsImageRendererFormat()
format.scale = 1.0
let renderer = UIGraphicsImageRenderer(size: dimensions, format: format)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: dimensions))
}
}
public enum PreparedShareItemError {
case generic
case fileTooBig(Int64)
}
private func preparedShareItem(postbox: Postbox, network: Network, to peerId: PeerId, value: [String: Any]) -> Signal<PreparedShareItem, PreparedShareItemError> {
if let imageData = value["scaledImageData"] as? Data, let dimensions = value["scaledImageDimensions"] as? NSValue {
let diminsionsSize = dimensions.cgSizeValue
return .single(.preparing(false))
|> then(
standaloneUploadedImage(postbox: postbox, network: network, peerId: peerId, text: "", data: imageData, dimensions: PixelDimensions(width: Int32(diminsionsSize.width), height: Int32(diminsionsSize.height)))
|> mapError { _ -> PreparedShareItemError in
return .generic
}
|> mapToSignal { event -> Signal<PreparedShareItem, PreparedShareItemError> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
)
} else if let image = value["image"] as? UIImage {
let nativeImageSize = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale)
let dimensions = nativeImageSize.fitted(CGSize(width: 1280.0, height: 1280.0))
if let scaledImage = scalePhotoImage(image, dimensions: dimensions), let imageData = scaledImage.jpegData(compressionQuality: 0.52) {
return .single(.preparing(false))
|> then(
standaloneUploadedImage(postbox: postbox, network: network, peerId: peerId, text: "", data: imageData, dimensions: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)))
|> mapError { _ -> PreparedShareItemError in
return .generic
}
|> mapToSignal { event -> Signal<PreparedShareItem, PreparedShareItemError> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
)
} else {
return .never()
}
} else if let asset = value["video"] as? AVURLAsset {
var flags: TelegramMediaVideoFlags = [.supportsStreaming]
let sendAsInstantRoundVideo = value["isRoundMessage"] as? Bool ?? false
var adjustments: TGVideoEditAdjustments? = nil
if sendAsInstantRoundVideo {
flags.insert(.instantRoundVideo)
if let width = value["width"] as? CGFloat, let height = value["height"] as? CGFloat {
let size = CGSize(width: width, height: height)
var cropRect = CGRect(origin: CGPoint(), size: size)
if abs(width - height) < CGFloat.ulpOfOne {
cropRect = cropRect.insetBy(dx: 13.0, dy: 13.0)
cropRect = cropRect.offsetBy(dx: 2.0, dy: 3.0)
} else {
let shortestSide = min(size.width, size.height)
cropRect = CGRect(x: (size.width - shortestSide) / 2.0, y: (size.height - shortestSide) / 2.0, width: shortestSide, height: shortestSide)
}
adjustments = TGVideoEditAdjustments(originalSize: size, cropRect: cropRect, cropOrientation: .up, cropRotation: 0.0, cropLockedAspectRatio: 1.0, cropMirrored: false, trimStartValue: 0.0, trimEndValue: 0.0, toolValues: nil, paintingData: nil, sendAsGif: false, preset: TGMediaVideoConversionPresetVideoMessage)
}
}
var finalDuration: Double = CMTimeGetSeconds(asset.duration)
func loadValues(_ avAsset: AVURLAsset) -> Signal<AVURLAsset, PreparedShareItemError> {
return Signal { subscriber in
avAsset.loadValuesAsynchronously(forKeys: ["tracks", "duration", "playable"]) {
subscriber.putNext(avAsset)
}
return EmptyDisposable
}
}
func getThumbnail(_ avAsset: AVURLAsset) -> Signal<UIImage?, NoError> {
return Signal { subscriber in
let imageGenerator = AVAssetImageGenerator(asset: asset)
imageGenerator.appliesPreferredTrackTransform = true
imageGenerator.maximumSize = CGSize(width: 640, height: 640)
imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in
subscriber.putNext(image.flatMap { UIImage(cgImage: $0) })
subscriber.putCompletion()
}
return ActionDisposable {
imageGenerator.cancelAllCGImageGeneration()
}
}
}
return .single(.preparing(true))
|> then(
loadValues(asset)
|> mapToSignal { asset -> Signal<PreparedShareItem, PreparedShareItemError> in
return getThumbnail(asset)
|> castError(PreparedShareItemError.self)
|> mapToSignal { thumbnail -> Signal<PreparedShareItem, PreparedShareItemError> in
let preset = adjustments?.preset ?? TGMediaVideoConversionPresetCompressedMedium
let finalDimensions = TGMediaVideoConverter.dimensions(for: asset.originalSize, adjustments: adjustments, preset: preset)
var resourceAdjustments: VideoMediaResourceAdjustments?
if let adjustments = adjustments {
if adjustments.trimApplied() {
finalDuration = adjustments.trimEndValue - adjustments.trimStartValue
}
if let dict = adjustments.dictionary(), let data = try? NSKeyedArchiver.archivedData(withRootObject: dict, requiringSecureCoding: false) {
let adjustmentsData = MemoryBuffer(data: data)
let digest = MemoryBuffer(data: adjustmentsData.md5Digest())
resourceAdjustments = VideoMediaResourceAdjustments(data: adjustmentsData, digest: digest, isStory: false)
}
}
let estimatedSize = TGMediaVideoConverter.estimatedSize(for: preset, duration: finalDuration, hasAudio: true)
let thumbnailData = thumbnail?.jpegData(compressionQuality: 0.6)
let resource = LocalFileVideoMediaResource(randomId: Int64.random(in: Int64.min ... Int64.max), path: asset.url.path, adjustments: resourceAdjustments)
return standaloneUploadedFile(postbox: postbox, network: network, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), thumbnailData: thumbnailData, mimeType: "video/mp4", attributes: [.Video(duration: finalDuration, size: PixelDimensions(width: Int32(finalDimensions.width), height: Int32(finalDimensions.height)), flags: flags, preloadSize: nil, coverTime: 0.0, videoCodec: nil)], hintFileIsLarge: estimatedSize > 10 * 1024 * 1024)
|> mapError { _ -> PreparedShareItemError in
return .generic
}
|> mapToSignal { event -> Signal<PreparedShareItem, PreparedShareItemError> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
}
}
)
} else if let data = value["data"] as? Data {
let fileName = value["fileName"] as? String
let mimeType = (value["mimeType"] as? String) ?? "application/octet-stream"
var treatAsFile = false
if let boolValue = value["treatAsFile"] as? Bool, boolValue {
treatAsFile = true
}
if !treatAsFile, let image = UIImage(data: data) {
var isGif = false
if data.count > 4 {
data.withUnsafeBytes { buffer -> Void in
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
if bytes.advanced(by: 0).pointee == 71 // G
&& bytes.advanced(by: 1).pointee == 73 // I
&& bytes.advanced(by: 2).pointee == 70 // F
&& bytes.advanced(by: 3).pointee == 56 // 8
{
isGif = true
}
}
}
if isGif {
#if DEBUG
let signal = SSignal(generator: { _ in
return SBlockDisposable(block: {})
})
let _ = signal.start(next: nil, error: nil, completed: nil)
#endif
let convertedData = Signal<(Data, CGSize, Double, Bool), NoError> { subscriber in
let disposable = MetaDisposable()
let signalDisposable = TGGifConverter.convertGif(toMp4: data).start(next: { next in
if let result = next as? NSDictionary, let path = result["path"] as? String, let convertedData = try? Data(contentsOf: URL(fileURLWithPath: path)), let duration = result["duration"] as? Double {
subscriber.putNext((convertedData, image.size, duration, true))
subscriber.putCompletion()
}
}, error: { _ in
subscriber.putNext((data, image.size, 0, false))
subscriber.putCompletion()
}, completed: nil)
disposable.set(ActionDisposable {
signalDisposable?.dispose()
})
return disposable
}
return .single(.preparing(true))
|> then(
convertedData
|> castError(PreparedShareItemError.self)
|> mapToSignal { data, dimensions, duration, converted in
var attributes: [TelegramMediaFileAttribute] = []
let mimeType: String
if converted {
mimeType = "video/mp4"
attributes = [.Video(duration: duration, size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height)), flags: [.supportsStreaming], preloadSize: nil, coverTime: nil, videoCodec: nil), .Animated, .FileName(fileName: "animation.mp4")]
} else {
mimeType = "animation/gif"
attributes = [.ImageSize(size: PixelDimensions(width: Int32(dimensions.width), height: Int32(dimensions.height))), .Animated, .FileName(fileName: fileName ?? "animation.gif")]
}
return standaloneUploadedFile(postbox: postbox, network: network, peerId: peerId, text: "", source: .data(data), mimeType: mimeType, attributes: attributes, hintFileIsLarge: data.count > 10 * 1024 * 1024)
|> mapError { _ -> PreparedShareItemError in
return .generic
}
|> mapToSignal { event -> Signal<PreparedShareItem, PreparedShareItemError> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
}
)
} else {
let scaledImage = scalePhotoImage(image, dimensions: CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale).fitted(CGSize(width: 1280.0, height: 1280.0)))!
let imageData = scaledImage.jpegData(compressionQuality: 0.54)!
return .single(.preparing(false))
|> then(
standaloneUploadedImage(postbox: postbox, network: network, peerId: peerId, text: "", data: imageData, dimensions: PixelDimensions(width: Int32(scaledImage.size.width), height: Int32(scaledImage.size.height)))
|> mapError { _ -> PreparedShareItemError in
return .generic
}
|> mapToSignal { event -> Signal<PreparedShareItem, PreparedShareItemError> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
)
}
} else {
var thumbnailData: Data?
if mimeType == "application/pdf", let image = generatePdfPreviewImage(data: data, size: CGSize(width: 256.0, height: 256.0)), let jpegData = image.jpegData(compressionQuality: 0.5) {
thumbnailData = jpegData
}
let long = data.count > Int32(1.5 * 1024 * 1024)
return .single(.preparing(long))
|> then(
standaloneUploadedFile(postbox: postbox, network: network, peerId: peerId, text: "", source: .data(data), thumbnailData: thumbnailData, mimeType: mimeType, attributes: [.FileName(fileName: fileName ?? "file")], hintFileIsLarge: data.count > 10 * 1024 * 1024)
|> mapError { _ -> PreparedShareItemError in
return .generic
}
|> mapToSignal { event -> Signal<PreparedShareItem, PreparedShareItemError> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
)
}
} else if let url = value["audio"] as? URL {
if let audioData = try? Data(contentsOf: url, options: [.mappedIfSafe]) {
let fileName = url.lastPathComponent
let duration = (value["duration"] as? NSNumber)?.doubleValue ?? 0.0
let isVoice = ((value["isVoice"] as? NSNumber)?.boolValue ?? false)
let title = value["title"] as? String
let artist = value["artist"] as? String
let mimeType = value["mimeType"] as? String ?? "audio/ogg"
var waveform: MemoryBuffer?
if let waveformData = TGItemProviderSignals.audioWaveform(url) {
waveform = MemoryBuffer(data: waveformData)
}
let long = audioData.count > Int32(1.5 * 1024 * 1024)
return .single(.preparing(long))
|> then(
standaloneUploadedFile(postbox: postbox, network: network, peerId: peerId, text: "", source: .data(audioData), mimeType: mimeType, attributes: [.Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: artist, waveform: waveform?.makeData()), .FileName(fileName: fileName)], hintFileIsLarge: audioData.count > 10 * 1024 * 1024)
|> mapError { _ -> PreparedShareItemError in
return .generic
}
|> mapToSignal { event -> Signal<PreparedShareItem, PreparedShareItemError> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
)
} else {
return .never()
}
} else if let text = value["text"] as? String {
return .single(.preparing(false))
|> then(
.single(.done(.text(text)))
)
} else if let url = value["url"] as? URL {
if TGShareLocationSignals.isLocationURL(url) {
return Signal<PreparedShareItem, PreparedShareItemError> { subscriber in
subscriber.putNext(.preparing(false))
let disposable = TGShareLocationSignals.locationMessageContent(for: url).start(next: { value in
if let value = value as? TGShareLocationResult {
if let title = value.title {
subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: title, address: value.address, provider: value.provider, id: value.venueId, type: value.venueType), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))))))
} else {
subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))))))
}
subscriber.putCompletion()
} else if let value = value as? String {
subscriber.putNext(.done(.text(value)))
subscriber.putCompletion()
}
})
return ActionDisposable {
disposable?.dispose()
}
}
} else {
return .single(.preparing(false))
|> then(
.single(.done(.text(url.absoluteString)))
)
}
} else if let vcard = value["contact"] as? Data, let contactData = DeviceContactExtendedData(vcard: vcard) {
return .single(.userInteractionRequired(.contact(contactData)))
} else {
return .never()
}
}
public func preparedShareItems(postbox: Postbox, network: Network, to peerId: PeerId, dataItems: [MTSignal]) -> Signal<PreparedShareItems, PreparedShareItemError> {
var dataSignals: Signal<[String: Any], PreparedShareItemError> = .complete()
for dataItem in dataItems {
let wrappedSignal: Signal<[String: Any], NoError> = Signal { subscriber in
let disposable = dataItem.start(next: { value in
subscriber.putNext(value as! [String : Any])
}, error: { _ in
}, completed: {
subscriber.putCompletion()
})
return ActionDisposable {
disposable?.dispose()
}
}
dataSignals = dataSignals
|> then(
wrappedSignal
|> castError(PreparedShareItemError.self)
|> take(1)
)
}
let shareItems = dataSignals
|> map { [$0] }
|> reduceLeft(value: [[String: Any]](), f: { list, rest in
return list + rest
})
|> mapToSignal { items -> Signal<[PreparedShareItem], PreparedShareItemError> in
return combineLatest(items.map {
preparedShareItem(postbox: postbox, network: network, to: peerId, value: $0)
})
}
return shareItems
|> map { items -> PreparedShareItems in
var result: [PreparedShareItemContent] = []
var progresses: [Float] = []
for item in items {
switch item {
case let .preparing(long):
return .preparing(long)
case let .progress(value):
progresses.append(value)
case let .userInteractionRequired(value):
return .userInteractionRequired([value])
case let .done(content):
result.append(content)
progresses.append(1.0)
}
}
if result.count == items.count {
return .done(result)
} else {
let value = progresses.reduce(0.0, +) / Float(progresses.count)
return .progress(value)
}
}
|> distinctUntilChanged(isEqual: { lhs, rhs in
if case .preparing = lhs, case .preparing = rhs {
return true
} else {
return false
}
})
}
public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, auxiliaryMethods: AccountAuxiliaryMethods, to peerIds: [PeerId], threadIds: [PeerId: Int64], requireStars: [PeerId: StarsAmount], items: [PreparedShareItemContent], silently: Bool, additionalText: String) -> Signal<Float, Void> {
var messages: [StandaloneSendEnqueueMessage] = []
var groupingKey: Int64?
var mediaTypes: (photo: Int, video: Int, music: Int, other: Int) = (0, 0, 0, 0)
if items.count > 1 {
for item in items {
if case let .media(result) = item, case let .media(media) = result {
if media.media is TelegramMediaImage {
mediaTypes.photo += 1
} else if let media = media.media as? TelegramMediaFile {
if media.isVideo {
mediaTypes.video += 1
} else if media.isVoice || media.isAnimated || media.isSticker {
mediaTypes = (0, 0, 0, 0)
break
} else if media.isMusic {
mediaTypes.music += 1
} else if let fileName = media.fileName?.lowercased(), fileName.hasPrefix(".mp3") || fileName.hasPrefix("m4a") {
mediaTypes.music += 1
} else {
mediaTypes.other += 1
}
} else {
mediaTypes = (0, 0, 0, 0)
break
}
}
}
}
if ((mediaTypes.photo + mediaTypes.video) > 1) && (mediaTypes.music == 0 && mediaTypes.other == 0) {
groupingKey = Int64.random(in: Int64.min ... Int64.max)
} else if ((mediaTypes.photo + mediaTypes.video) == 0) && ((mediaTypes.music > 1 && mediaTypes.other == 0) || (mediaTypes.music == 0 && mediaTypes.other > 1)) {
groupingKey = Int64.random(in: Int64.min ... Int64.max)
}
var mediaMessageCount = 0
var consumedText = false
for item in items {
switch item {
case let .text(text):
var message = StandaloneSendEnqueueMessage(
content: .text(text: StandaloneSendEnqueueMessage.Text(
string: text,
entities: []
)),
replyToMessageId: nil
)
message.isSilent = silently
messages.append(message)
case let .media(media):
switch media {
case let .media(reference):
var message = StandaloneSendEnqueueMessage(
content: .arbitraryMedia(
media: reference,
text: StandaloneSendEnqueueMessage.Text(
string: additionalText,
entities: []
)
),
replyToMessageId: nil
)
consumedText = true
message.isSilent = silently
message.groupingKey = groupingKey
messages.append(message)
mediaMessageCount += 1
}
if let _ = groupingKey, mediaMessageCount % 10 == 0 {
groupingKey = Int64.random(in: Int64.min ... Int64.max)
}
}
}
if !consumedText && !additionalText.isEmpty {
var message = StandaloneSendEnqueueMessage(
content: .text(text: StandaloneSendEnqueueMessage.Text(
string: additionalText,
entities: []
)),
replyToMessageId: nil
)
message.isSilent = silently
messages.append(message)
}
var peerSignals: Signal<Float, StandaloneSendMessagesError> = .single(0.0)
for peerId in peerIds {
var peerMessages = messages
if let amount = requireStars[peerId] {
var updatedMessages: [StandaloneSendEnqueueMessage] = []
for message in peerMessages {
var message = message
message.sendPaidMessageStars = amount
updatedMessages.append(message)
}
peerMessages = updatedMessages
}
peerSignals = peerSignals |> then(standaloneSendEnqueueMessages(
accountPeerId: accountPeerId,
postbox: postbox,
network: network,
stateManager: stateManager,
auxiliaryMethods: auxiliaryMethods,
peerId: peerId,
threadId: threadIds[peerId],
messages: peerMessages
)
|> mapToSignal { status -> Signal<Float, StandaloneSendMessagesError> in
switch status {
case let .progress(progress):
//return .single(progress)
let _ = progress
return .complete()
case .done:
return .complete()
}
})
}
return peerSignals
|> mapError { _ -> Void in
return Void()
}
}