From e4162a6f0b0d7e88ff49cf46d7579b0b74ce7803 Mon Sep 17 00:00:00 2001 From: yandevelop <82288425+yandevelop@users.noreply.github.com> Date: Tue, 6 Jun 2023 13:05:34 +0200 Subject: [PATCH] 1.2 - Add "BeFake" functionality: upload custom BeReals from camera roll, spoof location and the time the photo was taken - Stability improvements (random app crashes aren't caused by Bea. BeReal itself is aware of those crashes) --- .gitignore | 5 +- Makefile | 13 +- README.md | 24 +- Tweak/Tweak.h | 6 +- Tweak/Tweak.x | 142 +++-- Utilities/BeaUtilities.m | 11 +- .../BeaInfoViewController.h | 7 + .../BeaInfoViewController.m | 85 +++ .../BeaLocationViewController.h | 19 + .../BeaLocationViewController.m | 120 ++++ Utilities/UploadTask/BeaUploadTask.h | 15 + Utilities/UploadTask/BeaUploadTask.m | 243 ++++++++ .../BeaUploadViewController.h | 35 ++ .../BeaUploadViewController.m | 537 ++++++++++++++++++ .../StatusView/BeaStatusView.h | 6 + .../StatusView/BeaStatusView.m | 56 ++ control | 2 +- .../Application Support/Bea.bundle/BeFake.png | Bin 0 -> 7696 bytes 18 files changed, 1270 insertions(+), 56 deletions(-) create mode 100644 Utilities/InfoViewController/BeaInfoViewController.h create mode 100644 Utilities/InfoViewController/BeaInfoViewController.m create mode 100644 Utilities/LocationViewController/BeaLocationViewController.h create mode 100644 Utilities/LocationViewController/BeaLocationViewController.m create mode 100644 Utilities/UploadTask/BeaUploadTask.h create mode 100644 Utilities/UploadTask/BeaUploadTask.m create mode 100644 Utilities/UploadViewController/BeaUploadViewController.h create mode 100644 Utilities/UploadViewController/BeaUploadViewController.m create mode 100644 Utilities/UploadViewController/StatusView/BeaStatusView.h create mode 100644 Utilities/UploadViewController/StatusView/BeaStatusView.m create mode 100755 layout/Library/Application Support/Bea.bundle/BeFake.png diff --git a/.gitignore b/.gitignore index 30f486c..a6b5be2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ .theos/ packages/ .DS_Store -Versions/ \ No newline at end of file +Versions/ +Utilities/errorCodes +notes +realmoji \ No newline at end of file diff --git a/Makefile b/Makefile index c2b3f97..cf7f030 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ TARGET := iphone:clang:latest:14.0 INSTALL_TARGET_PROCESSES = BeReal ARCHS = arm64 arm64e FINALPACKAGE = 1 -PACKAGE_VERSION = 1.1.2 +PACKAGE_VERSION = 1.2 export SYSROOT = $(THEOS)/sdks/iPhoneOS15.5.sdk @@ -12,6 +12,15 @@ TWEAK_NAME = Bea Bea_FILES = Tweak/Tweak.x Bea_CFLAGS = -fobjc-arc -Bea_FRAMEWORKS = UIKit +Bea_LDFLAGS += -ObjC +Bea_FRAMEWORKS = UIKit MapKit + +ifeq ($(JAILED), 1) +Bea_CFLAGS += -D JAILED=1 +endif + +ifeq ($(LEGACY_SUPPORT), 1) +Bea_CFLAGS += -D LEGACY_SUPPORT=1 +endif include $(THEOS_MAKE_PATH)/tweak.mk diff --git a/README.md b/README.md index f6223b9..9c6b51d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,30 @@ # Bea - Bea adds small enhancements to the BeReal. app. - -## Installation - 1. Add https://havoc.app to your package manager. - 2. Install Bea + Bea is an iOS tweak that adds enhancements to the BeReal. app. ## Compatibility -All iOS devices running iOS 14 or later. +Compatible with all iOS devices running iOS 14 or later. + +## Features + - View BeReals without posting your own + - Upload BeReals from the camera roll: + - Post on time, even if you're posting late + - Set a custom location + - Set a custom retake count + - Download BeReals + - Bypass screenshot detection ## Build +
+ To build Bea for the purpose of injecting it into an IPA and installing it through sideloading, set the JAILED flag to true.
+ - Clone this repository using `git clone https://github.com/yandevelop/Bea` - `cd Bea` - Run `make package` +## Installation + 1. Add https://havoc.app to your package manager. + 2. Install Bea + ## License You may not copy, modify, sublicense or distribute the source code or any packages from it. diff --git a/Tweak/Tweak.h b/Tweak/Tweak.h index 77638ff..910b663 100644 --- a/Tweak/Tweak.h +++ b/Tweak/Tweak.h @@ -1,5 +1,9 @@ #import #import #import "../Utilities/BeaUtilities.m" +#import "../Utilities/UploadViewController/BeaUploadViewController.m" +#import -BOOL isUnblurred = NO; \ No newline at end of file +BOOL isUnblurred = NO; + +NSString *authorizationKey = nil; \ No newline at end of file diff --git a/Tweak/Tweak.x b/Tweak/Tweak.x index 05f4544..0f5c26e 100644 --- a/Tweak/Tweak.x +++ b/Tweak/Tweak.x @@ -1,12 +1,17 @@ #import "Tweak.h" + %hook DoublePhotoView - (void)layoutSubviews { %orig; UIView *doublePhotoView = (UIView *)self; + if ([doublePhotoView.subviews.lastObject isKindOfClass:[BeaButton class]] || doublePhotoView.frame.size.width < 180) return; + // make the view accept touches (dragging photos etc) + doublePhotoView.superview.userInteractionEnabled = YES; + UIResponder *responder = self; while (responder && ![responder isKindOfClass:[UIViewController class]]) { responder = [responder nextResponder]; @@ -27,26 +32,32 @@ %hook UIAlertController - (void)viewDidLoad { - UIAlertController *alertController = (UIAlertController *)self; %orig; + if (isUnblurred) return; + + UIAlertController *alertController = (UIAlertController *)self; - if (![alertController.presentingViewController isKindOfClass:objc_getClass("BeReal.NavigationController")]) return %orig; - UIAlertAction *thirdAction = alertController.actions[2]; - id block = [thirdAction valueForKey:@"_handler"]; - if (block) { - void (^handler)(UIAlertAction *) = block; - handler(thirdAction); - isUnblurred = YES; + if ([alertController.actions[2].title isEqual:@"👀 Unblur"]) { + UIAlertAction *thirdAction = alertController.actions[2]; + id block = [thirdAction valueForKey:@"_handler"]; + if (block) { + void (^handler)(UIAlertAction *) = block; + handler(thirdAction); + } } } - (void)viewWillAppear:(id)arg1 { %orig; - if ([self.presentingViewController isKindOfClass:objc_getClass("BeReal.NavigationController")] || [self.presentingViewController isKindOfClass:[UINavigationController class]]) { + if (isUnblurred) return; + UIAlertController *alertController = (UIAlertController *)self; + if ([alertController.actions[2].title isEqual:@"👀 Unblur"]) { // Set the whole view to hidden self.view.superview.hidden = YES; + + isUnblurred = YES; - // Dismiss the UIAlertController automatically + // Dismiss the UIAlertController [self dismissViewControllerAnimated:NO completion:nil]; } } @@ -55,40 +66,63 @@ %hook HomeViewController - (void)viewDidLoad { %orig; - UIViewController *homeViewController = (UIViewController *)self; - if (!isUnblurred && [homeViewController respondsToSelector:@selector(openDebugMenu)] && [homeViewController.childViewControllers.lastObject isKindOfClass:objc_getClass("BeReal.SUIFeedViewController")]) { - [homeViewController performSelector:@selector(openDebugMenu)]; + UIViewController *homeViewController = (UIViewController *)self; + + if (!isUnblurred && [homeViewController respondsToSelector:@selector(openDebugMenu)] && [homeViewController.childViewControllers.lastObject isKindOfClass:objc_getClass("BeReal.SUIFeedViewController")]) { + [homeViewController performSelector:@selector(openDebugMenu)]; + #ifndef LEGACY_SUPPORT + NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + NSComparisonResult result = [version compare:@"1.1.2" options:NSNumericSearch]; + if (result == NSOrderedAscending) { + BeaAlertView *alertView = [[BeaAlertView alloc] init]; + [homeViewController.view addSubview:alertView]; + } + #endif + } - NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; - NSComparisonResult result = [version compare:@"1.1.2" options:NSNumericSearch]; - if (result == NSOrderedAscending) { - BeaAlertView *alertView = [[BeaAlertView alloc] init]; - [homeViewController.view addSubview:alertView]; - } - } + UIImageView *beRealLogoView = [self valueForKey:@"ibNavBarLogoImageView"]; + beRealLogoView.userInteractionEnabled = YES; + + #ifdef JAILED + NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"Bea" ofType:@"bundle"]; + NSBundle *bundle = [NSBundle bundleWithPath:bundlePath]; + UIImage *beFakeLogo = [UIImage imageWithContentsOfFile:[bundle pathForResource:@"BeFake" ofType:@"png"]]; + #else + NSBundle *bundle = [NSBundle bundleWithPath:ROOT_PATH_NS(@"/Library/Application Support/Bea.bundle")]; + UIImage *beFakeLogo = [UIImage imageNamed:@"BeFake.png" inBundle:bundle compatibleWithTraitCollection:nil]; + #endif + + CGSize targetSize = beRealLogoView.image.size; + + UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0); + [beFakeLogo drawInRect:CGRectMake(0, 0, targetSize.width, targetSize.height)]; + UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + beRealLogoView.image = resizedImage; + + UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; + [beRealLogoView addGestureRecognizer:tapGestureRecognizer]; } -%end -%hook SwiftView -- (void)layoutSubviews { - %orig; - UIView *s = (UIView *)self; - // removes the eye view and the post late button from the blurred view - for (UIView *v in s.subviews) { - if (v.frame.size.width <= 48 && v.frame.size.width > 32) { - v.hidden = YES; - } - if (([v isKindOfClass:objc_getClass("SwiftUI._UIGraphicsView")] || [v isKindOfClass:[UIView class]]) && v.frame.size.width > 350 && v.subviews.count == 0) { - v.hidden = YES; - } - } +%new +- (void)handleTap:(UITapGestureRecognizer *)gestureRecognizer { + + UIViewController *vc = (UIViewController *)self; + // display the error view here + if (!authorizationKey) return; + BeaUploadViewController *beaUploadViewController = [[BeaUploadViewController alloc] initWithAuthorization:authorizationKey]; + beaUploadViewController.modalPresentationStyle = UIModalPresentationFullScreen; + [vc presentViewController:beaUploadViewController animated:YES completion:nil]; } %end + %hook CALayer - (void)setFilters:(NSArray *)filter { - return; + if (!isUnblurred) return; + %orig; } %end @@ -100,7 +134,7 @@ UITableView *labelView = vc.view.subviews.firstObject; UILabel *headerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, labelView.frame.size.width, 50)]; - headerLabel.text = @"Bea 1.1.2\nmade with ❤️ by yan"; + headerLabel.text = @"Bea 1.2\nmade with ❤️ by yan"; headerLabel.numberOfLines = 0; headerLabel.font = [UIFont fontWithName:@"Inter" size:10]; headerLabel.textAlignment = NSTextAlignmentCenter; @@ -135,14 +169,36 @@ } %end +%hook NSMutableURLRequest +-(void)setAllHTTPHeaderFields:(NSDictionary *)arg1 { + %orig; + if ([[arg1 allKeys] containsObject:@"Authorization"] && !authorizationKey) { + authorizationKey = arg1[@"Authorization"]; + } +} +%end + +%hook UIHostingView +- (void)layoutSubviews { + %orig; + UIView *s = (UIView *)self; + for (UIView *v in s.superview.subviews) { + if ((v.frame.size.width <= 48 && v.frame.size.width > 32) || (([v isKindOfClass:objc_getClass("SwiftUI._UIGraphicsView")] || [v isKindOfClass:[UIView class]]) && v.frame.size.width > 350 && v.subviews.count == 0)) { + v.hidden = YES; + } + } +} +%end %ctor { - %init(HomeViewController = objc_getClass("_TtC6BeReal18HomeViewController"), - DoublePhotoView = objc_getClass("RealComponents.DoublePhotoView"), - SettingsViewController = objc_getClass("_TtC6BeReal22SettingsViewController"), - SwiftView = objc_getClass("_TtCC7SwiftUI17HostingScrollView22PlatformGroupContainer")); + #ifdef LEGACY_SUPPORT + Class photoView = objc_getClass("BeReal.DoublePhotoView"); + #else + Class photoView = objc_getClass("RealComponents.DoublePhotoView"); + #endif - // Enable dualCamera? - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"debug_dualCameraPreviewEnabled"]; - [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"debug_developerModeEnabled"]; + %init(HomeViewController = objc_getClass("_TtC6BeReal18HomeViewController"), + DoublePhotoView = photoView, + SettingsViewController = objc_getClass("_TtC6BeReal22SettingsViewController"), + UIHostingView = objc_getClass("_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697116_UIInheritedView")); } \ No newline at end of file diff --git a/Utilities/BeaUtilities.m b/Utilities/BeaUtilities.m index 7833618..e135a00 100644 --- a/Utilities/BeaUtilities.m +++ b/Utilities/BeaUtilities.m @@ -5,8 +5,15 @@ + (void)downloadImage:(id)sender { UIButton *button = (UIButton *)sender; UIView *tableContentView = button.superview.superview; UIImageView *imageView = nil; + + #ifdef LEGACY_SUPPORT + NSString *viewClass = @"BeReal.DoublePhotoView"; + #else + NSString *viewClass = @"RealComponents.DoublePhotoView"; + #endif + for (UIView *view in tableContentView.subviews) { - if ([NSStringFromClass([view class]) isEqualToString:@"RealComponents.DoublePhotoView"]) { + if ([NSStringFromClass([view class]) isEqualToString:viewClass]) { imageView = view.subviews.firstObject; break; } @@ -125,7 +132,7 @@ - (void)setupAlertView { } - (void)updateButtonTapped { - NSString *appStoreLink = @"https://apps.apple.com/de/app/id1459645446"; + NSString *appStoreLink = @"https://apps.apple.com/app/id1459645446"; NSURL *appStoreURL = [NSURL URLWithString:appStoreLink]; if ([[UIApplication sharedApplication] canOpenURL:appStoreURL]) { diff --git a/Utilities/InfoViewController/BeaInfoViewController.h b/Utilities/InfoViewController/BeaInfoViewController.h new file mode 100644 index 0000000..41b1a46 --- /dev/null +++ b/Utilities/InfoViewController/BeaInfoViewController.h @@ -0,0 +1,7 @@ +@interface BeaInfoViewController : UIViewController +@property (nonatomic, strong) UIImageView *profileImageView; +@property (nonatomic, strong) UILabel *twitterLabel; +@property (nonatomic, strong) UILabel *smallLabel; +@property (nonatomic, strong) UIView *wrapperView; +@property (nonatomic, strong) UILabel *versionLabel; +@end \ No newline at end of file diff --git a/Utilities/InfoViewController/BeaInfoViewController.m b/Utilities/InfoViewController/BeaInfoViewController.m new file mode 100644 index 0000000..a66ae45 --- /dev/null +++ b/Utilities/InfoViewController/BeaInfoViewController.m @@ -0,0 +1,85 @@ +#import "BeaInfoViewController.h" + +@implementation BeaInfoViewController +- (void)viewDidLoad { + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor clearColor]; + + self.wrapperView = [[UIView alloc] init]; + self.wrapperView.translatesAutoresizingMaskIntoConstraints = NO; + self.wrapperView.userInteractionEnabled = YES; + UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwitterTap)]; + [self.wrapperView addGestureRecognizer:tapGestureRecognizer]; + + self.profileImageView = [[UIImageView alloc] init]; + self.profileImageView.contentMode = UIViewContentModeScaleAspectFit; + self.profileImageView.layer.cornerRadius = 25; + self.profileImageView.layer.masksToBounds = YES; + self.profileImageView.translatesAutoresizingMaskIntoConstraints = NO; + + NSData *profileImageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://avatars.githubusercontent.com/u/82288425?v=4"]]; + UIImage *profileImage = [UIImage imageWithData:profileImageData]; + self.profileImageView.image = profileImage; + [self.wrapperView addSubview:self.profileImageView]; + + self.smallLabel = [[UILabel alloc] init]; + self.smallLabel.textAlignment = NSTextAlignmentCenter; + self.smallLabel.textColor = [UIColor whiteColor]; + self.smallLabel.text = @"developed by"; + self.smallLabel.font = [UIFont fontWithName:@"Inter" size:10]; + self.smallLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.wrapperView addSubview:self.smallLabel]; + + self.twitterLabel = [[UILabel alloc] init]; + self.twitterLabel.textAlignment = NSTextAlignmentCenter; + self.twitterLabel.textColor = [UIColor whiteColor]; + self.twitterLabel.font = [UIFont fontWithName:@"Inter" size:18]; + self.twitterLabel.text = @"yandevelop"; + self.twitterLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.wrapperView addSubview:self.twitterLabel]; + + self.versionLabel = [[UILabel alloc] init]; + self.versionLabel.textAlignment = NSTextAlignmentCenter; + self.versionLabel.textColor = [UIColor whiteColor]; + self.versionLabel.font = [UIFont fontWithName:@"Inter" size:9]; + self.versionLabel.text = @"Bea\nVersion 1.2"; + self.versionLabel.numberOfLines = 0; + self.versionLabel.lineBreakMode = NSLineBreakByWordWrapping; + self.versionLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.versionLabel]; + + UIVisualEffectView *blurEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleRegular]]; + blurEffectView.frame = self.view.bounds; + blurEffectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view addSubview:blurEffectView]; + [self.view sendSubviewToBack:blurEffectView]; + + [self.view addSubview:self.wrapperView]; + + [NSLayoutConstraint activateConstraints:@[ + [self.wrapperView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor], + [self.wrapperView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor], + [self.wrapperView.widthAnchor constraintEqualToConstant:125], + [self.wrapperView.heightAnchor constraintEqualToConstant:100], + + [self.profileImageView.centerXAnchor constraintEqualToAnchor:self.wrapperView.centerXAnchor], + [self.profileImageView.widthAnchor constraintEqualToConstant:50], + [self.profileImageView.heightAnchor constraintEqualToConstant:50], + + [self.smallLabel.centerXAnchor constraintEqualToAnchor:self.wrapperView.centerXAnchor], + [self.smallLabel.topAnchor constraintEqualToAnchor:self.profileImageView.bottomAnchor constant:10], + + [self.twitterLabel.centerXAnchor constraintEqualToAnchor:self.wrapperView.centerXAnchor], + [self.twitterLabel.topAnchor constraintEqualToAnchor:self.smallLabel.bottomAnchor], + + [self.versionLabel.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor], + [self.versionLabel.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor] + ]]; +} + +- (void)handleTwitterTap { + NSURL *twitterWebURL = [NSURL URLWithString:@"https://twitter.com/yandevelop"]; + [[UIApplication sharedApplication] openURL:twitterWebURL options:@{} completionHandler:nil]; +} +@end \ No newline at end of file diff --git a/Utilities/LocationViewController/BeaLocationViewController.h b/Utilities/LocationViewController/BeaLocationViewController.h new file mode 100644 index 0000000..a3caf5c --- /dev/null +++ b/Utilities/LocationViewController/BeaLocationViewController.h @@ -0,0 +1,19 @@ +#import +#import + +@protocol BeaLocationViewControllerDelegate; + +@interface BeaLocationViewController : UIViewController +@property (nonatomic, strong) MKMapView *mapView; +@property (nonatomic, strong) UIButton *doneButton; +@property (nonatomic, weak) id delegate; +@property (nonatomic, strong) UIButton *userLocationButton; +@property (nonatomic, assign) BOOL userLocationEnabled; +@property (nonatomic, assign) double latitude; +@property (nonatomic, assign) double longitude; +@property (nonatomic, strong) CLLocationManager *locationManager; +@end + +@protocol BeaLocationViewControllerDelegate +- (void)locationViewController:(BeaLocationViewController *)viewController didSelectLocationWithLatitude:(CLLocationDegrees)latitude longitude:(CLLocationDegrees)longitude; +@end \ No newline at end of file diff --git a/Utilities/LocationViewController/BeaLocationViewController.m b/Utilities/LocationViewController/BeaLocationViewController.m new file mode 100644 index 0000000..6ee5b1a --- /dev/null +++ b/Utilities/LocationViewController/BeaLocationViewController.m @@ -0,0 +1,120 @@ +#import "BeaLocationViewController.h" + +@implementation BeaLocationViewController +- (void)viewDidLoad { + [super viewDidLoad]; + + self.mapView = [[MKMapView alloc] initWithFrame:self.view.frame]; + self.mapView.delegate = self; + [self.view addSubview:self.mapView]; + + self.locationManager = [CLLocationManager performSelector:@selector(sharedManager)]; + //self.locationManager = [[CLLocationManager alloc] init]; + self.locationManager.delegate = self; + + self.doneButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.doneButton setTitle:@"Done" forState:UIControlStateNormal]; + [self.doneButton addTarget:self action:@selector(doneButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + [self.doneButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + self.doneButton.backgroundColor = [UIColor whiteColor]; + self.doneButton.layer.cornerRadius = 8.0; + self.doneButton.titleLabel.font = [UIFont fontWithName:@"Inter" size:17]; + self.doneButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.doneButton]; + + self.userLocationButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.userLocationButton setImage:[UIImage systemImageNamed:@"location"] forState:UIControlStateNormal]; + self.userLocationButton.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.6]; + self.userLocationButton.layer.cornerRadius = 8.0; + self.userLocationButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.userLocationButton addTarget:self action:@selector(locationButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.userLocationButton]; + + UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; + [self.mapView addGestureRecognizer:gestureRecognizer]; + + [NSLayoutConstraint activateConstraints:@[ + [self.doneButton.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20.0], + [self.doneButton.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20.0], + [self.doneButton.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor constant:-10.0], + [self.doneButton.heightAnchor constraintEqualToConstant:44.0], + + [self.userLocationButton.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:20.0], + [self.userLocationButton.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20.0], + [self.userLocationButton.widthAnchor constraintEqualToConstant:44.0], + [self.userLocationButton.heightAnchor constraintEqualToConstant:44.0], + ]]; +} + +- (void)setUserLocationEnabledState:(BOOL)arg1 { + self.userLocationEnabled = arg1; + + // if the user dropped a pin, remove it + if (self.mapView.annotations.firstObject) { + [self.mapView removeAnnotations:self.mapView.annotations]; + } + + if (self.userLocationEnabled) { + [UIView animateWithDuration:0.3 animations:^{ + [self.userLocationButton setImage:[UIImage systemImageNamed:@"location.fill"] forState:UIControlStateNormal]; + }]; + [self.locationManager requestWhenInUseAuthorization]; + [self.locationManager startUpdatingLocation]; + self.mapView.showsUserLocation = YES; + } else { + [UIView animateWithDuration:0.3 animations:^{ + [self.userLocationButton setImage:[UIImage systemImageNamed:@"location"] forState:UIControlStateNormal]; + }]; + [self.locationManager stopUpdatingLocation]; + self.mapView.showsUserLocation = NO; + + self.longitude = 0.0; + self.latitude = 0.0; + } +} + +- (void)locationButtonTapped { + [self setUserLocationEnabledState:!self.userLocationEnabled]; +} + +- (void)handleTap:(UITapGestureRecognizer *)gestureRecognizer { + if (self.userLocationEnabled) [self setUserLocationEnabledState:NO]; + + if (gestureRecognizer.state == UIGestureRecognizerStateEnded) { + CGPoint touchPoint = [gestureRecognizer locationInView:self.mapView]; + CLLocationCoordinate2D coordinate = [self.mapView convertPoint:touchPoint toCoordinateFromView:self.mapView]; + + MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init]; + annotation.coordinate = coordinate; + [self.mapView removeAnnotations:self.mapView.annotations]; + [self.mapView addAnnotation:annotation]; + + self.latitude = coordinate.latitude; + self.longitude = coordinate.longitude; + } +} + +- (void)doneButtonTapped { + if ([self.delegate respondsToSelector:@selector(locationViewController:didSelectLocationWithLatitude:longitude:)]) { + [self.delegate locationViewController:self didSelectLocationWithLatitude:self.latitude longitude:self.longitude]; + } + [self.locationManager stopUpdatingLocation]; + [self dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - CLLocationManagerDelegate + +- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { + CLLocation *location = locations.lastObject; + self.longitude = location.coordinate.longitude; + self.latitude = location.coordinate.latitude; +} + +#pragma mark - MKMapViewDelegate + +// update the maps view when user location updated +- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation { + MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(userLocation.coordinate, 1000, 1000); + [mapView setRegion:region animated:YES]; +} +@end \ No newline at end of file diff --git a/Utilities/UploadTask/BeaUploadTask.h b/Utilities/UploadTask/BeaUploadTask.h new file mode 100644 index 0000000..44119d2 --- /dev/null +++ b/Utilities/UploadTask/BeaUploadTask.h @@ -0,0 +1,15 @@ +@interface BeaUploadTask : NSObject +- (instancetype)initWithData:(NSDictionary *)data frontImage:(UIImage *)frontImage backImage:(UIImage *)backImage; +@property (nonatomic, strong) NSString *authorizationKey; +@property (nonatomic, retain) NSData *frontImageData; +@property (nonatomic, retain) NSData *backImageData; +@property (nonatomic, strong) NSDictionary *userDictionary; +@property (nonatomic, strong) NSString *takenAt; +@property (nonatomic, strong) NSString *lastMoment; +- (void)uploadBeRealWithCompletion:(void (^)(BOOL success, NSError *error))completion; +- (void)makePUTRequestWithData:(NSDictionary *)data completion:(void (^)(BOOL success, NSError *error))completion; +- (void)putPhotoWithURL:(NSURL *)url headers:(NSDictionary *)headers imageData:(NSData *)imageData completion:(void (^)(BOOL success))completion; +- (void)postBeRealWithFrontPath:(NSString *)frontPath backPath:(NSString *)backPath frontBucket:(NSString *)frontBucket backBucket:(NSString *)backBucket completion:(void (^)(BOOL success, NSError *error))completion; +- (void)getLastMoment; +- (void)handleErrorWithTitle:(NSString *)title message:(NSString *)message completion:(void (^)(BOOL success, NSError *error))completion; +@end \ No newline at end of file diff --git a/Utilities/UploadTask/BeaUploadTask.m b/Utilities/UploadTask/BeaUploadTask.m new file mode 100644 index 0000000..46af81e --- /dev/null +++ b/Utilities/UploadTask/BeaUploadTask.m @@ -0,0 +1,243 @@ +#import "BeaUploadTask.h" + +@implementation BeaUploadTask +NSData* compressImage(UIImage *image, NSUInteger targetDataSize) { + CGFloat compressionFactor = 1.0; + NSData *imageData = UIImageJPEGRepresentation(image, compressionFactor); + + // if the current data length is below the target's size return the image + if (imageData.length < targetDataSize) { + return imageData; + } + + while (imageData.length > targetDataSize && compressionFactor > 0.0) { + compressionFactor -= 0.1; + imageData = UIImageJPEGRepresentation(image, compressionFactor); + } + + return imageData; +} + +- (UIImage *)resizeImage:(UIImage *)image toSize:(CGSize)size { + UIGraphicsBeginImageContextWithOptions(size, NO, image.scale); + [image drawInRect:CGRectMake(0, 0, size.width, size.height)]; + UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return resizedImage; +} + +- (instancetype)initWithData:(NSDictionary *)data frontImage:(UIImage *)frontImage backImage:(UIImage *)backImage { + self = [super init]; + if (self) { + self.userDictionary = data; + self.authorizationKey = data[@"authorization"]; + + UIImage *resizedFrontImage = [self resizeImage:frontImage toSize:CGSizeMake(1500, 2000)]; + UIImage *resizedBackImage = [self resizeImage:backImage toSize:CGSizeMake(1500, 2000)]; + + self.frontImageData = compressImage(resizedFrontImage, 1048576); + self.backImageData = compressImage(resizedBackImage, 1048576); + } + return self; +} + +- (void)handleErrorWithTitle:(NSString *)title message:(NSString *)message completion:(void (^)(BOOL success, NSError *error))completion { + NSError *error = [NSError errorWithDomain:@"com.yan.bea" code:0 userInfo:@{ @"title":title, @"description":message }]; + completion(NO, error); +} + +- (void)uploadBeRealWithCompletion:(void (^)(BOOL success, NSError *error))completion { + + [self getLastMoment]; + + // create the first request + NSURL *uploadRequestURL = [NSURL URLWithString:@"https://mobile.bereal.com/api/content/posts/upload-url?mimeType=image/webp"]; + NSMutableURLRequest *uploadRequest = [NSMutableURLRequest requestWithURL:uploadRequestURL]; + [uploadRequest setHTTPMethod:@"GET"]; + [uploadRequest setValue:self.authorizationKey forHTTPHeaderField:@"Authorization"]; + [uploadRequest setValue:@"*/*" forHTTPHeaderField:@"Accept"]; + [uploadRequest setValue:@"iOS" forHTTPHeaderField:@"bereal-platform"]; + [uploadRequest setValue:@"14.7.1" forHTTPHeaderField:@"bereal-os-version"]; + [uploadRequest setValue:@"en-US;q=1.0" forHTTPHeaderField:@"Accept-Language"]; + [uploadRequest setValue:@"BeReal/0.28.2 (AlexisBarreyat.BeReal; build:8425; iOS 14.7.1) 1.0.0/BRApiKit" forHTTPHeaderField:@"User-Agent"]; + [uploadRequest setValue:@"en-US" forHTTPHeaderField:@"bereal-app-language"]; + [uploadRequest setValue:@"en" forHTTPHeaderField:@"bereal-device-language"]; + + NSURLSession *session = [NSURLSession sharedSession]; + NSURLSessionDataTask *uploadRequestTask = [session dataTaskWithRequest:uploadRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *getError) { + NSDictionary *uploadRequestResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + if (uploadRequestResponse[@"error"] || getError) { + [self handleErrorWithTitle:@"Something went wrong..." message:@"0 - Bea could not initiate the upload process" completion:completion]; + } else { + [self makePUTRequestWithData:uploadRequestResponse completion:completion]; + } + }]; + + [uploadRequestTask resume]; + +} + +- (void)makePUTRequestWithData:(NSDictionary *)response completion:(void (^)(BOOL success, NSError *error))completion { + if (!response) return; + + NSString *frontCameraURLString = response[@"data"][0][@"url"]; + NSString *backCameraURLString = response[@"data"][1][@"url"]; + + NSURL *frontCameraURL = [NSURL URLWithString:frontCameraURLString]; + NSURL *backCameraURL = [NSURL URLWithString:backCameraURLString]; + + // those headers have to be included in the next put request + NSDictionary *frontHeaders = response[@"data"][0][@"headers"]; + NSDictionary *backHeaders = response[@"data"][1][@"headers"]; + + NSString *frontImageUploadPath = response[@"data"][0][@"path"]; + NSString *backImageUploadPath = response[@"data"][1][@"path"]; + + NSString *frontImageBucket = response[@"data"][0][@"bucket"]; + NSString *backImageBucket = response[@"data"][1][@"bucket"]; + + // otherwise the postbereal function would get called even if one of the put requests didnt succeed + dispatch_group_t group = dispatch_group_create(); + dispatch_group_enter(group); + [self putPhotoWithURL:frontCameraURL headers:frontHeaders imageData:self.frontImageData completion:^(BOOL success) { + if (!success) { + return; + } + dispatch_group_leave(group); + }]; + + dispatch_group_enter(group); + [self putPhotoWithURL:backCameraURL headers:backHeaders imageData:self.backImageData completion:^(BOOL success) { + if (!success) { + return; + } + dispatch_group_leave(group); + }]; + + + dispatch_group_notify(group, dispatch_get_main_queue(), ^{ + [self postBeRealWithFrontPath:frontImageUploadPath backPath:backImageUploadPath frontBucket:frontImageBucket backBucket:backImageBucket completion:completion]; + }); +} + +- (void)putPhotoWithURL:(NSURL *)url headers:(NSDictionary *)headers imageData:(NSData *)imageData completion:(void (^)(BOOL success))completion { + + NSMutableURLRequest *putRequest = [NSMutableURLRequest requestWithURL:url]; + [putRequest setHTTPMethod:@"PUT"]; + [putRequest setAllHTTPHeaderFields:headers]; + + NSURLSessionTask *task = [[NSURLSession sharedSession] uploadTaskWithRequest:putRequest fromData:imageData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (error || httpResponse.statusCode > 299) { + completion(NO); + return; + } + + if (data) { + completion(YES); + } + }]; + + [task resume]; +} + +- (void)postBeRealWithFrontPath:(NSString *)frontPath backPath:(NSString *)backPath frontBucket:(NSString *)frontBucket backBucket:(NSString *)backBucket completion:(void (^)(BOOL success, NSError *error))completion { + NSDate *currentDate = [NSDate date]; + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSXXX"]; + [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + + if ([self.userDictionary[@"isLate"] boolValue]) { + self.takenAt = [dateFormatter stringFromDate:currentDate]; + } else { + // randomize the taken at to be between the startDate and endDate because its + // logically impossible to "post" on the start time + NSDate *moment = [dateFormatter dateFromString:self.lastMoment]; + NSInteger randomSeconds = arc4random_uniform(105 - 60) + 60; + NSDate *dateInRange = [moment dateByAddingTimeInterval:randomSeconds]; + NSString *dateString = [dateFormatter stringFromDate:dateInRange]; + self.takenAt = dateString; + } + + NSMutableDictionary *payload = [NSMutableDictionary dictionaryWithDictionary:@{ + @"visibility": @[@"friends"], + @"isLate": @([self.userDictionary[@"isLate"] boolValue]), + @"retakeCounter": self.userDictionary[@"retakeCounter"] ?: @0, + @"takenAt": self.takenAt, + @"backCamera": @{ + @"bucket": backBucket, + @"height": @1500, + @"width": @2000, + @"path": backPath + }, + @"frontCamera": @{ + @"bucket": frontBucket, + @"height": @1500, + @"width": @2000, + @"path": frontPath + } + }]; + + if (self.userDictionary[@"longitude"] && self.userDictionary[@"latitude"]) { + NSDictionary *locationDict = @{ + @"latitude": self.userDictionary[@"latitude"], + @"longitude": self.userDictionary[@"longitude"] + }; + [payload setObject:locationDict forKey:@"location"]; + } + + if (self.userDictionary[@"caption"]) { + [payload setObject:self.userDictionary[@"caption"] forKey:@"caption"]; + } + + NSData *payloadJSON = [NSJSONSerialization dataWithJSONObject:payload options:NSJSONWritingWithoutEscapingSlashes error:nil]; + + NSURL *postBeRealURL = [NSURL URLWithString:@"https://mobile.bereal.com/api/content/posts"]; + NSMutableURLRequest *postBeRealRequest = [NSMutableURLRequest requestWithURL:postBeRealURL]; + + [postBeRealRequest setHTTPMethod:@"POST"]; + [postBeRealRequest setValue:@"application/json" forHTTPHeaderField:@"content-type"]; + [postBeRealRequest setValue:@"*/*" forHTTPHeaderField:@"Accept"]; + [postBeRealRequest setValue:self.authorizationKey forHTTPHeaderField:@"Authorization"]; + [postBeRealRequest setValue:@"en-US" forHTTPHeaderField:@"bereal-app-language"]; + + + NSURLSessionUploadTask *uploadTask = [[NSURLSession sharedSession] uploadTaskWithRequest:postBeRealRequest fromData:payloadJSON completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (error || httpResponse.statusCode > 299) { + NSDictionary *responseDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSString *message = [NSString stringWithFormat:@"1 - Uploading failed: %@: %@", responseDictionary[@"statusCode"], responseDictionary[@"errorKey"]]; + [self handleErrorWithTitle:@"API Error" message:message completion:completion]; + return; + } + + if (data) { + // the upload succeded + completion(YES, nil); + } + }]; + + [uploadTask resume]; +} + +- (void)getLastMoment { + NSURL *lastMomentURL = [NSURL URLWithString:@"https://mobile.bereal.com/api/bereal/moments/last/"]; + + NSMutableURLRequest *lastMomentRequest = [NSMutableURLRequest requestWithURL:lastMomentURL]; + [lastMomentRequest setHTTPMethod:@"GET"]; + [lastMomentRequest setValue:self.authorizationKey forHTTPHeaderField:@"Authorization"]; + [lastMomentRequest setValue:@"*/*" forHTTPHeaderField:@"Accept"]; + + NSURLSession *session = [NSURLSession sharedSession]; + NSURLSessionDataTask *lastMomentRequestTask = [session dataTaskWithRequest:lastMomentRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (error || httpResponse.statusCode != 200) { + return; + } else { + NSDictionary *lastMomentResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + self.lastMoment = lastMomentResponse[@"startDate"]; + } + }]; + [lastMomentRequestTask resume]; +} +@end \ No newline at end of file diff --git a/Utilities/UploadViewController/BeaUploadViewController.h b/Utilities/UploadViewController/BeaUploadViewController.h new file mode 100644 index 0000000..d58fa2b --- /dev/null +++ b/Utilities/UploadViewController/BeaUploadViewController.h @@ -0,0 +1,35 @@ +#import +#import +#import "StatusView/BeaStatusView.m" +#import "../InfoViewController/BeaInfoViewController.m" +#import "../UploadTask/BeaUploadTask.m" +#import "../LocationViewController/BeaLocationViewController.m" + +@interface BeaUploadViewController : UIViewController +@property (nonatomic, strong) BeaLocationViewController *locationVC; +@property (nonatomic, strong) NSString *authorizationKey; +@property (nonatomic, strong) UIImageView *frontImageView; +@property (nonatomic, strong) UIImageView *backImageView; +@property (nonatomic, strong) UILabel *frontTextLabel; +@property (nonatomic, strong) UILabel *backTextLabel; +@property (nonatomic, strong) UIImage *frontImage; +@property (nonatomic, strong) UIImage *backImage; +@property (nonatomic, strong) UITextField *captionTextField; +@property (nonatomic, strong) NSString *caption; +@property (nonatomic, strong) UITextField *retakeTextField; +@property (nonatomic, strong) NSNumber *retakeCount; +@property (nonatomic, strong) UIButton *actionButton; +@property (nonatomic, strong) BeaStatusView *statusView; +@property (nonatomic, strong) UIButton *locationButton; +@property (nonatomic, strong) UILabel *locationLabel; +@property (nonatomic, assign) double latitude; +@property (nonatomic, assign) double longitude; +@property (nonatomic, strong) UIImageView *titleImageView; +@property (nonatomic, strong) UIButton *backButton; +@property (nonatomic, strong) UIImageView *backButtonImageView; +@property (nonatomic, strong) UISwitch *isLateSwitch; +@property (nonatomic, strong) UILabel *isLateLabel; +@property (nonatomic, assign) BOOL isLate; +@property (nonatomic, strong) UIButton *infoButton; +@property (nonatomic, strong) UIImageView *infoButtonImageView; +@end \ No newline at end of file diff --git a/Utilities/UploadViewController/BeaUploadViewController.m b/Utilities/UploadViewController/BeaUploadViewController.m new file mode 100644 index 0000000..c67dadc --- /dev/null +++ b/Utilities/UploadViewController/BeaUploadViewController.m @@ -0,0 +1,537 @@ +#import "BeaUploadViewController.h" + +@implementation BeaUploadViewController + +- (instancetype)initWithAuthorization:(NSString *)authKey { + self = [super init]; + if (self) { + if (!authKey) return nil; + self.authorizationKey = authKey; + self.locationVC = [[BeaLocationViewController alloc] init]; + self.locationVC.delegate = self; + } + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + #ifdef JAILED + NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"Bea" ofType:@"bundle"]; + NSBundle *bundle = [NSBundle bundleWithPath:bundlePath]; + UIImage *beFakeLogo = [UIImage imageWithContentsOfFile:[bundle pathForResource:@"BeFake" ofType:@"png"]]; + #else + NSBundle *bundle = [NSBundle bundleWithPath:ROOT_PATH_NS(@"/Library/Application Support/Bea.bundle")]; + UIImage *beFakeLogo = [UIImage imageNamed:@"BeFake.png" inBundle:bundle compatibleWithTraitCollection:nil]; + #endif + + self.view.backgroundColor = [UIColor blackColor]; + + self.titleImageView = [[UIImageView alloc] initWithImage:beFakeLogo]; + self.titleImageView.contentMode = UIViewContentModeScaleAspectFit; + self.titleImageView.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.titleImageView]; + + self.backButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.backButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.backButton addTarget:self action:@selector(dismissViewController) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.backButton]; + + UIImage *backButtonImage = [[UIImage systemImageNamed:@"chevron.down"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + self.backButtonImageView = [[UIImageView alloc] initWithImage:backButtonImage]; + self.backButtonImageView.tintColor = [UIColor whiteColor]; + self.backButtonImageView.translatesAutoresizingMaskIntoConstraints = NO; + [self.backButton addSubview:self.backButtonImageView]; + + self.infoButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.infoButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.infoButton addTarget:self action:@selector(infoButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.infoButton]; + + UIImage *infoButtonImage = [[UIImage systemImageNamed:@"info.circle.fill"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + self.infoButtonImageView = [[UIImageView alloc] initWithImage:infoButtonImage]; + self.infoButtonImageView.tintColor = [UIColor whiteColor]; + self.infoButtonImageView.translatesAutoresizingMaskIntoConstraints = NO; + [self.infoButton addSubview:self.infoButtonImageView]; + + self.statusView = [[BeaStatusView alloc] initWithFrame:CGRectZero]; + [self.view addSubview:self.statusView]; + self.statusView.translatesAutoresizingMaskIntoConstraints = NO; + + self.frontImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; + self.frontImageView.backgroundColor = [UIColor blackColor]; + self.frontImageView.layer.borderWidth = 1.8; + self.frontImageView.layer.cornerRadius = 8.0; + self.frontImageView.layer.masksToBounds = YES; + self.frontImageView.layer.borderColor = [UIColor whiteColor].CGColor; + self.frontImageView.userInteractionEnabled = YES; + self.frontImageView.contentMode = UIViewContentModeScaleAspectFill; + self.frontImageView.translatesAutoresizingMaskIntoConstraints = NO; + + UITapGestureRecognizer *frontTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageViewTapped:)]; + [self.frontImageView addGestureRecognizer:frontTapRecognizer]; + + [self.view addSubview:self.frontImageView]; + + self.frontTextLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.frontTextLabel.font = [UIFont fontWithName:@"Inter" size:14]; + self.frontTextLabel.text = @"Front image"; + self.frontTextLabel.textAlignment = NSTextAlignmentCenter; + self.frontTextLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.frontImageView addSubview:self.frontTextLabel]; + + self.backImageView = [[UIImageView alloc] initWithFrame:CGRectZero]; + self.backImageView.backgroundColor = [UIColor blackColor]; + self.backImageView.layer.borderWidth = 1.8; + self.backImageView.layer.cornerRadius = 8.0; + self.backImageView.layer.masksToBounds = YES; + self.backImageView.layer.borderColor = [UIColor whiteColor].CGColor; + self.backImageView.userInteractionEnabled = YES; + self.backImageView.contentMode = UIViewContentModeScaleAspectFill; + self.backImageView.translatesAutoresizingMaskIntoConstraints = NO; + + UITapGestureRecognizer *backTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageViewTapped:)]; + [self.backImageView addGestureRecognizer:backTapRecognizer]; + [self.view addSubview:self.backImageView]; + + self.backTextLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.backTextLabel.font = [UIFont fontWithName:@"Inter" size:14]; + self.backTextLabel.text = @"Back image"; + self.backTextLabel.textAlignment = NSTextAlignmentCenter; + self.backTextLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.backImageView addSubview:self.backTextLabel]; + + self.captionTextField = [[UITextField alloc] initWithFrame:CGRectZero]; + self.captionTextField.placeholder = @"Caption"; + self.captionTextField.font = [UIFont fontWithName:@"Inter" size:13]; + self.captionTextField.backgroundColor = [UIColor blackColor]; + self.captionTextField.layer.cornerRadius = 8.0; + self.captionTextField.layer.borderWidth = 1.2; + self.captionTextField.layer.borderColor = [UIColor whiteColor].CGColor; + self.captionTextField.translatesAutoresizingMaskIntoConstraints = NO; + self.captionTextField.delegate = self; + + self.captionTextField.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 12, 40)]; + self.captionTextField.leftViewMode = UITextFieldViewModeAlways; + + [self.captionTextField addTarget:self action:@selector(captionTextFieldDidChange:) forControlEvents:UIControlEventEditingDidEnd]; + [self.view addSubview: self.captionTextField]; + + self.retakeTextField = [[UITextField alloc] initWithFrame:CGRectZero]; + self.retakeTextField.keyboardType = UIKeyboardTypeNumberPad; + self.retakeTextField.placeholder = @"Retakes"; + self.retakeTextField.font = [UIFont fontWithName:@"Inter" size:13]; + self.retakeTextField.backgroundColor = [UIColor blackColor]; + self.retakeTextField.layer.cornerRadius = 8.0; + self.retakeTextField.layer.borderWidth = 1.2; + self.retakeTextField.layer.borderColor = [UIColor whiteColor].CGColor; + self.retakeTextField.translatesAutoresizingMaskIntoConstraints = NO; + self.retakeTextField.delegate = self; + + self.retakeTextField.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 12, 40)]; + self.retakeTextField.leftViewMode = UITextFieldViewModeAlways; + + [self.retakeTextField addTarget:self action:@selector(retakeTextFieldDidChange:) forControlEvents:UIControlEventEditingDidEnd]; + [self.view addSubview: self.retakeTextField]; + + self.actionButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.actionButton setTitle:@"Send" forState:UIControlStateNormal]; + [self.actionButton addTarget:self action:@selector(sendBeReal) forControlEvents:UIControlEventTouchUpInside]; + [self.actionButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + self.actionButton.backgroundColor = [UIColor whiteColor]; + self.actionButton.titleLabel.font = [UIFont fontWithName:@"Inter" size:17]; + self.actionButton.layer.cornerRadius = 8.0; + self.actionButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.actionButton]; + + self.locationLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.locationLabel.font = [UIFont fontWithName:@"Inter" size:22]; + self.locationLabel.text = @"Location"; + self.locationLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.locationLabel]; + + self.locationButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.locationButton.frame = CGRectMake(0, 0, 32, 32); + self.locationButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.locationButton addTarget:self action:@selector(locationButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.locationButton]; + + UIImage *image = [[UIImage systemImageNamed:@"mappin.circle"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; + imageView.tintColor = [UIColor whiteColor]; + imageView.frame = self.locationButton.bounds; + [self.locationButton addSubview:imageView]; + + self.isLateSwitch = [[UISwitch alloc] init]; + self.isLateSwitch.translatesAutoresizingMaskIntoConstraints = NO; + [self.isLateSwitch addTarget:self action:@selector(isLateStateChanged:) forControlEvents:UIControlEventValueChanged]; + [self.isLateSwitch setOn:NO animated:NO]; + [self.view addSubview:self.isLateSwitch]; + + self.isLateLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.isLateLabel.font = [UIFont fontWithName:@"Inter" size:22]; + self.isLateLabel.text = @"Post late"; + self.isLateLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.isLateLabel]; + + [NSLayoutConstraint activateConstraints:@[ + [self.frontImageView.centerXAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:2 + self.view.frame.size.width / 4], + [self.frontImageView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:120], + [self.frontImageView.widthAnchor constraintEqualToConstant:150], + [self.frontImageView.heightAnchor constraintEqualToConstant:200], + [self.frontTextLabel.centerXAnchor constraintEqualToAnchor:self.frontImageView.centerXAnchor], + [self.frontTextLabel.centerYAnchor constraintEqualToAnchor:self.frontImageView.centerYAnchor], + + [self.backImageView.centerXAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-2 - self.view.frame.size.width / 4], + [self.backImageView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:120], + [self.backImageView.widthAnchor constraintEqualToConstant:150], + [self.backImageView.heightAnchor constraintEqualToConstant:200], + [self.backTextLabel.centerXAnchor constraintEqualToAnchor:self.backImageView.centerXAnchor], + [self.backTextLabel.centerYAnchor constraintEqualToAnchor:self.backImageView.centerYAnchor], + + [self.captionTextField.topAnchor constraintEqualToAnchor:self.frontImageView.bottomAnchor constant:20], + [self.captionTextField.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:22], + [self.captionTextField.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-22], + [self.captionTextField.heightAnchor constraintEqualToConstant:40], + + [self.retakeTextField.topAnchor constraintEqualToAnchor:self.captionTextField.bottomAnchor constant:20], + [self.retakeTextField.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:22], + [self.retakeTextField.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-22], + [self.retakeTextField.heightAnchor constraintEqualToConstant:40], + + [self.isLateSwitch.topAnchor constraintEqualToAnchor:self.retakeTextField.bottomAnchor constant:30], + [self.isLateSwitch.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-22], + + [self.isLateLabel.centerYAnchor constraintEqualToAnchor:self.isLateSwitch.centerYAnchor], + [self.isLateLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:22], + + [self.locationButton.topAnchor constraintEqualToAnchor:self.isLateSwitch.bottomAnchor constant:20], + [self.locationButton.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-22], + [self.locationButton.widthAnchor constraintEqualToConstant:32], + [self.locationButton.heightAnchor constraintEqualToAnchor:self.locationButton.widthAnchor], + + [self.locationLabel.centerYAnchor constraintEqualToAnchor:self.locationButton.centerYAnchor], + [self.locationLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:22], + [self.locationLabel.trailingAnchor constraintEqualToAnchor:self.locationButton.leadingAnchor], + + [self.actionButton.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20], + [self.actionButton.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20], + [self.actionButton.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor constant:-20], + [self.actionButton.heightAnchor constraintEqualToConstant:44], + + [self.statusView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20], + [self.statusView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20], + [self.statusView.bottomAnchor constraintEqualToAnchor:self.actionButton.topAnchor constant: -20], + + [self.titleImageView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:12], + [self.titleImageView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor], + [self.titleImageView.heightAnchor constraintEqualToConstant:18], + [self.titleImageView.widthAnchor constraintEqualToConstant:84], + + [self.backButton.centerYAnchor constraintEqualToAnchor:self.titleImageView.centerYAnchor], + [self.backButton.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20], + [self.backButton.widthAnchor constraintEqualToConstant:40], + [self.backButton.heightAnchor constraintEqualToConstant:40], + + [self.backButtonImageView.centerYAnchor constraintEqualToAnchor:self.backButton.centerYAnchor], + [self.backButtonImageView.widthAnchor constraintEqualToConstant:20], + [self.backButtonImageView.heightAnchor constraintEqualToConstant:20], + + [self.infoButton.centerYAnchor constraintEqualToAnchor:self.titleImageView.centerYAnchor], + [self.infoButton.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20], + [self.infoButton.widthAnchor constraintEqualToConstant:40], + [self.infoButton.heightAnchor constraintEqualToConstant:40], + + [self.infoButtonImageView.centerYAnchor constraintEqualToAnchor:self.infoButton.centerYAnchor], + [self.infoButtonImageView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20], + [self.infoButtonImageView.widthAnchor constraintEqualToConstant:20], + [self.infoButtonImageView.heightAnchor constraintEqualToConstant:20] + ]]; +} + +- (void)infoButtonTapped { + BeaInfoViewController *infoViewController = [[BeaInfoViewController alloc] init]; + [self presentViewController:infoViewController animated:YES completion:nil]; +} + +- (void)isLateStateChanged:(UISwitch *)sender { + if (sender.isOn) { + self.isLate = YES; + } else { + self.isLate = NO; + } +} + +- (void)dismissViewController { + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)locationButtonTapped { + [self presentViewController:self.locationVC animated:YES completion:nil]; +} + +- (void)locationViewController:(BeaLocationViewController *)viewController didSelectLocationWithLatitude:(CLLocationDegrees)latitude longitude:(CLLocationDegrees)longitude { + self.latitude = latitude; + self.longitude = longitude; + + if (latitude == 0.0 && longitude == 0.0) { + self.locationLabel.text = @"Location"; + return; + } + + self.locationLabel.text = @"Loading..."; + + CLLocation *location = [[CLLocation alloc] initWithLatitude:self.latitude longitude:self.longitude]; + CLGeocoder *geocoder = [[CLGeocoder alloc] init]; + [geocoder reverseGeocodeLocation:location completionHandler:^(NSArray * _Nullable placemarks, NSError * _Nullable error) { + if (error) { + self.locationLabel.text = @"An error occured"; + return; + } + + if (placemarks.count > 0) { + CLPlacemark *placemark = placemarks.firstObject; + if (placemark.locality && placemark.country) { + self.locationLabel.text = [NSString stringWithFormat:@"%@, %@", placemark.locality, placemark.ISOcountryCode]; + return; + } + } + self.locationLabel.text = @"City not found"; + }]; +} + +- (void)showErrorWithTitle:(NSString *)title message:(NSString *)message { + self.statusView.titleLabel.text = title; + self.statusView.messageLabel.text = message; + self.statusView.backgroundColor = [UIColor colorWithRed: 0.95 green: 0.15 blue: 0.07 alpha: 1.00]; + self.statusView.imageView.image = self.statusView.image; + + [UIView animateWithDuration:0.3 animations:^{ + self.statusView.alpha = 1.0; + }]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 6 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [UIView animateWithDuration:0.3 animations:^{ + self.statusView.alpha = 0.0; + }]; + }); +} + +- (void)imageViewTapped:(UITapGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer.state == UIGestureRecognizerStateEnded) { + UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init]; + imagePicker.delegate = self; + UIImageView *tapped = (UIImageView *)gestureRecognizer.view; + if (tapped == self.frontImageView) { + imagePicker.view.tag = 1; + } else if (tapped == self.backImageView) { + imagePicker.view.tag = 2; + } + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Choose an Image Source" message:@"Select the desired image source for your content." preferredStyle:UIAlertControllerStyleActionSheet]; + + UIAlertAction *cameraAction = [UIAlertAction actionWithTitle:@"Open Camera" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera; + if (tapped == self.frontImageView) { + imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceFront; + } else { + imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceRear; + } + [self presentViewController:imagePicker animated:YES completion:nil]; + }]; + + UIImage *cameraImage = [UIImage systemImageNamed:@"camera" withConfiguration:[UIImageSymbolConfiguration configurationWithPointSize:19]]; + [cameraAction setValue:cameraImage forKey:@"image"]; + + UIAlertAction *photoAction = [UIAlertAction actionWithTitle:@"Choose from Library" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { + imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + [self presentViewController:imagePicker animated:YES completion:nil]; + }]; + + UIImage *photoImage = [UIImage systemImageNamed:@"photo" withConfiguration:[UIImageSymbolConfiguration configurationWithPointSize:19]]; + [photoAction setValue:photoImage forKey:@"image"]; + + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]; + + [alertController addAction:cameraAction]; + [alertController addAction:photoAction]; + [alertController addAction:cancelAction]; + + [self presentViewController:alertController animated:YES completion:nil]; + } +} + +- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { + UIImage *pickedImage = info[UIImagePickerControllerOriginalImage]; + + // check if the source type is camera or photo library and then check + // if the picked photo is for the front image or back image view and assign it to it + if (picker.sourceType == UIImagePickerControllerSourceTypeCamera) { + if (picker.view.tag == 1) { + UIImage *mirrored = [UIImage imageWithCGImage:pickedImage.CGImage scale:pickedImage.scale orientation:UIImageOrientationLeftMirrored]; + self.frontImage = mirrored; + self.frontImageView.image = self.frontImage; + } else if (picker.view.tag == 2) { + self.backImage = pickedImage; + self.backImageView.image = self.backImage; + } + } else if (picker.sourceType == UIImagePickerControllerSourceTypePhotoLibrary) { + if (picker.view.tag == 1) { + self.frontImage = pickedImage; + self.frontImageView.image = self.frontImage; + } else if (picker.view.tag == 2) { + self.backImage = pickedImage; + self.backImageView.image = self.backImage; + } + } + + [picker dismissViewControllerAnimated:YES completion:nil]; +} + +// methods for the text field +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + [textField resignFirstResponder]; + return YES; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + [self.view endEditing:YES]; +} + +// method for the caption text field +- (void)captionTextFieldDidChange:(UITextField *)textField { + self.caption = textField.text; +} + +// method for the retake text field +- (void)retakeTextFieldDidChange:(UITextField *)textField { + NSCharacterSet *nonDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet]; + BOOL containsNonDigits = [textField.text rangeOfCharacterFromSet:nonDigits].location != NSNotFound; + + if (!containsNonDigits) { + NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; + NSNumber *number = [numberFormatter numberFromString:textField.text]; + self.retakeCount = number; + textField.layer.borderColor = [UIColor whiteColor].CGColor; + } else { + textField.layer.borderColor = [UIColor redColor].CGColor; + CALayer *layer = textField.layer; + + CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"]; + animation.duration = 0.3; + animation.repeatCount = 1; + animation.values = @[ + [NSValue valueWithCGPoint:CGPointMake(layer.position.x - 10, layer.position.y)], + [NSValue valueWithCGPoint:CGPointMake(layer.position.x + 10, layer.position.y)], + [NSValue valueWithCGPoint:CGPointMake(layer.position.x - 10, layer.position.y)], + [NSValue valueWithCGPoint:CGPointMake(layer.position.x + 10, layer.position.y)], + [NSValue valueWithCGPoint:CGPointMake(layer.position.x, layer.position.y)] + ]; + + [layer addAnimation:animation forKey:@"shake"]; + } +} + +- (void)sendBeReal { + if (!self.frontImage || !self.backImage) { + [self showErrorWithTitle:@"Missing images" message:@"Select all required images."]; + return; + } + self.actionButton.enabled = NO; + [self.actionButton setTitle:@"" forState:UIControlStateNormal]; + + UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; + spinner.center = self.actionButton.center; + [self.view addSubview:spinner]; + [spinner startAnimating]; + + [UIView animateWithDuration:0.3 animations:^{ + self.actionButton.alpha = 0.4; + }]; + + NSDictionary *userData = [self createDataDictionary]; + + + // because of processing the images the spinner lags a bit + BeaUploadTask *task = [[BeaUploadTask alloc] initWithData:userData frontImage:self.frontImage backImage:self.backImage]; + [task uploadBeRealWithCompletion:^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [spinner stopAnimating]; + [spinner removeFromSuperview]; + self.actionButton.enabled = YES; + [self.actionButton setTitle:@"Send" forState:UIControlStateNormal]; + [UIView animateWithDuration:0.3 animations:^{ + self.actionButton.alpha = 1.0; + }]; + + if (success) { + [self uploadDidSucceed]; + } else { + [self showErrorWithTitle:error.userInfo[@"title"] message:error.userInfo[@"description"]]; + } + }); + }]; +} + +- (void)uploadDidSucceed { + self.statusView.backgroundColor = [UIColor colorWithRed:76.0/255.0 green:178.0/255.0 blue:80.0/255.0 alpha:1.0]; + self.statusView.titleLabel.text = @"Success 🎉"; + self.statusView.messageLabel.text = @"Your BeReal was uploaded successfully!"; + + UIImage *checkmarkImage = [UIImage systemImageNamed:@"checkmark.circle"]; + + self.statusView.imageView.image = checkmarkImage; + + [UIView animateWithDuration:0.3 animations:^{ + self.statusView.alpha = 1.0; + self.frontTextLabel.alpha = 1.0; + self.backTextLabel.alpha = 1.0; + }]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 7 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [UIView animateWithDuration:0.3 animations:^{ + self.statusView.alpha = 0.0; + }]; + }); + + // reset our view and properties to initial state + self.frontImageView.image = nil; + self.backImageView.image = nil; + self.frontImage = nil; + self.backImage = nil; + self.caption = nil; + self.captionTextField.text = nil; + self.retakeCount = nil; + self.retakeTextField.text = nil; + self.longitude = 0.0; + self.latitude = 0.0; +} + +- (NSDictionary *)createDataDictionary { + NSMutableDictionary *data = [NSMutableDictionary dictionary]; + + if (!self.authorizationKey) { + [self showErrorWithTitle:@"Something went wrong" message:@"2 - Please restart the app and try again."]; + return nil; + } + + [data setObject:self.authorizationKey forKey:@"authorization"]; + [data setValue:@(self.isLate) forKey:@"isLate"]; + + if (self.caption) { + [data setObject:self.caption forKey:@"caption"]; + } + if (self.retakeCount) { + [data setObject:self.retakeCount forKey:@"retakeCounter"]; + } + if ((self.latitude != 0.0) && (self.longitude != 0.0)) { + NSNumber *longitudeNumber = @(self.longitude); + NSNumber *latitudeNumber = @(self.latitude); + + [data setObject:longitudeNumber forKey:@"longitude"]; + [data setObject:latitudeNumber forKey:@"latitude"]; + } + + + return [data copy]; +} +@end \ No newline at end of file diff --git a/Utilities/UploadViewController/StatusView/BeaStatusView.h b/Utilities/UploadViewController/StatusView/BeaStatusView.h new file mode 100644 index 0000000..62bbb4d --- /dev/null +++ b/Utilities/UploadViewController/StatusView/BeaStatusView.h @@ -0,0 +1,6 @@ +@interface BeaStatusView : UIView +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *messageLabel; +@property (nonatomic, strong) UIImage *image; +@property (nonatomic, strong) UIImageView *imageView; +@end diff --git a/Utilities/UploadViewController/StatusView/BeaStatusView.m b/Utilities/UploadViewController/StatusView/BeaStatusView.m new file mode 100644 index 0000000..53341a0 --- /dev/null +++ b/Utilities/UploadViewController/StatusView/BeaStatusView.m @@ -0,0 +1,56 @@ +#import "BeaStatusView.h" + +@implementation BeaStatusView +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.alpha = 0.0; + + self.layer.cornerRadius = 8.0; + self.layer.masksToBounds = YES; + + self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.titleLabel.font = [UIFont fontWithName:@"Inter" size:20.0]; + self.titleLabel.textColor = [UIColor whiteColor]; + + self.messageLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.messageLabel.font = [UIFont fontWithName:@"Inter" size:11.0]; + self.messageLabel.numberOfLines = 2; + self.messageLabel.lineBreakMode = NSLineBreakByWordWrapping; + self.messageLabel.textColor = [UIColor whiteColor]; + + [self addSubview:self.titleLabel]; + [self addSubview:self.messageLabel]; + + self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO; + self.messageLabel.translatesAutoresizingMaskIntoConstraints = NO; + + self.image = [UIImage systemImageNamed:@"exclamationmark.triangle"]; + self.imageView = [[UIImageView alloc] initWithImage:self.image]; + self.imageView.contentMode = UIViewContentModeScaleAspectFit; + self.imageView.tintColor = [UIColor whiteColor]; + self.imageView.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:self.imageView]; + + [NSLayoutConstraint activateConstraints:@[ + [self.heightAnchor constraintEqualToConstant:64.0], + + [self.titleLabel.topAnchor constraintEqualToAnchor:self.topAnchor constant:8], + [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:66], + [self.titleLabel.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-8], + [self.titleLabel.heightAnchor constraintEqualToConstant:24], + + [self.messageLabel.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:-2], + [self.messageLabel.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:66], + [self.messageLabel.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-8], + [self.messageLabel.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-8], + + [self.imageView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:12.0], + [self.imageView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor constant:-2], + [self.imageView.widthAnchor constraintEqualToConstant:38.0], + [self.imageView.heightAnchor constraintEqualToConstant:38.0] + ]]; + } + return self; +} +@end \ No newline at end of file diff --git a/control b/control index 0c91688..d401012 100644 --- a/control +++ b/control @@ -1,6 +1,6 @@ Package: com.yan.bea Name: Bea -Version: 1.1.2 +Version: 1.2 Architecture: iphoneos-arm Description: Lightweight BeReal. enhancement tweak. Maintainer: yan diff --git a/layout/Library/Application Support/Bea.bundle/BeFake.png b/layout/Library/Application Support/Bea.bundle/BeFake.png new file mode 100755 index 0000000000000000000000000000000000000000..e44ae28d75810a658fbe5c0d678f04fe9633c58a GIT binary patch literal 7696 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 w4mTM@oO!tN%d*V59$Y0(O