Files
RyukGram/src/Features/General/NotesCustomization.x
T
faroukbmiled 86eaa95019 [release] RyukGram v1.2.0
### Features
- **Open Instagram links in app (Safari extension)** — bundled Safari web extension (sideload IPA only). Enable in Safari → Extensions; instagram.com links open in the app.
- **Localization** — every user-facing string flows through a central translation layer. Globe button in Settings; missing keys fall back to English. Ships English only — see the "Translating RyukGram" section in the README to add more.
- **Action buttons** — context-aware menus on feed, reels, and stories (expand, repost, download, copy caption, etc.) with per-context default tap action and carousel/multi-story bulk download
- **Enhanced HD downloads** — up to 1080p via DASH + FFmpegKit with quality picker, preview playback, encoding-speed options, and 720p fallback
- **Repost**, **media viewer**, **media zoom** (long-press), **download pill** (frosted glass, stacks concurrent downloads)
- **Fake location** — overrides CoreLocation app-wide, map picker + saved presets, optional quick-toggle button on the Friends Map
- **Messages-only mode** — strips every tab except DM inbox + profile
- **Launch tab** — pick which tab the app opens to
- Full last active date in DMs — show full date instead of "Active 2h ago"
- Custom date format — 12 formats with per-surface toggles (feed, notes/comments/stories, DMs)
- Send files in DMs (experimental)
- View story mentions
- Hide suggested stories
- Story tray long-press actions — view HD profile picture from the tray menu
- Advance on story reply — auto-skip to next story after sending a reply or reaction
- Mark story as seen on reply or emoji reaction
- Hide metrics (likes, comments, shares counts)
- Hide messages tab
- Hide voice/video call buttons in DM thread header (independent toggles)
- Disable app haptics
- Disable reels tab refresh
- Disable disappearing messages mode in DMs
- Follow indicator — shows whether the profile user follows you
- Copy note text on long press
- Zoom profile photo — long press opens full-screen viewer
- Notes actions — copy text, download GIF/audio from notes long-press menu
- Confirm unfollow
- Feed refresh controls — disable background refresh, home button refresh, and home button scroll

### Improvements
- Default tap action: added copy URL, repost, and view mentions options; dynamic menu generation per context
- Settings pages reordered: General → Feed → Stories → Reels → Messages → Profile → Navigation → Saving → Confirmations
- Fake location picker: native Apple Maps-style UI (search, long-press to drop pin, current location)
- Liquid glass floating tab bar + dynamic sizing
- Upload audio: FFmpegKit re-encode + trim for any audio/video input
- Settings reorganized with per-context action button config; new Profile page
- Highlight cover: full-screen viewer replaces direct download
- Switched HD encoder to `h264_videotoolbox` (hardware) — no GPL FFmpegKit required
- Legacy long-press download deprecated (off by default), replaced by action buttons

### Fixes
- Hide suggested stories no longer removes followed users' stories on scroll
- Settings search bar transparency with liquid glass off; auto-deactivates on push
- HD download cancel: tapping pill aborts in-flight downloads + FFmpeg sessions cleanly
- Download pill stuck state on background/foreground, progress reset per download
- Disappearing messages mode confirmation not firing on swipe
- Detailed color picker not working on story draw `†`
- DM seen toggle menu not updating after tap
- Reel refresh confirmation appearing on first app launch `†`
- Reels action button displacing profile pictures on photo reels
- Disappearing DM media download (expand, share, save to Photos with progress pill)
- Carousel "Download all" not showing item count in feed
- Encoding speed setting being ignored for HD downloads
- Various upstream SCInsta merges (Meta AI hiding, suggested chats hiding, notes tray) — marked `†`

> `†` Merged from upstream [SCInsta](https://github.com/SoCuul/SCInsta) by SoCuul

### Credits
- Thanks to [@erupts0](https://github.com/erupts0) (John) for testing and feature suggestions
- Thanks to [@euoradan](https://t.me/euoradan) (Radan) for experimental Instagram feature flag research
- Safari extension forked/cleaned from [BillyCurtis/OpenInstagramSafariExtension](https://github.com/BillyCurtis/OpenInstagramSafariExtension)

### Known Issues
- 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)
2026-04-16 03:03:30 +01:00

293 lines
12 KiB
Plaintext

#import "../../Utils.h"
static char targetStaticRef[] = "target";
%hook IGDirectNotesCreationView
- (id)initWithViewModel:(id)model
featureSupport:(IGNotesCreationFeatureSupportModel *)support
presentationAnimation:(id)animation
composerUpdateListener:(id)listener
delegate:(id)delegate
layoutType:(long long)type
userSession:(id)session
{
if ([SCIUtils getBoolPref:@"enable_notes_customization"]) {
// enableAnimatedEmojisInCreation
@try {
[support setValue:@(YES) forKey:@"enableAnimatedEmojisInCreation"];
}
@catch (NSException *exception) {
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, support);
}
// enableBubbleCustomization
@try {
[support setValue:@(YES) forKey:@"enableBubbleCustomization"];
}
@catch (NSException *exception) {
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, support);
}
// enableRandomThemeGenerator
@try {
[support setValue:@(YES) forKey:@"enableRandomThemeGenerator"];
}
@catch (NSException *exception) {
NSLog(@"[SCInsta] WARNING: %@\n\nFull object: %@", exception.reason, support);
}
}
return %orig(model, support, animation, listener, delegate, type, session);
}
%end
// Demangled name: IGDirectNotesUISwift.IGDirectNotesBubbleEditorColorPaletteView
%hook _TtC20IGDirectNotesUISwift41IGDirectNotesBubbleEditorColorPaletteView
%property (nonatomic, copy) UIColor *backgroundColor;
%property (nonatomic, copy) UIColor *textColor;
%property (nonatomic, copy) NSString *emojiText;
- (void)didMoveToWindow {
%orig;
if (![SCIUtils getBoolPref:@"custom_note_themes"]) return;
// Inject buttons once in view lifecycle
static char didInjectButtons;
if (objc_getAssociatedObject(self, &didInjectButtons)) {
return;
}
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) self = weakSelf;
if (!self || !self.window) {
return;
}
UIView *container = self.superview ?: self.window;
if (!container) {
return;
}
// Button config
UIButtonConfiguration *config = [UIButtonConfiguration tintedButtonConfiguration];
config.background.cornerRadius = 12.0;
config.cornerStyle = UIButtonConfigurationCornerStyleFixed;
config.contentInsets = NSDirectionalEdgeInsetsMake(13.7, 10, 13.7, 10);
// Left button
UIButton *leftButton = [UIButton buttonWithType:UIButtonTypeSystem];
leftButton.configuration = config;
leftButton.translatesAutoresizingMaskIntoConstraints = NO;
leftButton.tintColor = [SCIUtils SCIColor_Primary];
NSMutableAttributedString *attrTitleLeft = [[NSMutableAttributedString alloc] initWithString:@"Background"];
[attrTitleLeft addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:14 weight:UIFontWeightSemibold]
range:NSMakeRange(0, attrTitleLeft.length)
];
[leftButton setAttributedTitle:attrTitleLeft forState:UIControlStateNormal];
[leftButton sizeToFit];
[leftButton addAction:[UIAction actionWithHandler:^(__kindof UIAction * _Nonnull action) {
[self presentColorPicker:@"Background"];
}] forControlEvents:UIControlEventTouchUpInside];
// Middle button
UIButton *middleButton = [UIButton buttonWithType:UIButtonTypeSystem];
middleButton.configuration = config;
middleButton.translatesAutoresizingMaskIntoConstraints = NO;
middleButton.tintColor = [SCIUtils SCIColor_Primary];
NSMutableAttributedString *attrTitleMiddle = [[NSMutableAttributedString alloc] initWithString:@"Text"];
[attrTitleMiddle addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:14 weight:UIFontWeightSemibold]
range:NSMakeRange(0, attrTitleMiddle.length)
];
[middleButton setAttributedTitle:attrTitleMiddle forState:UIControlStateNormal];
[middleButton sizeToFit];
[middleButton addAction:[UIAction actionWithHandler:^(__kindof UIAction * _Nonnull action) {
[self presentColorPicker:@"Text"];
}] forControlEvents:UIControlEventTouchUpInside];
// Right button
UIButton *rightButton = [UIButton buttonWithType:UIButtonTypeSystem];
rightButton.configuration = config;
rightButton.translatesAutoresizingMaskIntoConstraints = NO;
rightButton.tintColor = [SCIUtils SCIColor_Primary];
NSMutableAttributedString *attrTitleRight = [[NSMutableAttributedString alloc] initWithString:@"Emoji"];
[attrTitleRight addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:14 weight:UIFontWeightSemibold]
range:NSMakeRange(0, attrTitleRight.length)
];
[rightButton setAttributedTitle:attrTitleRight forState:UIControlStateNormal];
[rightButton sizeToFit];
[rightButton addAction:[UIAction actionWithHandler:^(__kindof UIAction * _Nonnull action) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:SCILocalized(@"Enter Emoji Text")
message:SCILocalized(@"Click the Apply button after this to see the emoji")
preferredStyle:UIAlertControllerStyleAlert];
[alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = SCILocalized(@"Type emoji...");
}];
[alert addAction:[UIAlertAction actionWithTitle:@"OK"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
self.emojiText = alert.textFields[0].text;
[self applySCICustomTheme:@"Emoji"];
}]];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel")
style:UIAlertActionStyleCancel
handler:nil]];
UIViewController *vc = [SCIUtils nearestViewControllerForView:self];
[vc presentViewController:alert animated:YES completion:nil];
}] forControlEvents:UIControlEventTouchUpInside];
// Create stack view
UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:@[leftButton, middleButton, rightButton]];
stack.axis = UILayoutConstraintAxisHorizontal;
stack.spacing = 15.0;
stack.alignment = UIStackViewAlignmentCenter;
stack.distribution = UIStackViewDistributionFillEqually;
// Find max height among arranged subviews
CGFloat maxHeight = 0.0;
for (UIView *subview in stack.arrangedSubviews) {
maxHeight = MAX(maxHeight, subview.bounds.size.height);
}
// Manual frame with side padding
CGFloat bottomMargin = 15.0;
CGRect viewFrame = [self convertRect:self.bounds toView:container];
CGFloat y = CGRectGetMinY(viewFrame) - maxHeight - bottomMargin;
CGFloat width = container.bounds.size.width - stack.spacing * 2;
stack.frame = CGRectMake(stack.spacing, y, width, maxHeight);
[stack layoutIfNeeded];
[container addSubview:stack];
objc_setAssociatedObject(
self,
&didInjectButtons,
@YES,
OBJC_ASSOCIATION_RETAIN_NONATOMIC
);
});
}
%new - (void)presentColorPicker:(NSString *)target {
UIColorPickerViewController *colorPickerController = [[UIColorPickerViewController alloc] init];
colorPickerController.delegate = (id<UIColorPickerViewControllerDelegate>)self; // cast to suppress warnings
colorPickerController.title = [NSString stringWithFormat:@"%@ color", target];
colorPickerController.modalPresentationStyle = UIModalPresentationPopover;
colorPickerController.supportsAlpha = NO;
// Show last picked color for type
if ([target isEqualToString:@"Background"]) {
colorPickerController.selectedColor = self.backgroundColor;
}
else if ([target isEqualToString:@"Text"]) {
colorPickerController.selectedColor = self.textColor;
}
UIViewController *presentingVC = [SCIUtils nearestViewControllerForView:self];
if (presentingVC != nil) {
[presentingVC presentViewController:colorPickerController animated:YES completion:nil];
}
// Save which color target to update
objc_setAssociatedObject(
presentingVC,
&targetStaticRef,
target,
OBJC_ASSOCIATION_RETAIN_NONATOMIC
);
}
// UIColorPickerViewControllerDelegate Protocol
%new - (void)colorPickerViewController:(UIColorPickerViewController *)viewController
didSelectColor:(UIColor *)color
continuously:(BOOL)continuously
{
_TtC20IGDirectNotesUISwift41IGDirectNotesBubbleEditorColorPaletteView *bubbleEditorVC = [SCIUtils nearestViewControllerForView:self];
NSString *target = objc_getAssociatedObject(bubbleEditorVC, &targetStaticRef);
if (!target) return;
// Update saved color target
if ([target isEqualToString:@"Background"]) {
self.backgroundColor = color;
}
else if ([target isEqualToString:@"Text"]) {
self.textColor = color;
}
[self applySCICustomTheme:target];
};
%new - (void)applySCICustomTheme:(NSString *)target {
// Get notes composer vc
_TtC20IGDirectNotesUISwift39IGDirectNotesBubbleEditorViewController *parentVC = [SCIUtils nearestViewControllerForView:self];
if (!parentVC) return;
IGDirectNotesComposerViewController *composerVC = parentVC.delegate;
if (!composerVC) return;
// Get current theme model
IGNotesCustomThemeCreationModel *model = [composerVC valueForKey:@"_selectedCustomThemeCreationModel"];
if (!model) {
// Create new note theme model
model = [[%c(IGNotesCustomThemeCreationModel) alloc] init];
if (!model) return;
}
//SCILog(@"Current note theme model: %@", model);
[model setValue:[composerVC valueForKey:@"_composerText"] forKey:@"customEmoji"];
// Update saved color target
if ([target isEqualToString:@"Background"]) {
[model setValue:self.backgroundColor forKey:@"backgroundColor"];
}
else if ([target isEqualToString:@"Text"]) {
[model setValue:self.textColor forKey:@"textColor"];
[model setValue:self.textColor forKey:@"secondaryTextColor"];
}
// Always set emoji to prevent it being overwritten
[model setValue:self.emojiText forKey:@"customEmoji"];
//SCILog(@"Updated note theme model: %@", model);
// Apply custom notes theme
[composerVC notesBubbleEditorViewControllerDidUpdateWithCustomThemeCreationModel:model];
// Enable apply/cancel buttons
UIView *parentVCView = [parentVC view];
if (!parentVCView) return;
NSArray<UIView *> *parentVCSubviews = [parentVCView subviews];
if (!parentVCSubviews) return;
[parentVCSubviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([obj isKindOfClass:%c(IGDSBottomButtonsView)]) {
[obj setPrimaryButtonEnabled:YES];
[obj setSecondaryButtonEnabled:YES];
}
}];
}
%end