feat: Confirm story like

feat: Confirm story emoji reaction
feat: Spanish translation
feat: Language switcher + import/export localization from Debug
feat: Swipe down to dismiss media viewer
feat: Manually add users to story/chat exclusion lists by username
feat: Keep stories visually seen locally — split mode (grey ring locally, seen receipt still blockedon server)
feat: Auto-scroll reels — IG default or RyukGram mode, keeps advancing after swiping back (#3)
fix: Messages-only mode — tab swiping disabled
fix: Settings quick-access broken in non-English languages
fix: Story seen-receipt block restored on IG v425+ (Sundial uploader), per-owner, both "Block all" and "Block selected" modes
fix: Block selected mode no longer marks listed stories as seen
imp: Story-interaction pipeline unifies confirm + seen/advance side effects
This commit is contained in:
faroukbmiled
2026-04-18 00:57:52 +01:00
parent 51c1dc59cf
commit 0b9992ee30
26 changed files with 2032 additions and 395 deletions
+34 -3
View File
@@ -7,6 +7,7 @@
#import "../Utils.h"
#import "../Downloader/Download.h"
#import "../PhotoAlbum.h"
#import "../Features/StoriesAndMessages/SCIExcludedStoryUsers.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <Photos/Photos.h>
@@ -23,9 +24,9 @@ extern BOOL sciIsStoryAudioEnabled(void);
// Match keys used in the settings-entry title map for openSettingsForContext:
static NSString *sciSettingsTitleForContext(SCIActionContext ctx) {
switch (ctx) {
case SCIActionContextFeed: return @"Feed";
case SCIActionContextReels: return @"Reels";
case SCIActionContextStories: return @"Stories";
case SCIActionContextFeed: return SCILocalized(@"Feed");
case SCIActionContextReels: return SCILocalized(@"Reels");
case SCIActionContextStories: return SCILocalized(@"Stories");
}
return @"General";
}
@@ -888,6 +889,36 @@ static UIView *sciFindSubviewOfClass(UIView *root, NSString *className, int maxD
}
}
// Story user list management (add/remove from exclusion list).
if (ctx == SCIActionContextStories && [SCIUtils getBoolPref:@"enable_story_user_exclusions"]) {
extern NSDictionary *sciOwnerInfoForView(UIView *);
extern void sciRefreshAllVisibleOverlays(UIViewController *);
extern __weak UIViewController *sciActiveStoryViewerVC;
NSDictionary *ownerInfo = sourceView ? sciOwnerInfoForView(sourceView) : nil;
NSString *ownerPK = ownerInfo[@"pk"];
if (ownerPK.length) {
BOOL inList = [SCIExcludedStoryUsers isInList:ownerPK];
BOOL bs = [SCIExcludedStoryUsers isBlockSelectedMode];
NSString *addLabel = bs ? SCILocalized(@"Add to block list") : SCILocalized(@"Exclude from seen");
NSString *removeLabel = bs ? SCILocalized(@"Remove from block list") : SCILocalized(@"Remove from exclude list");
NSString *title = inList ? removeLabel : addLabel;
NSString *icon = inList ? @"eye.fill" : @"eye.slash";
NSString *capturedPK = [ownerPK copy];
NSString *capturedUser = [ownerInfo[@"username"] ?: @"" copy];
NSString *capturedName = [ownerInfo[@"fullName"] ?: @"" copy];
[out addObject:[SCIAction actionWithTitle:title icon:icon handler:^{
if (inList) {
[SCIExcludedStoryUsers removePK:capturedPK];
[SCIUtils showToastForDuration:2.0 title:bs ? SCILocalized(@"Unblocked") : SCILocalized(@"Removed from list")];
} else {
[SCIExcludedStoryUsers addOrUpdateEntry:@{@"pk": capturedPK, @"username": capturedUser, @"fullName": capturedName}];
[SCIUtils showToastForDuration:2.0 title:bs ? SCILocalized(@"Added to block list") : SCILocalized(@"Added to exclude list")];
}
sciRefreshAllVisibleOverlays(sciActiveStoryViewerVC);
}]];
}
}
if (ctx != SCIActionContextStories) {
// Caption lives on the parent media (not on carousel children).
[out addObject:[SCIAction actionWithTitle:SCILocalized(@"Copy caption")
+46 -9
View File
@@ -131,7 +131,7 @@
#pragma mark - Container VC (PageViewController-based)
// ═══════════════════════════════════════════════════════════════════════════
@interface _SCIMediaViewerContainerVC : UIViewController <UIPageViewControllerDataSource, UIPageViewControllerDelegate>
@interface _SCIMediaViewerContainerVC : UIViewController <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIGestureRecognizerDelegate>
@property (nonatomic, strong) NSArray<SCIMediaViewerItem *> *items;
@property (nonatomic, assign) NSUInteger currentIndex;
@property (nonatomic, strong) UIPageViewController *pageVC;
@@ -238,18 +238,16 @@
[self.captionLabel.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor constant:-8],
]];
// Swipe down to dismiss
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleDismissPan:)];
pan.delegate = (id<UIGestureRecognizerDelegate>)self;
[self.view addGestureRecognizer:pan];
// Single tap toggles chrome
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleChrome)];
tap.cancelsTouchesInView = NO;
[self.pageVC.view addGestureRecognizer:tap];
// For photos, let double-tap zoom work without triggering single-tap
for (UIGestureRecognizer *gr in self.pageVC.view.gestureRecognizers) {
if ([gr isKindOfClass:[UITapGestureRecognizer class]] && ((UITapGestureRecognizer *)gr).numberOfTapsRequired == 1) {
// Already have our tap
}
}
[self updateChrome];
}
@@ -290,6 +288,45 @@
}];
}
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gr {
if (![gr isKindOfClass:[UIPanGestureRecognizer class]]) return YES;
CGPoint v = [gr velocityInView:self.view];
return fabs(v.y) > fabs(v.x) && v.y > 0;
}
- (void)handleDismissPan:(UIPanGestureRecognizer *)gr {
CGFloat ty = [gr translationInView:self.view].y;
CGFloat h = self.view.bounds.size.height;
CGFloat progress = fmin(fmax(ty / h, 0), 1);
switch (gr.state) {
case UIGestureRecognizerStateChanged: {
self.view.transform = CGAffineTransformMakeTranslation(0, ty);
self.view.alpha = 1.0 - progress * 0.5;
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
CGFloat vy = [gr velocityInView:self.view].y;
if (progress > 0.25 || vy > 800) {
[UIView animateWithDuration:0.2 animations:^{
self.view.transform = CGAffineTransformMakeTranslation(0, h);
self.view.alpha = 0;
} completion:^(BOOL finished) {
[self dismissViewControllerAnimated:NO completion:nil];
}];
} else {
[UIView animateWithDuration:0.25 delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0 options:0 animations:^{
self.view.transform = CGAffineTransformIdentity;
self.view.alpha = 1;
} completion:nil];
}
break;
}
default: break;
}
}
- (void)closeTapped {
// Pause any playing video
UIViewController *current = self.pageVC.viewControllers.firstObject;
@@ -424,7 +461,7 @@
_SCIMediaViewerContainerVC *vc = [[_SCIMediaViewerContainerVC alloc] init];
vc.items = items;
vc.currentIndex = index;
vc.modalPresentationStyle = UIModalPresentationFullScreen;
vc.modalPresentationStyle = UIModalPresentationOverFullScreen;
vc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[topMostController() presentViewController:vc animated:YES completion:nil];
});