mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-05-31 20:51:35 +02:00
2977873932
- Profile Analyzer (beta) — follower/following scans with mutuals, non-followbacks, new/lost trackers, and profile change history; searchable lists with batch follow/unfollow - Theme settings — force dark mode, Full OLED, OLED chat theme, and keyboard theme picker - Confirm story like - Confirm story emoji reaction - Swipe down to dismiss media viewer - Manually add users to story/chat exclusion lists by username - Keep stories visually seen locally - Auto-scroll reels mode - Quality picker: audio-only and raw photo download rows - Clear cache button with optional auto-clear interval - Spanish, Russian, Korean, Arabic, and Chinese (Traditional) translations - About page with version, credits, and links - Release notes popup on first launch of a new version - Anonymous live viewing - Toggle live comments - Disappearing DM media overlay — action button, mark-as-viewed eye, and audio toggle - Hide RyukGram UI on screenshots, screen recordings, and mirroring - Open link from clipboard — long-press the search tab - Messages-only mode: optional "Hide tab bar" sub-toggle - Fake profile stats — verified badge and follower/following/post counts on your own profile - Language switcher + import/export localization from Debug - Reveal poll/slider vote counts and quiz answers on stories and reels before interacting - Force legacy Quiz sticker back into the story composer tray - Advanced experimental features menu — toggle hidden IG experiments (QuickSnap, Homecoming, Prism, Direct Notes reply types) with apply-on-restart batching and a crash-loop auto-reset - Shortcut to Advanced experimental features from the General experimental features section - Push notifications render with rich previews on sideload again - IG 426 compatibility across story audio toggle, like confirmation, seen-on-like, live comments, notes audio download - Call confirm split into separate voice-call and video-call toggles - Messages-only mode: tab swiping disabled - Settings quick-access broken in non-English languages - Story seen-receipt block restored on IG v426 - Block selected mode no longer marks listed stories as seen - Hide explore posts grid works again on recent IG versions - Hide suggested stories no longer breaks profile highlights - Hide trending searches now also hides the category chip bar - Story eye long-press menu opens next to the button - Disable video autoplay: tap-to-play now works on videos inside carousels - Disable vanish mode swipe fixed on IG 426 - "Confirm shh mode" renamed to "Confirm vanish mode" across all languages - Confirm sticker interaction split into separate story and highlight toggles - Shared link embed presets: added eeinstagram.com and vxinstagram.com - Downloaded media filenames follow `@username_context_timestamp` - Reels pause mode: optional tap-to-mute on photo reels - Backup & Restore — scope picker with live preview for Settings / Excluded lists / Analyzer data - Profile Analyzer: filter by Not verified - Settings header: tap to open a sheet with GitHub and Telegram channel links - Thanks to Furamako for the Spanish translation - Thanks to [ZomkaDEV](https://github.com/ZomkaDEV) for the Russian translation - Thanks to [@ch1tmdgus](https://github.com/ch1tmdgus) (N4C) for the Korean translation - Thanks to [@bruuhim](https://github.com/bruuhim) for the Arabic translation - Thanks to [@jaydenjcpy](https://github.com/jaydenjcpy) for the Chinese (Traditional) translation - Thanks to [@darthplagueiswise](https://github.com/darthplagueiswise) (Radan) for the experimental flag feature set - Thanks to [@asdfzxcvbn](https://github.com/asdfzxcvbn) for [zxPluginsInject](https://github.com/asdfzxcvbn/zxPluginsInject) and [ipapatch](https://github.com/asdfzxcvbn/ipapatch) - Preserved unsent messages can't be removed via "Delete for you"; pull-to-refresh clears them (warning available in settings) - "Delete for you" detection uses a ~2s window after the local action — a real unsend landing in that window may be missed (rare) - With Liquid Glass buttons + Hide UI on capture both on, the DM eye leaves an empty glass bubble in captures
446 lines
18 KiB
Objective-C
446 lines
18 KiB
Objective-C
#import "Utils.h"
|
|
#import "PhotoAlbum.h"
|
|
#import "Settings/TweakSettings.h"
|
|
|
|
@implementation SCIUtils
|
|
|
|
+ (BOOL)getBoolPref:(NSString *)key {
|
|
if (![key length] || [[NSUserDefaults standardUserDefaults] objectForKey:key] == nil) return false;
|
|
|
|
return [[NSUserDefaults standardUserDefaults] boolForKey:key];
|
|
}
|
|
+ (double)getDoublePref:(NSString *)key {
|
|
if (![key length] || [[NSUserDefaults standardUserDefaults] objectForKey:key] == nil) return 0;
|
|
|
|
return [[NSUserDefaults standardUserDefaults] doubleForKey:key];
|
|
}
|
|
+ (NSString *)getStringPref:(NSString *)key {
|
|
if (![key length] || [[NSUserDefaults standardUserDefaults] objectForKey:key] == nil) return @"";
|
|
|
|
return [[NSUserDefaults standardUserDefaults] stringForKey:key];
|
|
}
|
|
|
|
static NSDictionary *sciRegisteredDefaultsRef = nil;
|
|
|
|
+ (NSDictionary<NSString *, id> *)sciRegisteredDefaults { return sciRegisteredDefaultsRef ?: @{}; }
|
|
+ (void)setSciRegisteredDefaults:(NSDictionary<NSString *, id> *)defaults {
|
|
sciRegisteredDefaultsRef = [defaults copy];
|
|
}
|
|
|
|
+ (_Bool)liquidGlassEnabledBool:(_Bool)fallback {
|
|
BOOL setting = [SCIUtils getBoolPref:@"liquid_glass_surfaces"];
|
|
return setting ? true : fallback;
|
|
}
|
|
|
|
// Displaying View Controllers
|
|
+ (void)showQuickLookVC:(NSArray<id> *)items {
|
|
UIViewController *topVC = topMostController();
|
|
if (!topVC) {
|
|
NSLog(@"[SCInsta] No view controller available to present QuickLook");
|
|
return;
|
|
}
|
|
|
|
QLPreviewController *previewController = [[QLPreviewController alloc] init];
|
|
QuickLookDelegate *quickLookDelegate = [[QuickLookDelegate alloc] initWithPreviewItemURLs:items];
|
|
|
|
previewController.dataSource = quickLookDelegate;
|
|
|
|
[topVC presentViewController:previewController animated:true completion:nil];
|
|
}
|
|
+ (void)showShareVC:(id)item {
|
|
UIViewController *topVC = topMostController();
|
|
if (!topVC) {
|
|
NSLog(@"[SCInsta] No view controller available to present share sheet");
|
|
return;
|
|
}
|
|
|
|
UIActivityViewController *acVC = [[UIActivityViewController alloc] initWithActivityItems:@[item] applicationActivities:nil];
|
|
if (is_iPad()) {
|
|
acVC.popoverPresentationController.sourceView = topVC.view;
|
|
acVC.popoverPresentationController.sourceRect = CGRectMake(topVC.view.bounds.size.width / 2.0, topVC.view.bounds.size.height / 2.0, 1.0, 1.0);
|
|
}
|
|
|
|
// If the user picks "Save to Photos" from the share sheet, route the new
|
|
// asset into the RyukGram album via a one-shot photo library observer.
|
|
if ([self getBoolPref:@"save_to_ryukgram_album"]) {
|
|
[SCIPhotoAlbum watchForNextSavedAsset];
|
|
}
|
|
|
|
[topVC presentViewController:acVC animated:true completion:nil];
|
|
}
|
|
+ (void)showSettingsVC:(UIWindow *)window {
|
|
UIViewController *rootController = [window rootViewController];
|
|
SCISettingsViewController *settingsViewController = [SCISettingsViewController new];
|
|
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:settingsViewController];
|
|
if ([SCIUtils getBoolPref:@"settings_pause_playback"])
|
|
navigationController.modalPresentationStyle = UIModalPresentationFullScreen;
|
|
|
|
[rootController presentViewController:navigationController animated:YES completion:nil];
|
|
}
|
|
|
|
// Open settings and push straight into a named top-level entry (e.g. "Messages").
|
|
+ (void)showSettingsVC:(UIWindow *)window atTopLevelEntry:(NSString *)entryTitle {
|
|
UIViewController *rootController = [window rootViewController];
|
|
while (rootController.presentedViewController) rootController = rootController.presentedViewController;
|
|
SCISettingsViewController *root = [SCISettingsViewController new];
|
|
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:root];
|
|
if ([SCIUtils getBoolPref:@"settings_pause_playback"])
|
|
nav.modalPresentationStyle = UIModalPresentationFullScreen;
|
|
|
|
NSArray *targetNavSections = nil;
|
|
for (NSDictionary *section in [SCITweakSettings sections]) {
|
|
for (SCISetting *row in section[@"rows"]) {
|
|
if (row.type == SCITableCellNavigation && [row.title isEqualToString:entryTitle]) {
|
|
targetNavSections = row.navSections;
|
|
break;
|
|
}
|
|
}
|
|
if (targetNavSections) break;
|
|
}
|
|
|
|
if (targetNavSections) {
|
|
SCISettingsViewController *child = [[SCISettingsViewController alloc]
|
|
initWithTitle:entryTitle sections:targetNavSections reduceMargin:NO];
|
|
child.title = entryTitle;
|
|
[nav pushViewController:child animated:NO];
|
|
}
|
|
|
|
[rootController presentViewController:nav animated:YES completion:nil];
|
|
}
|
|
|
|
// Colours
|
|
+ (UIColor *)SCIColor_Primary {
|
|
return [UIColor colorWithRed:0/255.0 green:152/255.0 blue:254/255.0 alpha:1];
|
|
};
|
|
|
|
// Errors
|
|
+ (NSError *)errorWithDescription:(NSString *)errorDesc {
|
|
return [self errorWithDescription:errorDesc code:1];
|
|
}
|
|
+ (NSError *)errorWithDescription:(NSString *)errorDesc code:(NSInteger)errorCode {
|
|
NSError *error = [ NSError errorWithDomain:@"com.socuul.scinsta" code:errorCode userInfo:@{ NSLocalizedDescriptionKey: errorDesc } ];
|
|
return error;
|
|
}
|
|
|
|
+ (JGProgressHUD *)showErrorHUDWithDescription:(NSString *)errorDesc {
|
|
return [self showErrorHUDWithDescription:errorDesc dismissAfterDelay:4.0];
|
|
}
|
|
+ (JGProgressHUD *)showErrorHUDWithDescription:(NSString *)errorDesc dismissAfterDelay:(CGFloat)dismissDelay {
|
|
JGProgressHUD *hud = [[JGProgressHUD alloc] init];
|
|
hud.textLabel.text = errorDesc;
|
|
hud.indicatorView = [[JGProgressHUDErrorIndicatorView alloc] init];
|
|
|
|
UIView *hudView = topMostController().view;
|
|
if (!hudView) hudView = [UIApplication sharedApplication].keyWindow;
|
|
if (hudView) {
|
|
[hud showInView:hudView];
|
|
[hud dismissAfterDelay:dismissDelay];
|
|
} else {
|
|
NSLog(@"[SCInsta] No valid view for error HUD: %@", errorDesc);
|
|
}
|
|
|
|
return hud;
|
|
}
|
|
|
|
// Media
|
|
|
|
// fieldCache fallback — reads the Pando-backed dict directly for when
|
|
// IG's exposed property accessors break between versions.
|
|
static id sciFieldCacheValue(id obj, NSString *key) {
|
|
if (!obj || !key.length) return nil;
|
|
Ivar iv = NULL;
|
|
for (Class c = [obj class]; c && !iv; c = class_getSuperclass(c))
|
|
iv = class_getInstanceVariable(c, "_fieldCache");
|
|
if (!iv) return nil;
|
|
@try {
|
|
NSDictionary *dict = object_getIvar(obj, iv);
|
|
if (![dict isKindOfClass:[NSDictionary class]]) return nil;
|
|
return dict[key];
|
|
} @catch (__unused id e) { return nil; }
|
|
}
|
|
|
|
+ (NSURL *)getPhotoUrl:(IGPhoto *)photo {
|
|
if (!photo) return nil;
|
|
@try {
|
|
if ([photo respondsToSelector:@selector(imageURLForWidth:)]) {
|
|
NSURL *url = [photo imageURLForWidth:100000.00];
|
|
if (url) return url;
|
|
}
|
|
} @catch (__unused NSException *e) {}
|
|
return nil;
|
|
}
|
|
|
|
+ (NSURL *)getPhotoUrlForMedia:(IGMedia *)media {
|
|
if (!media) return nil;
|
|
|
|
// fieldCache first — IGPhoto selectors crash on newer IG builds.
|
|
@try {
|
|
NSDictionary *imageVersions = sciFieldCacheValue(media, @"image_versions2");
|
|
NSArray *candidates = [imageVersions isKindOfClass:[NSDictionary class]] ? imageVersions[@"candidates"] : nil;
|
|
if ([candidates isKindOfClass:[NSArray class]] && candidates.count) {
|
|
NSDictionary *best = nil;
|
|
NSInteger bestW = -1;
|
|
for (id c in candidates) {
|
|
if (![c isKindOfClass:[NSDictionary class]]) continue;
|
|
NSInteger w = [[c objectForKey:@"width"] integerValue];
|
|
if (w > bestW) { bestW = w; best = c; }
|
|
}
|
|
NSString *urlStr = best[@"url"] ?: [[candidates firstObject] objectForKey:@"url"];
|
|
if ([urlStr isKindOfClass:[NSString class]] && urlStr.length) {
|
|
return [NSURL URLWithString:urlStr];
|
|
}
|
|
}
|
|
} @catch (__unused NSException *e) {}
|
|
|
|
IGPhoto *photo = nil;
|
|
@try {
|
|
if ([media respondsToSelector:@selector(photo)]) photo = media.photo;
|
|
} @catch (__unused NSException *e) {}
|
|
if (photo) return [SCIUtils getPhotoUrl:photo];
|
|
return nil;
|
|
}
|
|
|
|
+ (NSURL *)getVideoUrl:(IGVideo *)video {
|
|
if (!video) return nil;
|
|
|
|
@try {
|
|
if ([video respondsToSelector:@selector(sortedVideoURLsBySize)]) {
|
|
NSArray<NSDictionary *> *sorted = [video sortedVideoURLsBySize];
|
|
NSString *urlString = [sorted.firstObject isKindOfClass:[NSDictionary class]] ? sorted.firstObject[@"url"] : nil;
|
|
if ([urlString isKindOfClass:[NSString class]] && urlString.length) return [NSURL URLWithString:urlString];
|
|
}
|
|
} @catch (__unused NSException *e) {}
|
|
|
|
@try {
|
|
if ([video respondsToSelector:@selector(allVideoURLs)]) {
|
|
id set = [video allVideoURLs];
|
|
if ([set respondsToSelector:@selector(anyObject)]) {
|
|
id obj = [set anyObject];
|
|
if ([obj isKindOfClass:[NSURL class]]) {
|
|
NSString *abs = nil;
|
|
@try { abs = [(NSURL *)obj absoluteString]; } @catch (__unused NSException *e) {}
|
|
if (abs.length && ([abs hasPrefix:@"http"] || [abs hasPrefix:@"file:"])) {
|
|
return [NSURL URLWithString:abs];
|
|
}
|
|
} else if ([obj isKindOfClass:[NSString class]]) {
|
|
NSString *s = (NSString *)obj;
|
|
if (s.length && ([s hasPrefix:@"http"] || [s hasPrefix:@"file:"])) return [NSURL URLWithString:s];
|
|
}
|
|
}
|
|
}
|
|
} @catch (__unused NSException *e) {}
|
|
return nil;
|
|
}
|
|
|
|
+ (NSURL *)getVideoUrlForMedia:(IGMedia *)media {
|
|
if (!media) return nil;
|
|
|
|
// fieldCache first — IGVideo selectors crash on newer IG builds.
|
|
@try {
|
|
NSArray *versions = sciFieldCacheValue(media, @"video_versions");
|
|
if ([versions isKindOfClass:[NSArray class]] && versions.count) {
|
|
NSDictionary *best = nil;
|
|
NSInteger bestType = -1;
|
|
for (id v in versions) {
|
|
if (![v isKindOfClass:[NSDictionary class]]) continue;
|
|
NSInteger type = [[v objectForKey:@"type"] integerValue];
|
|
if (type > bestType) { bestType = type; best = v; }
|
|
}
|
|
NSString *urlStr = best[@"url"] ?: [[versions firstObject] objectForKey:@"url"];
|
|
if ([urlStr isKindOfClass:[NSString class]] && urlStr.length) {
|
|
return [NSURL URLWithString:urlStr];
|
|
}
|
|
}
|
|
} @catch (__unused NSException *e) {}
|
|
|
|
IGVideo *video = nil;
|
|
@try {
|
|
if ([media respondsToSelector:@selector(video)]) video = media.video;
|
|
} @catch (__unused NSException *e) {}
|
|
if (video) return [SCIUtils getVideoUrl:video];
|
|
return nil;
|
|
}
|
|
|
|
// View Controllers
|
|
+ (UIViewController *)viewControllerForView:(UIView *)view {
|
|
NSString *viewDelegate = @"viewDelegate";
|
|
if ([view respondsToSelector:NSSelectorFromString(viewDelegate)]) {
|
|
return [view valueForKey:viewDelegate];
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
+ (UIViewController *)viewControllerForAncestralView:(UIView *)view {
|
|
NSString *_viewControllerForAncestor = @"_viewControllerForAncestor";
|
|
if ([view respondsToSelector:NSSelectorFromString(_viewControllerForAncestor)]) {
|
|
return [view valueForKey:_viewControllerForAncestor];
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
+ (UIViewController *)nearestViewControllerForView:(UIView *)view {
|
|
return [self viewControllerForView:view] ?: [self viewControllerForAncestralView:view];
|
|
}
|
|
|
|
// Functions
|
|
+ (NSString *)IGVersionString {
|
|
return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
|
|
};
|
|
+ (BOOL)isNotch {
|
|
return [[[UIApplication sharedApplication] keyWindow] safeAreaInsets].bottom > 0;
|
|
};
|
|
|
|
+ (BOOL)existingLongPressGestureRecognizerForView:(UIView *)view {
|
|
NSArray *allRecognizers = view.gestureRecognizers;
|
|
|
|
for (UIGestureRecognizer *recognizer in allRecognizers) {
|
|
if ([[recognizer class] isSubclassOfClass:[UILongPressGestureRecognizer class]]) {
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
// Alerts
|
|
+ (BOOL)showConfirmation:(void(^)(void))okHandler title:(NSString *)title {
|
|
UIAlertController* alert = [UIAlertController alertControllerWithTitle:title message:SCILocalized(@"Are you sure?") preferredStyle:UIAlertControllerStyleAlert];
|
|
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Yes") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
|
okHandler();
|
|
}]];
|
|
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"No!") style:UIAlertActionStyleCancel handler:nil]];
|
|
|
|
[topMostController() presentViewController:alert animated:YES completion:nil];
|
|
|
|
return nil;
|
|
};
|
|
+ (BOOL)showConfirmation:(void(^)(void))okHandler cancelHandler:(void(^)(void))cancelHandler title:(NSString *)title {
|
|
UIAlertController* alert = [UIAlertController alertControllerWithTitle:title message:SCILocalized(@"Are you sure?") preferredStyle:UIAlertControllerStyleAlert];
|
|
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Yes") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
|
okHandler();
|
|
}]];
|
|
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"No!") style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
|
|
if (cancelHandler != nil) {
|
|
cancelHandler();
|
|
}
|
|
}]];
|
|
|
|
[topMostController() presentViewController:alert animated:YES completion:nil];
|
|
|
|
return nil;
|
|
};
|
|
+ (BOOL)showConfirmation:(void(^)(void))okHandler {
|
|
return [self showConfirmation:okHandler title:nil];
|
|
};
|
|
+ (BOOL)showConfirmation:(void(^)(void))okHandler cancelHandler:(void(^)(void))cancelHandler {
|
|
return [self showConfirmation:okHandler cancelHandler:cancelHandler title:nil];
|
|
}
|
|
+ (void)showRestartConfirmation {
|
|
UIAlertController* alert = [UIAlertController alertControllerWithTitle:SCILocalized(@"Restart required") message:SCILocalized(@"You must restart the app to apply this change") preferredStyle:UIAlertControllerStyleAlert];
|
|
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Restart") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
|
exit(0);
|
|
}]];
|
|
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Later") style:UIAlertActionStyleCancel handler:nil]];
|
|
|
|
[topMostController() presentViewController:alert animated:YES completion:nil];
|
|
};
|
|
|
|
// Toasts
|
|
+ (void)showToastForDuration:(double)duration title:(NSString *)title {
|
|
[SCIUtils showToastForDuration:duration title:title subtitle:nil];
|
|
}
|
|
+ (void)showToastForDuration:(double)duration title:(NSString *)title subtitle:(NSString *)subtitle {
|
|
// Root VC
|
|
Class rootVCClass = NSClassFromString(@"IGRootViewController");
|
|
|
|
UIViewController *topMostVC = topMostController();
|
|
if (![topMostVC isKindOfClass:rootVCClass]) return;
|
|
|
|
IGRootViewController *rootVC = (IGRootViewController *)topMostVC;
|
|
|
|
// Presenter
|
|
IGActionableConfirmationToastPresenter *toastPresenter = [rootVC toastPresenter];
|
|
if (toastPresenter == nil) return;
|
|
|
|
// View Model
|
|
Class modelClass = NSClassFromString(@"IGActionableConfirmationToastViewModel");
|
|
IGActionableConfirmationToastViewModel *model = [modelClass new];
|
|
|
|
[model setValue:title forKey:@"text_annotatedTitleText"];
|
|
[model setValue:subtitle forKey:@"text_annotatedSubtitleText"];
|
|
|
|
// Show new toast, after clearing existing one
|
|
[toastPresenter hideAlert];
|
|
[toastPresenter showAlertWithViewModel:model isAnimated:true animationDuration:duration presentationPriority:0 tapActionBlock:nil presentedHandler:nil dismissedHandler:nil];
|
|
}
|
|
|
|
// Math
|
|
+ (NSUInteger)decimalPlacesInDouble:(double)value {
|
|
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
|
|
[formatter setNumberStyle:NSNumberFormatterDecimalStyle];
|
|
[formatter setMaximumFractionDigits:15]; // Allow enough digits for double precision
|
|
[formatter setMinimumFractionDigits:0];
|
|
[formatter setDecimalSeparator:@"."]; // Force dot for internal logic, then respect locale for final display if needed
|
|
|
|
NSString *stringValue = [formatter stringFromNumber:@(value)];
|
|
|
|
// Find decimal separator
|
|
NSRange decimalRange = [stringValue rangeOfString:formatter.decimalSeparator];
|
|
|
|
if (decimalRange.location == NSNotFound) {
|
|
return 0;
|
|
} else {
|
|
return stringValue.length - (decimalRange.location + decimalRange.length);
|
|
}
|
|
}
|
|
|
|
// Ivars
|
|
+ (id)getIvarForObj:(id)obj name:(const char *)name {
|
|
Ivar ivar = class_getInstanceVariable(object_getClass(obj), name);
|
|
if (!ivar) return nil;
|
|
|
|
return object_getIvar(obj, ivar);
|
|
}
|
|
+ (void)setIvarForObj:(id)obj name:(const char *)name value:(id)value {
|
|
Ivar ivar = class_getInstanceVariable(object_getClass(obj), name);
|
|
if (!ivar) return;
|
|
|
|
object_setIvarWithStrongDefault(obj, ivar, value);
|
|
}
|
|
|
|
+ (id)activeUserSession {
|
|
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
|
|
if (![scene isKindOfClass:[UIWindowScene class]]) continue;
|
|
for (UIWindow *w in ((UIWindowScene *)scene).windows) {
|
|
@try {
|
|
id s = [w valueForKey:@"userSession"];
|
|
if (s) return s;
|
|
} @catch (__unused id e) {}
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
+ (NSString *)pkFromIGUser:(id)user {
|
|
if (!user) return nil;
|
|
Ivar pkIvar = NULL;
|
|
for (Class c = [user class]; c && !pkIvar; c = class_getSuperclass(c)) {
|
|
pkIvar = class_getInstanceVariable(c, "_pk");
|
|
}
|
|
if (!pkIvar) return nil;
|
|
id pk = object_getIvar(user, pkIvar);
|
|
return pk ? [pk description] : nil;
|
|
}
|
|
|
|
+ (NSString *)currentUserPK {
|
|
id session = [self activeUserSession];
|
|
if (!session) return nil;
|
|
@try {
|
|
id user = [session valueForKey:@"user"];
|
|
return [self pkFromIGUser:user];
|
|
} @catch (__unused id e) { return nil; }
|
|
}
|
|
|
|
@end |