Files
RyukGram/src/Features/General/FakeLocationMapButton.x
T
faroukbmiled 2977873932 [release] RyukGram v1.2.2
- 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
2026-04-24 02:50:30 +01:00

280 lines
14 KiB
Plaintext

// Quick fake-location toggle injected into IG's Friends Map (DMs > Maps).
#import "../../Utils.h"
#import "../../SCIChrome.h"
#import "../../Settings/SCIFakeLocationSettingsVC.h"
#import "../../Settings/SCIFakeLocationPickerVC.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <substrate.h>
static const NSInteger kSciMapBtnTag = 0x5C1F4B;
static const NSInteger kSciMapHitBtnTag = 0x5C1F4C;
static UIViewController *sciTopMost(void) {
UIWindow *win = nil;
for (UIScene *sc in [UIApplication sharedApplication].connectedScenes) {
if (![sc isKindOfClass:[UIWindowScene class]]) continue;
for (UIWindow *w in ((UIWindowScene *)sc).windows) if (w.isKeyWindow) { win = w; break; }
if (win) break;
}
UIViewController *v = win.rootViewController;
while (v.presentedViewController) v = v.presentedViewController;
return v;
}
static void sciRefreshMapButton(UIView *mapView);
static void sciAddMapButton(UIView *mapView);
static void sciRemoveMapButton(UIView *mapView);
static UIMenu *sciBuildMapMenu(void);
static void sciWalkMapViews(UIView *root, Class mapCls, void (^block)(UIView *)) {
if (!root) return;
if (mapCls && [root isKindOfClass:mapCls]) block(root);
for (UIView *s in root.subviews) sciWalkMapViews(s, mapCls, block);
}
static void sciRefreshActiveMapButton(void) {
Class mapCls = NSClassFromString(@"IGFriendsMapCoreUI.IGFriendsMapView");
for (UIScene *sc in [UIApplication sharedApplication].connectedScenes) {
if (![sc isKindOfClass:[UIWindowScene class]]) continue;
for (UIWindow *w in ((UIWindowScene *)sc).windows) {
sciWalkMapViews(w, mapCls, ^(UIView *mv) {
if (![SCIUtils getBoolPref:@"show_fake_location_map_button"]) {
sciRemoveMapButton(mv);
} else {
sciAddMapButton(mv);
sciRefreshMapButton(mv);
}
});
}
}
}
static void sciOpenPickerForCurrent(void) {
UIViewController *top = sciTopMost();
if (!top) return;
SCIFakeLocationPickerVC *vc = [SCIFakeLocationPickerVC new];
NSUserDefaults *d = [NSUserDefaults standardUserDefaults];
vc.initialCoord = CLLocationCoordinate2DMake([[d objectForKey:@"fake_location_lat"] doubleValue],
[[d objectForKey:@"fake_location_lon"] doubleValue]);
vc.titleText = SCILocalized(@"Set location");
vc.onPick = ^(double lat, double lon, NSString *name) {
NSUserDefaults *u = [NSUserDefaults standardUserDefaults];
[u setObject:@(lat) forKey:@"fake_location_lat"];
[u setObject:@(lon) forKey:@"fake_location_lon"];
[u setObject:(name ?: @"") forKey:@"fake_location_name"];
if (![u boolForKey:@"fake_location_enabled"]) [u setBool:YES forKey:@"fake_location_enabled"];
sciRefreshActiveMapButton();
};
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
nav.modalPresentationStyle = UIModalPresentationPageSheet;
[top presentViewController:nav animated:YES completion:nil];
}
static void sciOpenPickerForNewPreset(void) {
UIViewController *top = sciTopMost();
if (!top) return;
SCIFakeLocationPickerVC *vc = [SCIFakeLocationPickerVC new];
NSUserDefaults *d = [NSUserDefaults standardUserDefaults];
vc.initialCoord = CLLocationCoordinate2DMake([[d objectForKey:@"fake_location_lat"] doubleValue],
[[d objectForKey:@"fake_location_lon"] doubleValue]);
vc.titleText = SCILocalized(@"Add preset");
vc.onPick = ^(double lat, double lon, NSString *name) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:SCILocalized(@"Save preset")
message:nil
preferredStyle:UIAlertControllerStyleAlert];
[alert addTextFieldWithConfigurationHandler:^(UITextField *tf) { tf.placeholder = SCILocalized(@"Name"); tf.text = name; }];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Save") style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) {
NSString *n = alert.textFields.firstObject.text.length ? alert.textFields.firstObject.text : name;
NSUserDefaults *u = [NSUserDefaults standardUserDefaults];
NSArray *raw = [u objectForKey:@"fake_location_presets"];
NSMutableArray *presets = [raw isKindOfClass:[NSArray class]] ? [raw mutableCopy] : [NSMutableArray array];
[presets addObject:@{@"name": n ?: @"", @"lat": @(lat), @"lon": @(lon)}];
[u setObject:presets forKey:@"fake_location_presets"];
sciRefreshActiveMapButton();
}]];
[sciTopMost() presentViewController:alert animated:YES completion:nil];
};
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
nav.modalPresentationStyle = UIModalPresentationPageSheet;
[top presentViewController:nav animated:YES completion:nil];
}
static UIMenu *sciBuildMapMenu(void) {
NSUserDefaults *d = [NSUserDefaults standardUserDefaults];
BOOL enabled = [d boolForKey:@"fake_location_enabled"];
NSString *name = [d objectForKey:@"fake_location_name"] ?: @"(unset)";
// Header section: current location (disabled), enable/disable, change location
UIAction *header = [UIAction actionWithTitle:[NSString stringWithFormat:SCILocalized(@"Current: %@"), name]
image:[UIImage systemImageNamed:@"mappin.and.ellipse"]
identifier:nil handler:^(__unused UIAction *a) {}];
header.attributes = UIMenuElementAttributesDisabled;
UIAction *toggle = [UIAction actionWithTitle:enabled ? SCILocalized(@"Disable") : SCILocalized(@"Enable")
image:[UIImage systemImageNamed:enabled ? @"location.slash.fill" : @"location.fill"]
identifier:nil
handler:^(__unused UIAction *a) {
[d setBool:!enabled forKey:@"fake_location_enabled"];
sciRefreshActiveMapButton();
}];
if (enabled) toggle.attributes = UIMenuElementAttributesDestructive;
UIAction *change = [UIAction actionWithTitle:SCILocalized(@"Change location")
image:[UIImage systemImageNamed:@"map"]
identifier:nil
handler:^(__unused UIAction *a) { sciOpenPickerForCurrent(); }];
UIMenu *headerSection = [UIMenu menuWithTitle:@"" image:nil identifier:nil
options:UIMenuOptionsDisplayInline children:@[header, toggle, change]];
// Presets + Add
NSMutableArray<UIMenuElement *> *presetItems = [NSMutableArray array];
NSArray *presets = [d objectForKey:@"fake_location_presets"];
if ([presets isKindOfClass:[NSArray class]]) {
for (NSDictionary *p in presets) {
if (![p isKindOfClass:[NSDictionary class]]) continue;
NSString *pname = p[@"name"] ?: @"Preset";
BOOL active = [p[@"name"] isEqualToString:name];
UIAction *act = [UIAction actionWithTitle:pname
image:[UIImage systemImageNamed:@"mappin.circle.fill"]
identifier:nil
handler:^(__unused UIAction *x) {
[d setObject:p[@"lat"] forKey:@"fake_location_lat"];
[d setObject:p[@"lon"] forKey:@"fake_location_lon"];
[d setObject:p[@"name"] ?: @"" forKey:@"fake_location_name"];
if (![d boolForKey:@"fake_location_enabled"]) [d setBool:YES forKey:@"fake_location_enabled"];
sciRefreshActiveMapButton();
}];
if (active) act.state = UIMenuElementStateOn;
[presetItems addObject:act];
}
}
[presetItems addObject:[UIAction actionWithTitle:SCILocalized(@"Add location")
image:[UIImage systemImageNamed:@"plus.circle.fill"]
identifier:nil
handler:^(__unused UIAction *x) { sciOpenPickerForNewPreset(); }]];
UIMenu *presetSection = [UIMenu menuWithTitle:SCILocalized(@"Saved locations") image:nil identifier:nil
options:UIMenuOptionsDisplayInline children:presetItems];
// Settings
UIAction *openSettings = [UIAction actionWithTitle:SCILocalized(@"Settings…")
image:[UIImage systemImageNamed:@"gearshape.fill"]
identifier:nil
handler:^(__unused UIAction *x) {
UIViewController *top = sciTopMost();
if (!top) return;
SCIFakeLocationSettingsVC *vc = [SCIFakeLocationSettingsVC new];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
nav.modalPresentationStyle = UIModalPresentationFormSheet;
[top presentViewController:nav animated:YES completion:nil];
}];
UIMenu *settingsSection = [UIMenu menuWithTitle:@"" image:nil identifier:nil
options:UIMenuOptionsDisplayInline children:@[openSettings]];
return [UIMenu menuWithTitle:SCILocalized(@"Fake location") image:nil identifier:nil options:0
children:@[headerSection, presetSection, settingsSection]];
}
static void sciRemoveMapButton(UIView *mapView) {
UIView *btn = [mapView viewWithTag:kSciMapBtnTag];
if (btn) [btn removeFromSuperview];
UIView *hit = [mapView viewWithTag:kSciMapHitBtnTag];
if (hit) [hit removeFromSuperview];
}
static void sciAddMapButton(UIView *mapView) {
if (!mapView) return;
if (![SCIUtils getBoolPref:@"show_fake_location_map_button"]) { sciRemoveMapButton(mapView); return; }
if ([mapView viewWithTag:kSciMapBtnTag]) return;
// Visible chrome — static, never absorbed into the menu platter animation.
BOOL on = [SCIUtils getBoolPref:@"fake_location_enabled"];
SCIChromeButton *chrome = [[SCIChromeButton alloc] initWithSymbol:on ? @"location.fill" : @"location.slash"
pointSize:18
diameter:48];
chrome.tag = kSciMapBtnTag;
chrome.bubbleColor = [UIColor secondarySystemBackgroundColor];
chrome.iconTint = on ? [UIColor systemGreenColor] : [UIColor labelColor];
chrome.layer.shadowColor = [UIColor blackColor].CGColor;
chrome.layer.shadowOpacity = 0.18;
chrome.layer.shadowRadius = 5;
chrome.layer.shadowOffset = CGSizeMake(0, 2);
chrome.userInteractionEnabled = NO;
[mapView addSubview:chrome];
[NSLayoutConstraint activateConstraints:@[
[chrome.leadingAnchor constraintEqualToAnchor:mapView.leadingAnchor constant:16],
[chrome.topAnchor constraintEqualToAnchor:mapView.safeAreaLayoutGuide.topAnchor constant:78],
[chrome.widthAnchor constraintEqualToConstant:48],
[chrome.heightAnchor constraintEqualToConstant:48],
]];
// Invisible hit target owns the menu; visible chrome below stays put
// when UIKit absorbs the hit into the menu platter on dismiss.
UIButton *hit = [UIButton buttonWithType:UIButtonTypeCustom];
hit.tag = kSciMapHitBtnTag;
hit.backgroundColor = [UIColor clearColor];
hit.translatesAutoresizingMaskIntoConstraints = NO;
hit.showsMenuAsPrimaryAction = YES;
hit.menu = sciBuildMapMenu();
[hit addAction:[UIAction actionWithHandler:^(__unused UIAction *a) {
hit.menu = sciBuildMapMenu();
}] forControlEvents:UIControlEventMenuActionTriggered];
[mapView addSubview:hit];
[NSLayoutConstraint activateConstraints:@[
[hit.leadingAnchor constraintEqualToAnchor:chrome.leadingAnchor],
[hit.trailingAnchor constraintEqualToAnchor:chrome.trailingAnchor],
[hit.topAnchor constraintEqualToAnchor:chrome.topAnchor],
[hit.bottomAnchor constraintEqualToAnchor:chrome.bottomAnchor],
]];
}
static void sciRefreshMapButton(UIView *mapView) {
SCIChromeButton *btn = (SCIChromeButton *)[mapView viewWithTag:kSciMapBtnTag];
if (![btn isKindOfClass:[SCIChromeButton class]]) return;
BOOL on = [SCIUtils getBoolPref:@"fake_location_enabled"];
btn.symbolName = on ? @"location.fill" : @"location.slash";
btn.iconTint = on ? [UIColor systemGreenColor] : [UIColor labelColor];
// Don't touch btn.menu here — reassigning mid-dismiss flickers the button.
// UIControlEventMenuActionTriggered rebuilds on next open.
}
static void (*orig_mapLayout)(UIView *, SEL);
static void new_mapLayout(UIView *self, SEL _cmd) {
orig_mapLayout(self, _cmd);
if (![SCIUtils getBoolPref:@"show_fake_location_map_button"]) {
sciRemoveMapButton(self);
return;
}
sciAddMapButton(self);
sciRefreshMapButton(self);
UIView *btn = [self viewWithTag:kSciMapBtnTag];
if (btn) [self bringSubviewToFront:btn];
}
static void sciInstallMapHooks(void) {
static BOOL installed = NO;
if (installed) return;
Class c = NSClassFromString(@"IGFriendsMapCoreUI.IGFriendsMapView");
if (!c) return;
installed = YES;
SEL sel = @selector(layoutSubviews);
if (class_getInstanceMethod(c, sel))
MSHookMessageEx(c, sel, (IMP)new_mapLayout, (IMP *)&orig_mapLayout);
}
%ctor {
sciInstallMapHooks();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
sciInstallMapHooks();
});
[[NSNotificationCenter defaultCenter] addObserverForName:@"SCIFakeLocationMapBtnPrefChanged"
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(__unused NSNotification *n) {
sciRefreshActiveMapButton();
}];
}