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
+24 -6
View File
@@ -13,6 +13,11 @@ on:
default: true
required: false
type: boolean
build_tipa:
description: "Build tipa"
default: false
required: false
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -91,10 +96,7 @@ jobs:
pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip
cd main
curl -Lo ipapatch https://github.com/asdfzxcvbn/ipapatch/releases/download/v2.1.3/ipapatch.macos-arm64
chmod +x ipapatch
export PATH=.:$PATH
# ipapatch disabled — upstream issues.
./build.sh sideload
ls -la packages
env:
@@ -106,12 +108,18 @@ jobs:
IPA=$(ls -t *.ipa | grep -iv instagram | head -n1)
[ -n "$IPA" ] && mv "$IPA" "RyukGram_sideloaded_v${VERSION}.ipa"
- name: Duplicate as .tipa
if: ${{ inputs.build_tipa }}
run: |
cd main/packages
cp "RyukGram_sideloaded_v${VERSION}.ipa" "RyukGram_trollstore_v${VERSION}.tipa"
- name: Pass package name to upload action
id: package_name
run: |
echo "package=$(ls -t main/packages/RyukGram_sideloaded_v*.ipa | head -n1 | xargs basename)" >> "$GITHUB_OUTPUT"
- name: Upload Artifact
- name: Upload IPA Artifact
if: ${{ inputs.upload_artifact }}
uses: actions/upload-artifact@v4
with:
@@ -119,11 +127,21 @@ jobs:
path: ${{ github.workspace }}/main/packages/${{ steps.package_name.outputs.package }}
if-no-files-found: error
- name: Upload TIPA Artifact
if: ${{ inputs.upload_artifact && inputs.build_tipa }}
uses: actions/upload-artifact@v4
with:
name: RyukGram_trollstore_v${{ steps.version.outputs.version }}
path: ${{ github.workspace }}/main/packages/RyukGram_trollstore_v*.tipa
if-no-files-found: error
- name: Create Release
uses: softprops/action-gh-release@v2.0.6
with:
name: RyukGram_sideloaded_v${{ steps.version.outputs.version }}
files: ${{ github.workspace }}/main/packages/RyukGram_sideloaded_v*.ipa
files: |
${{ github.workspace }}/main/packages/RyukGram_sideloaded_v*.ipa
${{ github.workspace }}/main/packages/RyukGram_trollstore_v*.tipa
draft: true
- name: Output Release URL
+20 -10
View File
@@ -65,6 +65,7 @@ A feature-rich iOS tweak for Instagram, forked from [SCInsta](https://github.com
### Reels
- Modify tap controls
- Auto-scroll reels — IG default or RyukGram mode (keeps advancing after swiping back) **\***
- Always show progress scrubber
- Disable auto-unmuting reels (properly blocks mute switch, volume buttons, and announcer broadcasts) **\***
- Confirm reel refresh
@@ -133,7 +134,7 @@ A feature-rich iOS tweak for Instagram, forked from [SCInsta](https://github.com
- Disable view-once limitations
- Disable screenshot detection
- Disable story seen receipt (blocks network upload, toggleable at runtime without restart) **\***
- Keep stories visually unseen — keeps the colorful ring in the tray after viewing **\***
- Keep stories visually seen locally — mark stories as seen locally (grey ring) while the seen receipt is still blocked on the server **\***
- Manual mark story as seen — button on story overlay to selectively mark stories as seen (button or toggle mode) **\***
- Long-press the story seen button for quick actions **\***
- Per-user story seen-receipt list with blocking mode — "Block all" (exclude list) or "Block selected only" (include list). Manage via 3-dot menu, eye button long-press, or settings list **\***
@@ -191,23 +192,31 @@ A feature-rich iOS tweak for Instagram, forked from [SCInsta](https://github.com
- Multi-language UI — every user-facing string in RyukGram flows through a central translation layer **\***
- Built-in language picker — globe icon in the top-right of Settings; pick System default or any shipped language **\***
- Falls back to English when a translation is missing, so nothing ever breaks **\***
- Currently shipping: **English only** — other languages land as translators submit them (see below).
- Currently shipping: **English**, **Spanish** — other languages land as translators submit them (see below).
### Optimization
- Automatically clears unneeded cache folders, reducing the size of your Instagram installation
# Translating RyukGram
Want to see RyukGram in your language? Open a PR — it takes about 30 minutes of copy-paste.
Want to see RyukGram in your language? Two ways:
1. Copy `src/Localization/Resources/en.lproj/Localizable.strings` into a new folder named after the language code, e.g. `ar.lproj` (Arabic), `es.lproj` (Spanish), `fr.lproj` (French), `pt.lproj` (Portuguese), `de.lproj` (German), `tr.lproj` (Turkish)…
2. Translate the **right-hand side** of every `"key" = "value";` line. Never touch the left-hand side — it's the lookup key and must match English.
3. Keep format specifiers (`%@`, `%lu`, `%d`, `%1$@`…) exactly as they appear, in the same order. Use positional specifiers (`%1$@ %2$lu`) if your language needs different word order.
4. Keep the same section banners and structure — it makes the diff easy to review.
5. Open a pull request at <https://github.com/faroukbmiled/RyukGram/pulls>. Title it e.g. `l10n: Add Arabic translation`.
### Option A: In-app (fastest)
1. Open **Settings → Debug → Localization → Export English strings** — share the base `.strings` file to yourself.
2. Translate the **right-hand side** of every `"key" = "value";` line. Never touch the left-hand side.
3. Go to **Debug → Localization → Update → + Add new language** — enter your language code (e.g. `fr`), pick the translated file, restart.
4. Your language now appears in the globe menu. Test it, tweak it, re-import as needed.
5. When ready, open a pull request with the file at `src/Localization/Resources/<code>.lproj/Localizable.strings`.
Partial translations are welcome — untranslated keys automatically fall back to English at runtime. Ship what you've got, iterate from there.
### Option B: PR directly
1. Copy `src/Localization/Resources/en.lproj/Localizable.strings` into a new folder: `<code>.lproj/Localizable.strings`
2. Translate the right-hand side of every line.
3. Keep format specifiers (`%@`, `%lu`, `%d`, `%1$@`…) exactly as-is. Use positional specifiers if your language needs different word order.
4. Keep section banners and structure — makes the diff easy to review.
5. Open a PR at <https://github.com/faroukbmiled/RyukGram/pulls>. Title it e.g. `l10n: Add French translation`.
If you find a string in the app that still renders in English on a translated build, open an issue with a screenshot and we'll add the key.
Partial translations are welcome — untranslated keys fall back to English at runtime.
If you find a string that still renders in English on a translated build, open an issue with a screenshot.
## Known Issues
- Preserved unsent messages cannot be removed using "Delete for you". Pull to refresh in the DMs tab clears all preserved messages (with optional confirmation if "Warn before clearing on refresh" is enabled).
@@ -249,3 +258,4 @@ $ ./build.sh <sideload/rootless/rootful>
- [@euoradan](https://t.me/euoradan) (Radan) — experimental Instagram feature flag research
- [@erupts0](https://github.com/erupts0) (John) — testing and feature suggestions
- [BillyCurtis/OpenInstagramSafariExtension](https://github.com/BillyCurtis/OpenInstagramSafariExtension) — base for the bundled Safari extension
- Furamako — Spanish translation
+110 -15
View File
@@ -194,14 +194,7 @@ then
echo -e '\033[0;33mOr use ./build.sh dylib to build the dylib for Feather injection.\033[0m'
exit 1
fi
if ! command -v ipapatch &> /dev/null; then
echo -e '\033[1m\033[0;31mipapatch not found. Install it from:\033[0m'
echo ' https://github.com/asdfzxcvbn/ipapatch/releases/latest'
echo
echo -e '\033[0;33mUse ./build.sh sideload --buildonly to just compile without creating the IPA.\033[0m'
echo -e '\033[0;33mOr use ./build.sh dylib to build the dylib for Feather injection.\033[0m'
exit 1
fi
# ipapatch disabled — upstream issues.
fi
echo -e '\033[1m\033[32mBuilding RyukGram tweak for sideloading (as IPA)\033[0m'
@@ -278,8 +271,7 @@ then
rm -rf "$INJECT_TMP"
fi
# Patch IPA for sideloading
ipapatch --input "packages/RyukGram-sideloaded.ipa" --inplace --noconfirm
# ipapatch disabled — upstream issues.
echo -e "\033[1m\033[32mDone, enjoy RyukGram!\033[0m\n\nYou can find the ipa file at: $(pwd)/packages"
@@ -333,16 +325,119 @@ then
echo -e "\033[1m\033[32mDone, enjoy RyukGram!\033[0m\n\nYou can find the deb file at: $(pwd)/packages"
# TrollStore build — .tipa is a renamed .ipa. Skip sideload re-sign; TS signs on-device.
elif [ "$1" == "trollstore" ];
then
HAS_FLEX=1
if [ -z "$(ls -A modules/FLEXing 2>/dev/null)" ]; then
HAS_FLEX=0
fi
if [ "$HAS_FLEX" == "1" ]; then
MAKEARGS='SIDELOAD=1'
FLEXPATH='.theos/obj/debug/FLEXing.dylib .theos/obj/debug/libflex.dylib'
else
MAKEARGS=''
FLEXPATH=''
fi
COMPRESSION=9
make clean 2>/dev/null || true
rm -rf .theos
mkdir -p packages
ipaFile="$(find ./packages/ -maxdepth 1 -type f \( -iname '*com.burbn.instagram*.ipa' -o -iname 'Instagram*.ipa' -o -iname '[0-9]*.ipa' \) ! -iname 'RyukGram*.ipa' -exec basename {} \; 2>/dev/null | head -1)"
if [ -z "${ipaFile}" ]; then
cwdIpa="$(find . -maxdepth 1 -type f \( -iname '*com.burbn.instagram*.ipa' -o -iname 'Instagram*.ipa' -o -iname '[0-9]*.ipa' \) 2>/dev/null | head -1)"
if [ -n "$cwdIpa" ]; then
mv "$cwdIpa" packages/
ipaFile="$(basename "$cwdIpa")"
fi
fi
if [ -z "${ipaFile}" ]; then
echo -e '\033[1m\033[0;31mDecrypted Instagram IPA not found.\033[0m'
exit 1
fi
if ! command -v cyan &> /dev/null; then
echo -e '\033[1m\033[0;31mcyan not found. Install it with:\033[0m'
echo ' pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip'
exit 1
fi
echo -e '\033[1m\033[32mBuilding RyukGram tweak for TrollStore (.tipa)\033[0m'
make $MAKEARGS
cp .theos/obj/debug/RyukGram.dylib packages/RyukGram.dylib
BUNDLE_PATH="packages/RyukGram.bundle"
rm -rf "$BUNDLE_PATH"
mkdir -p "$BUNDLE_PATH"
copy_localization_into_bundle "$BUNDLE_PATH"
if [ -d "modules/ffmpegkit/ffmpegkit.framework" ]; then
for fw in modules/ffmpegkit/*.framework; do
cp -R "$fw" "$BUNDLE_PATH/"
done
LIBS="libavutil libavcodec libavformat libavfilter libavdevice libswresample libswscale"
for lib in $LIBS; do
mv "$BUNDLE_PATH/${lib}.framework" "$BUNDLE_PATH/${lib}_sci.framework"
install_name_tool -id "@rpath/${lib}_sci.framework/${lib}" \
"$BUNDLE_PATH/${lib}_sci.framework/${lib}"
done
for target in "$BUNDLE_PATH/ffmpegkit.framework/ffmpegkit" \
"$BUNDLE_PATH"/libav*_sci.framework/libav* \
"$BUNDLE_PATH"/libsw*_sci.framework/libsw*; do
[ -f "$target" ] || continue
for lib in $LIBS; do
install_name_tool -change \
"@rpath/${lib}.framework/${lib}" \
"@rpath/${lib}_sci.framework/${lib}" \
"$target" 2>/dev/null || true
done
done
install_name_tool -add_rpath @loader_path/.. \
"$BUNDLE_PATH/ffmpegkit.framework/ffmpegkit" 2>/dev/null || true
fi
TWEAKPATH=".theos/obj/debug/RyukGram.dylib"
BUNDLE_ARG=""
[ -d "$BUNDLE_PATH" ] && BUNDLE_ARG="$BUNDLE_PATH"
echo -e '\033[1m\033[32mCreating the TIPA file...\033[0m'
rm -f packages/RyukGram-trollstore.tipa packages/RyukGram-trollstore.ipa
cyan -i "packages/${ipaFile}" -o packages/RyukGram-trollstore.ipa -f $TWEAKPATH $FLEXPATH $BUNDLE_ARG -c $COMPRESSION -m 15.0 -du
# Embed Safari extension.
APPEX_SRC="extensions/OpenInstagramSafariExtension.appex"
if [ -d "$APPEX_SRC" ]; then
echo -e '\033[1m\033[32mEmbedding Safari extension\033[0m'
INJECT_TMP=$(mktemp -d)
unzip -q packages/RyukGram-trollstore.ipa -d "$INJECT_TMP"
APP_DIR="$(find "$INJECT_TMP/Payload" -maxdepth 1 -type d -name '*.app' | head -1)"
if [ -n "$APP_DIR" ]; then
mkdir -p "$APP_DIR/PlugIns"
rm -rf "$APP_DIR/PlugIns/OpenInstagramSafariExtension.appex"
cp -R "$APPEX_SRC" "$APP_DIR/PlugIns/"
( cd "$INJECT_TMP" && zip -qr -${COMPRESSION} ../repacked.ipa Payload )
mv "$INJECT_TMP/../repacked.ipa" packages/RyukGram-trollstore.ipa
fi
rm -rf "$INJECT_TMP"
fi
mv packages/RyukGram-trollstore.ipa packages/RyukGram-trollstore.tipa
echo -e "\033[1m\033[32mDone!\033[0m\n\nTIPA at: $(pwd)/packages/RyukGram-trollstore.tipa"
else
echo '+----------------------+'
echo '|RyukGram Build Script |'
echo '+----------------------+'
echo
echo 'Usage: ./build.sh <dylib/sideload/rootless/rootful>'
echo 'Usage: ./build.sh <dylib/sideload/trollstore/rootless/rootful>'
echo
echo ' dylib - Build the dylib only (for Feather/manual injection)'
echo ' sideload - Build a patched IPA (requires cyan + ipapatch + decrypted IPA)'
echo ' rootless - Build a rootless .deb package (with FFmpegKit)'
echo ' rootful - Build a rootful .deb package (with FFmpegKit)'
echo ' dylib - Build the dylib only (for Feather/manual injection)'
echo ' sideload - Build a patched IPA (requires cyan + decrypted IPA)'
echo ' trollstore - Build a .tipa for TrollStore (requires cyan + decrypted IPA)'
echo ' rootless - Build a rootless .deb package (with FFmpegKit)'
echo ' rootful - Build a rootful .deb package (with FFmpegKit)'
exit 1
fi
+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];
});
+10 -70
View File
@@ -1,30 +1,14 @@
#import "../../Utils.h"
///////////////////////////////////////////////////////////
#define CONFIRMPOSTLIKE(orig) \
if ([SCIUtils getBoolPref:@"like_confirm"]) \
[SCIUtils showConfirmation:^(void) { orig; }]; \
else return orig;
// Confirmation handlers
#define CONFIRMPOSTLIKE(orig) \
if ([SCIUtils getBoolPref:@"like_confirm"]) { \
NSLog(@"[SCInsta] Confirm post like triggered"); \
\
[SCIUtils showConfirmation:^(void) { orig; }]; \
} \
else { \
return orig; \
} \
#define CONFIRMREELSLIKE(orig) \
if ([SCIUtils getBoolPref:@"like_confirm_reels"]) { \
NSLog(@"[SCInsta] Confirm reels like triggered"); \
\
[SCIUtils showConfirmation:^(void) { orig; }]; \
} \
else { \
return orig; \
} \
///////////////////////////////////////////////////////////
#define CONFIRMREELSLIKE(orig) \
if ([SCIUtils getBoolPref:@"like_confirm_reels"]) \
[SCIUtils showConfirmation:^(void) { orig; }]; \
else return orig;
// Liking posts
%hook IGUFIButtonBarView
@@ -96,53 +80,9 @@
}
%end
// Liking stories
%hook IGStoryFullscreenDefaultFooterView
- (void)_handleLikeTapped {
CONFIRMPOSTLIKE(%orig);
}
- (void)_likeTapped {
CONFIRMPOSTLIKE(%orig);
}
- (void)inputView:(id)arg1 didTapLikeButton:(id)arg2 {
CONFIRMPOSTLIKE(%orig);
}
// Story like/emoji confirm handled by SCIStoryInteractionPipeline.
// For some stupid reason they removed the "liketapped" methods on newer Instagram versions
// Now we have to do a shitty workaround instead :(
// Works 99% of the time, but sometimes clicks get through directly to the like button (somehow)
- (void)layoutSubviews {
%orig;
if (![SCIUtils getBoolPref:@"like_confirm"]) return;
UIButton *likeButton = [self valueForKey:@"likeButton"];
if (!likeButton) return;
// 129115 = L(12) I(9) K(11) E(5)
static NSInteger kOverlayTag = 129115;
if ([likeButton viewWithTag:kOverlayTag]) return;
UIButton *overlay = [UIButton buttonWithType:UIButtonTypeCustom];
overlay.tag = kOverlayTag;
overlay.frame = likeButton.bounds;
overlay.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[overlay addTarget:self action:@selector(overlayTapped:) forControlEvents:UIControlEventTouchUpInside];
[likeButton addSubview:overlay];
}
%new - (void)overlayTapped:(UIButton *)overlay {
UIButton *likeButton = (UIButton *)overlay.superview;
[SCIUtils showConfirmation:^{
dispatch_async(dispatch_get_main_queue(), ^{
[likeButton sendActionsForControlEvents:UIControlEventTouchUpInside];
});
}];
}
%end
// DM like button (seems to be hidden)
// DM like button
%hook IGDirectThreadViewController
- (void)_didTapLikeButton {
CONFIRMPOSTLIKE(%orig);
+2 -3
View File
@@ -92,12 +92,11 @@ NSArray *filterSurfacesArray(NSArray *surfaces) {
}
- (BOOL)isTabSwipingEnabled {
// Swipe lands on stripped tabs in messages-only.
if ([SCIUtils getBoolPref:@"messages_only"]) return NO;
if ([[SCIUtils getStringPref:@"swipe_nav_tabs"] isEqualToString:@"enabled"]) return YES;
else if ([[SCIUtils getStringPref:@"swipe_nav_tabs"] isEqualToString:@"disabled"]) return NO;
return %orig;
}
- (void)setIsTabSwipingEnabled:(BOOL)arg1 {
return;
+1 -1
View File
@@ -37,7 +37,7 @@
%orig;
BOOL msgOnly = [SCIUtils getBoolPref:@"messages_only"];
NSString *target = msgOnly ? SCILocalized(@"direct-inbox-tab") : SCILocalized(@"mainfeed-tab");
NSString *target = msgOnly ? @"direct-inbox-tab" : @"mainfeed-tab";
if (![self.accessibilityIdentifier isEqualToString:target]) return;
if ([SCIUtils getBoolPref:@"settings_shortcut"]) {
+71
View File
@@ -0,0 +1,71 @@
// Auto-scroll reels. Modes:
// * ig — flip IG's own auto-scroll gates; covers video + photo reels
// * custom — same flag flip (photos) + per-cell loopCount trigger calling
// WantsScrollToNextItem each loop (videos keep advancing after back-swipe)
#import "../../Utils.h"
#import "../../InstagramHeaders.h"
#import <objc/runtime.h>
#import <objc/message.h>
static const void *kSCILoopCountKey = &kSCILoopCountKey;
static BOOL sciAdvanceInFlight = NO;
static inline NSString *sciMode(void) {
NSString *m = [SCIUtils getStringPref:@"auto_scroll_reels_mode"];
return m.length ? m : @"off";
}
static inline BOOL sciModeOn(void) { return ![sciMode() isEqualToString:@"off"]; }
static inline BOOL sciModeCustom(void) { return [sciMode() isEqualToString:@"custom"]; }
static UIViewController *sciFindFeedVCFromView(UIView *view) {
UIResponder *r = view;
while (r) {
if ([r isKindOfClass:[UIViewController class]] &&
[NSStringFromClass([r class]) isEqualToString:@"IGSundialFeedViewController"])
return (UIViewController *)r;
r = [r nextResponder];
}
return nil;
}
%hook IGSundialFeedViewController
- (BOOL)shouldForceEnableAutoScroll {
if (sciModeOn()) return YES;
return %orig;
}
- (BOOL)autoAdvanceToNextItem {
if (sciModeOn()) return YES;
return %orig;
}
%end
%hook IGSundialViewerVideoCell
- (void)videoView:(id)v didUpdatePlaybackStatus:(id)status {
%orig;
if (!sciModeCustom() || !status) return;
SEL loopSel = @selector(loopCount);
if (![status respondsToSelector:loopSel]) return;
long long cur = ((long long(*)(id, SEL))objc_msgSend)(status, loopSel);
NSNumber *prev = objc_getAssociatedObject(self, kSCILoopCountKey);
objc_setAssociatedObject(self, kSCILoopCountKey, @(cur), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (!prev || cur <= prev.longLongValue || sciAdvanceInFlight) return;
UIViewController *feedVC = sciFindFeedVCFromView((UIView *)self);
if (!feedVC || !feedVC.viewIfLoaded.window) return;
sciAdvanceInFlight = YES;
dispatch_async(dispatch_get_main_queue(), ^{
SEL wants = @selector(sundialViewerInteractionCoordinatorWantsScrollToNextItemAnimated:);
if ([feedVC respondsToSelector:wants])
((void(*)(id, SEL, BOOL))objc_msgSend)(feedVC, wants, YES);
sciAdvanceInFlight = NO;
});
}
- (void)prepareForReuse {
objc_setAssociatedObject(self, kSCILoopCountKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
%orig;
}
%end
@@ -1,5 +1,11 @@
// Story seen receipt blocking + visual seen state blocking
// Story seen-receipt blocking. Legacy + Sundial uploads are Swift-dispatched
// via a `networker` ivar — we cache the uploaders at init and nil the ivar
// while the active owner is blocked. `keep_seen_visual_local` ON runs orig
// (local stores update, server blocked). OFF skips orig (full block).
#import "StoryHelpers.h"
#import "SCIStoryInteractionPipeline.h"
#import "SCIExcludedStoryUsers.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <substrate.h>
@@ -12,6 +18,8 @@ NSMutableSet *sciAllowedSeenPKs = nil;
extern BOOL sciIsCurrentStoryOwnerExcluded(void);
extern BOOL sciIsObjectStoryOwnerExcluded(id obj);
static void sciStateRestore(void); // fwd — used by VC hook above its definition
static BOOL sciStorySeenToggleBypass(void) {
return [[SCIUtils getStringPref:@"story_seen_mode"] isEqualToString:@"toggle"] && sciStorySeenToggleEnabled;
}
@@ -28,41 +36,48 @@ static BOOL sciIsPKAllowed(id media) {
if (!media || !sciAllowedSeenPKs || sciAllowedSeenPKs.count == 0) return NO;
id pk = sciCall(media, @selector(pk));
if (!pk) return NO;
return [sciAllowedSeenPKs containsObject:[NSString stringWithFormat:@"%@", pk]];
NSString *pkStr = [NSString stringWithFormat:@"%@", pk];
if (![sciAllowedSeenPKs containsObject:pkStr]) return NO;
if ([SCIExcludedStoryUsers isFeatureEnabled] && ![SCIExcludedStoryUsers isUserPKExcluded:pkStr])
return NO;
return YES;
}
static BOOL sciShouldBlockSeenNetwork() {
// ============ Feature gates ============
static BOOL sciShouldBlockSeenNetwork(void) {
if (sciSeenBypassActive) return NO;
if (sciStorySeenToggleBypass()) return NO;
if (sciIsCurrentStoryOwnerExcluded()) return NO;
return [SCIUtils getBoolPref:@"no_seen_receipt"];
}
static BOOL sciShouldBlockSeenVisual() {
static BOOL sciShouldBlockSeenVisual(void) {
if (sciSeenBypassActive) return NO;
if (sciStorySeenToggleBypass()) return NO;
if (sciIsCurrentStoryOwnerExcluded()) return NO;
return [SCIUtils getBoolPref:@"no_seen_receipt"] && [SCIUtils getBoolPref:@"no_seen_visual"];
if (![SCIUtils getBoolPref:@"no_seen_receipt"]) return NO;
return ![SCIUtils getBoolPref:@"keep_seen_visual_local"];
}
// Per-instance gating for tray/item/ring hooks where the "current" story
// VC may not be the owner of the model in question.
// Per-instance gate — tray/item/ring models may not match the active VC.
static BOOL sciShouldBlockSeenVisualForObj(id obj) {
if (sciSeenBypassActive) return NO;
if (sciStorySeenToggleBypass()) return NO;
if (![SCIUtils getBoolPref:@"no_seen_receipt"] || ![SCIUtils getBoolPref:@"no_seen_visual"]) return NO;
if (![SCIUtils getBoolPref:@"no_seen_receipt"]) return NO;
if ([SCIUtils getBoolPref:@"keep_seen_visual_local"]) return NO;
if (sciIsObjectStoryOwnerExcluded(obj)) return NO;
return YES;
}
// network seen blocking
// ============ Legacy network-upload hooks (pre-Sundial fallback) ============
%hook IGStorySeenStateUploader
- (void)uploadSeenStateWithMedia:(id)arg1 {
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return;
%orig;
}
- (void)uploadSeenState {
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !(sciAllowedSeenPKs && sciAllowedSeenPKs.count > 0)) return;
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork()) return;
%orig;
}
- (void)_uploadSeenState:(id)arg1 {
@@ -73,16 +88,16 @@ static BOOL sciShouldBlockSeenVisualForObj(id obj) {
if (!sciSeenBypassActive && sciShouldBlockSeenNetwork() && !sciIsPKAllowed(arg1)) return;
%orig;
}
- (id)networker { return %orig; }
%end
// visual seen blocking + story auto-advance
// ============ Visual-seen hooks + auto-advance ============
%hook IGStoryFullscreenSectionController
- (void)markItemAsSeen:(id)arg1 { if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg1)) return; %orig; }
- (void)_markItemAsSeen:(id)arg1 { if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg1)) return; %orig; }
- (void)storySeenStateDidChange:(id)arg1 { if (sciShouldBlockSeenVisual()) return; %orig; }
- (void)sendSeenRequestForCurrentItem { if (sciShouldBlockSeenVisual()) return; %orig; }
- (void)markCurrentItemAsSeen { if (sciShouldBlockSeenVisual()) return; %orig; }
- (void)sendSeenRequestForCurrentItem { if (sciShouldBlockSeenNetwork()) return; %orig; }
- (void)storyPlayerMediaViewDidPlayToEnd:(id)arg1 {
if (!sciAdvanceBypassActive && [SCIUtils getBoolPref:@"stop_story_auto_advance"]) return;
%orig;
@@ -93,13 +108,6 @@ static BOOL sciShouldBlockSeenVisualForObj(id obj) {
}
%end
%hook IGStoryViewerViewController
- (void)fullscreenSectionController:(id)arg1 didMarkItemAsSeen:(id)arg2 {
if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg2)) return;
%orig;
}
%end
%hook IGStoryTrayViewModel
- (void)markAsSeen { if (sciShouldBlockSeenVisualForObj(self)) return; %orig; }
- (void)setHasUnseenMedia:(BOOL)arg1 { if (sciShouldBlockSeenVisualForObj(self)) { %orig(YES); return; } %orig; }
@@ -119,9 +127,7 @@ static BOOL sciShouldBlockSeenVisualForObj(id obj) {
- (void)updateRingForSeenState:(BOOL)arg1 { if (sciShouldBlockSeenVisual()) { %orig(NO); return; } %orig; }
%end
// ============ STORY LIKE HOOKS ============
// Hooks all known like entry points to trigger mark-seen and auto-advance on like.
// Uses sciMarkSeenTapped: from OverlayButtons.xm for the actual seen flow.
// ============ Active story VC tracking ============
__weak UIViewController *sciActiveStoryVC = nil;
@@ -132,95 +138,171 @@ __weak UIViewController *sciActiveStoryVC = nil;
}
- (void)viewWillDisappear:(BOOL)animated {
if (sciActiveStoryVC == (UIViewController *)self) sciActiveStoryVC = nil;
sciStateRestore();
%orig;
}
- (void)fullscreenSectionController:(id)arg1 didMarkItemAsSeen:(id)arg2 {
if (sciShouldBlockSeenVisual() && !sciIsPKAllowed(arg2)) return;
%orig;
}
%end
static UIView *sciFindStoryOverlayView(UIViewController *vc) {
if (!vc) return nil;
Class targetCls = NSClassFromString(@"IGStoryFullscreenOverlayView");
if (!targetCls) return nil;
NSMutableArray *stack = [NSMutableArray arrayWithObject:vc.view];
while (stack.count > 0) {
UIView *v = stack.lastObject;
[stack removeLastObject];
if ([v isKindOfClass:targetCls]) return v;
for (UIView *sub in v.subviews) [stack addObject:sub];
}
return nil;
// ============ Networker-ivar swap (v425+ split-mode) ============
static __weak id sciLegacyUploader = nil; // IGStorySeenStateUploader
static __weak id sciSundialManager = nil; // IGSundialSeenStateManager
static id (*orig_pendingStoreInit)(id, SEL, id, id, id, BOOL);
static id new_pendingStoreInit(id self, SEL _cmd, id sessionPK, id uploader, id fileMgr, BOOL bgTask) {
if (uploader) sciLegacyUploader = uploader;
return orig_pendingStoreInit(self, _cmd, sessionPK, uploader, fileMgr, bgTask);
}
static void sciMarkActiveStorySeen(void) {
if (![SCIUtils getBoolPref:@"seen_on_story_like"]) return;
UIView *overlay = sciFindStoryOverlayView(sciActiveStoryVC);
if (!overlay) return;
SEL sel = NSSelectorFromString(@"sciMarkSeenTapped:");
if ([overlay respondsToSelector:sel])
((void(*)(id, SEL, id))objc_msgSend)(overlay, sel, nil);
static id (*orig_sundialMgrInit)(id, SEL, id, id, id, id);
static id new_sundialMgrInit(id self, SEL _cmd, id networker, id diskMgr, id launcherSet, id announcer) {
id res = orig_sundialMgrInit(self, _cmd, networker, diskMgr, launcherSet, announcer);
if (res) sciSundialManager = res;
return res;
}
// Dedup guard — multiple hooks fire for the same like event
static uint64_t sciLastLikeAdvanceTime = 0;
static void sciAdvanceOnStoryLike(void) {
if (![SCIUtils getBoolPref:@"advance_on_story_like"]) return;
UIViewController *storyVC = sciActiveStoryVC;
if (!storyVC) return;
id sectionCtrl = sciFindSectionController(storyVC);
if (!sectionCtrl) return;
uint64_t now = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
if (now - sciLastLikeAdvanceTime < 500000000ULL) return;
sciLastLikeAdvanceTime = now;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
sciAdvanceBypassActive = YES;
SEL advSel = NSSelectorFromString(@"advanceToNextItemWithNavigationAction:");
if ([sectionCtrl respondsToSelector:advSel])
((void(*)(id, SEL, NSInteger))objc_msgSend)(sectionCtrl, advSel, 1);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
id sc2 = storyVC ? sciFindSectionController(storyVC) : nil;
if (sc2) {
SEL resumeSel = NSSelectorFromString(@"tryResumePlaybackWithReason:");
if ([sc2 respondsToSelector:resumeSel])
((void(*)(id, SEL, NSInteger))objc_msgSend)(sc2, resumeSel, 0);
// Swap each cached uploader's networker ivar; saved dict is used to restore.
static NSDictionary *sciSwapNetworkers(id newNetworker) {
NSMutableDictionary *saved = [NSMutableDictionary dictionary];
@try {
id legacy = sciLegacyUploader;
if (legacy) {
Ivar iv = class_getInstanceVariable([legacy class], "_networker");
if (iv) {
id old = object_getIvar(legacy, iv);
if (old) saved[@"legacy"] = old;
object_setIvar(legacy, iv, newNetworker);
}
sciAdvanceBypassActive = NO;
});
});
}
id mgr = sciSundialManager;
if (mgr) {
for (NSString *ivName in @[@"seenStateUploader", @"seenStateUploaderDeprecated"]) {
Ivar mgrIv = class_getInstanceVariable([mgr class], [ivName UTF8String]);
if (!mgrIv) continue;
id up = object_getIvar(mgr, mgrIv);
if (!up) continue;
Ivar netIv = class_getInstanceVariable([up class], "networker");
if (!netIv) continue;
id oldNet = object_getIvar(up, netIv);
if (oldNet) saved[ivName] = oldNet;
object_setIvar(up, netIv, newNetworker);
}
}
} @catch (__unused id e) {}
return saved;
}
static void sciOnStoryLike(void) {
sciMarkActiveStorySeen();
sciAdvanceOnStoryLike();
static void sciRestoreNetworkers(NSDictionary *saved) {
@try {
id legacy = sciLegacyUploader;
if (legacy && saved[@"legacy"]) {
Ivar iv = class_getInstanceVariable([legacy class], "_networker");
if (iv) object_setIvar(legacy, iv, saved[@"legacy"]);
}
id mgr = sciSundialManager;
if (mgr) {
for (NSString *ivName in @[@"seenStateUploader", @"seenStateUploaderDeprecated"]) {
if (!saved[ivName]) continue;
Ivar mgrIv = class_getInstanceVariable([mgr class], [ivName UTF8String]);
if (!mgrIv) continue;
id up = object_getIvar(mgr, mgrIv);
if (!up) continue;
Ivar netIv = class_getInstanceVariable([up class], "networker");
if (netIv) object_setIvar(up, netIv, saved[ivName]);
}
}
} @catch (__unused id e) {}
}
// Idempotent block/restore. Guard prevents double-swap clobbering the saved originals.
static BOOL sciNetBlocked = NO;
static NSDictionary *sciNetSaved = nil;
static void sciStateBlock(void) {
if (sciNetBlocked) return;
sciNetSaved = sciSwapNetworkers(nil);
sciNetBlocked = YES;
}
static void sciStateRestore(void) {
if (!sciNetBlocked) return;
sciRestoreNetworkers(sciNetSaved);
sciNetSaved = nil;
sciNetBlocked = NO;
}
static NSString *sciExtractOwnerPKFromItem(id item) {
NSString *pk = nil;
@try {
id reelPk = [item respondsToSelector:@selector(reelPk)] ? [item performSelector:@selector(reelPk)] : nil;
if (reelPk) pk = [reelPk description];
if (!pk) {
id media = [item respondsToSelector:@selector(media)] ? [item performSelector:@selector(media)] : item;
id user = [media respondsToSelector:@selector(user)] ? [media performSelector:@selector(user)] : nil;
if (!user) user = [media respondsToSelector:@selector(owner)] ? [media performSelector:@selector(owner)] : nil;
if (user) {
Ivar pkIvar = NULL;
for (Class c = [user class]; c && !pkIvar; c = class_getSuperclass(c))
pkIvar = class_getInstanceVariable(c, "_pk");
if (pkIvar) pk = [object_getIvar(user, pkIvar) description];
}
}
} @catch (__unused id e) {}
return pk;
}
// Mark-seen delegate: restore on non-blocked owners, block + run orig on
// blocked owners when split-mode is on, skip orig when it's off.
static void (*orig_delegateMarkSeen)(id, SEL, id, id);
static void new_delegateMarkSeen(id self, SEL _cmd, id ctrl, id item) {
if (sciSeenBypassActive) { sciStateRestore(); orig_delegateMarkSeen(self, _cmd, ctrl, item); return; }
if (![SCIUtils getBoolPref:@"no_seen_receipt"]) { sciStateRestore(); orig_delegateMarkSeen(self, _cmd, ctrl, item); return; }
NSString *ownerPK = sciExtractOwnerPKFromItem(item);
BOOL shouldBlock;
if ([SCIExcludedStoryUsers isFeatureEnabled])
shouldBlock = ownerPK.length && ![SCIExcludedStoryUsers isUserPKExcluded:ownerPK];
else
shouldBlock = YES;
if (!shouldBlock) {
sciStateRestore();
orig_delegateMarkSeen(self, _cmd, ctrl, item);
return;
}
if (![SCIUtils getBoolPref:@"keep_seen_visual_local"]) {
sciStateRestore();
return;
}
sciStateBlock();
@try { orig_delegateMarkSeen(self, _cmd, ctrl, item); }
@catch (__unused id e) { sciStateRestore(); }
}
// ============ Like → mark-seen side effects ============
static void (*orig_didLikeSundial)(id, SEL, id);
static void new_didLikeSundial(id self, SEL _cmd, id pk) {
orig_didLikeSundial(self, _cmd, pk);
sciOnStoryLike();
sciStoryInteractionSideEffects(SCIStoryInteractionLike);
}
static void (*orig_overlaySetIsLiked)(id, SEL, BOOL, BOOL);
static void new_overlaySetIsLiked(id self, SEL _cmd, BOOL isLiked, BOOL animated) {
orig_overlaySetIsLiked(self, _cmd, isLiked, animated);
if (isLiked) sciOnStoryLike();
}
// IGUFIButton selected state: YES = heart filled (liked), NO = empty (not liked).
// handleStoryLikeTapWithButton: is a toggle — check state before orig to determine direction.
static void (*orig_handleLikeTap)(id, SEL, id);
static void new_handleLikeTap(id self, SEL _cmd, id button) {
BOOL isLike = [button isKindOfClass:[UIButton class]] && [(UIButton *)button isSelected];
orig_handleLikeTap(self, _cmd, button);
if (isLike) sciOnStoryLike();
if (isLiked) sciStoryInteractionSideEffects(SCIStoryInteractionLike);
}
static void (*orig_likeButtonSetIsLiked)(id, SEL, BOOL, BOOL);
static void new_likeButtonSetIsLiked(id self, SEL _cmd, BOOL isLiked, BOOL animated) {
orig_likeButtonSetIsLiked(self, _cmd, isLiked, animated);
if (isLiked) sciOnStoryLike();
if (isLiked) sciStoryInteractionSideEffects(SCIStoryInteractionLike);
}
%ctor {
@@ -229,23 +311,39 @@ static void new_likeButtonSetIsLiked(id self, SEL _cmd, BOOL isLiked, BOOL anima
SEL didLike = NSSelectorFromString(@"didLikeSundialWithMediaPK:");
if (class_getInstanceMethod(overlayCtl, didLike))
MSHookMessageEx(overlayCtl, didLike, (IMP)new_didLikeSundial, (IMP *)&orig_didLikeSundial);
SEL setLiked = @selector(setIsLiked:animated:);
if (class_getInstanceMethod(overlayCtl, setLiked))
MSHookMessageEx(overlayCtl, setLiked, (IMP)new_overlaySetIsLiked, (IMP *)&orig_overlaySetIsLiked);
}
Class likesImpl = NSClassFromString(@"IGStoryLikesInteractionControllingImpl");
if (likesImpl) {
SEL handleTap = NSSelectorFromString(@"handleStoryLikeTapWithButton:");
if (class_getInstanceMethod(likesImpl, handleTap))
MSHookMessageEx(likesImpl, handleTap, (IMP)new_handleLikeTap, (IMP *)&orig_handleLikeTap);
}
Class likeBtn = NSClassFromString(@"IGSundialViewerUFI.IGSundialLikeButton");
if (likeBtn) {
SEL setLiked = @selector(setIsLiked:animated:);
if (class_getInstanceMethod(likeBtn, setLiked))
MSHookMessageEx(likeBtn, setLiked, (IMP)new_likeButtonSetIsLiked, (IMP *)&orig_likeButtonSetIsLiked);
}
Class pending = NSClassFromString(@"IGStoryPendingSeenStateStore");
SEL pendingSel = NSSelectorFromString(@"initWithUserSessionPK:uploader:fileManager:uploadInBackgroundTask:");
if (pending && class_getInstanceMethod(pending, pendingSel))
MSHookMessageEx(pending, pendingSel, (IMP)new_pendingStoreInit, (IMP *)&orig_pendingStoreInit);
Class sundialMgr = NSClassFromString(@"_TtC23IGSundialSeenStateSwift25IGSundialSeenStateManager");
SEL mgrSel = NSSelectorFromString(@"initWithNetworker:diskManager:launcherSet:seenStateManagerAnnouncer:");
if (sundialMgr && class_getInstanceMethod(sundialMgr, mgrSel))
MSHookMessageEx(sundialMgr, mgrSel, (IMP)new_sundialMgrInit, (IMP *)&orig_sundialMgrInit);
// Mark-as-seen delegate; extras are forward-compat candidates.
for (NSString *clsName in @[
@"IGStoryViewerViewController",
@"IGStoryViewerUpdater",
@"IGStoryFullscreenViewModel",
@"IGStoriesManager",
]) {
Class cls = NSClassFromString(clsName);
if (!cls) continue;
SEL delegateSel = NSSelectorFromString(@"fullscreenSectionController:didMarkItemAsSeen:");
if (class_getInstanceMethod(cls, delegateSel))
MSHookMessageEx(cls, delegateSel, (IMP)new_delegateMarkSeen, (IMP *)&orig_delegateMarkSeen);
}
}
@@ -139,13 +139,14 @@ NSDictionary *sciOwnerInfoForView(UIView *view) {
BOOL sciIsCurrentStoryOwnerExcluded(void) {
NSDictionary *info = sciCurrentStoryOwnerInfo();
if (!info) return NO;
// Unknown owner: block_selected → don't block; block_all → block.
if (!info) return [SCIExcludedStoryUsers isBlockSelectedMode];
return [SCIExcludedStoryUsers isUserPKExcluded:info[@"pk"]];
}
BOOL sciIsObjectStoryOwnerExcluded(id obj) {
NSDictionary *info = sciOwnerInfoFromObject(obj);
if (!info) return NO;
if (!info) return [SCIExcludedStoryUsers isBlockSelectedMode];
return [SCIExcludedStoryUsers isUserPKExcluded:info[@"pk"]];
}
@@ -314,50 +314,31 @@ static void sciResumeStoryPlayback(UIView *sourceView) {
[btn setImage:[UIImage systemImageNamed:icon withConfiguration:cfg] forState:UIControlStateNormal];
}
// Rebuilds the eye button (tag 1339) based on current owner + prefs. Idempotent.
// Rebuilds the eye button (tag 1339). Visible only when the story is
// actively blocked for this owner. List management lives in the hold menu
// and the ellipsis action menu.
%new - (void)sciRefreshSeenButton {
BOOL seenBlockingOn = [SCIUtils getBoolPref:@"no_seen_receipt"];
BOOL storyBlockSelected = [SCIExcludedStoryUsers isBlockSelectedMode];
// In block_selected mode, show the eye for list management even if global toggle is off
if (!seenBlockingOn && !storyBlockSelected) return;
// Skip for DM visual messages inside an excluded thread
NSString *activeTid = [SCIExcludedThreads activeThreadId];
if (activeTid && [SCIExcludedThreads isInList:activeTid] && ![SCIExcludedThreads isBlockSelectedMode]) return;
if (!seenBlockingOn) return;
NSDictionary *ownerInfo = sciOwnerInfoForView(self);
NSString *ownerPK = ownerInfo[@"pk"] ?: @"";
BOOL ownerInList = ownerPK.length && [SCIExcludedStoryUsers isInList:ownerPK];
// block_all + in list: show remove icon (excluded user, behaves normally)
// block_selected + in list: show normal eye (blocked user, needs mark-seen)
// block_selected + not in list: show add icon
BOOL showExcludeIcon = ownerInList && !storyBlockSelected;
BOOL showAddIcon = storyBlockSelected && !ownerInList;
BOOL listBtnPref = [SCIUtils getBoolPref:@"story_excluded_show_unexclude_eye"];
BOOL hideForListedOwner = (showExcludeIcon || showAddIcon) && !listBtnPref;
BOOL toggleMode = [[SCIUtils getStringPref:@"story_seen_mode"] isEqualToString:@"toggle"];
BOOL excluded = ownerPK.length && [SCIExcludedStoryUsers isUserPKExcluded:ownerPK];
UIButton *existing = (UIButton *)[self viewWithTag:1339];
// Not blocked → no eye button.
if (excluded) { [existing removeFromSuperview]; return; }
BOOL toggleMode = [[SCIUtils getStringPref:@"story_seen_mode"] isEqualToString:@"toggle"];
NSString *symName;
UIColor *tint;
if (showExcludeIcon) {
// block_all + in list: remove-from-exclude icon
symName = @"eye.slash.fill"; tint = SCIUtils.SCIColor_Primary;
} else if (storyBlockSelected && !ownerInList) {
// block_selected + not in list: add-to-block icon
symName = @"eye.slash"; tint = [UIColor whiteColor];
} else if (toggleMode) {
if (toggleMode) {
symName = sciStorySeenToggleEnabled ? @"eye.fill" : @"eye";
tint = sciStorySeenToggleEnabled ? SCIUtils.SCIColor_Primary : [UIColor whiteColor];
} else {
symName = @"eye"; tint = [UIColor whiteColor];
}
UIButton *existing = (UIButton *)[self viewWithTag:1339];
if (hideForListedOwner) {
[existing removeFromSuperview];
return;
}
UIImageSymbolConfiguration *cfg = [UIImageSymbolConfiguration configurationWithPointSize:18 weight:UIImageSymbolWeightSemibold];
if (existing) {
@@ -444,57 +425,6 @@ static void sciResumeStoryPlayback(UIView *sourceView) {
// ============ Seen button tap ============
%new - (void)sciSeenButtonTapped:(UIButton *)sender {
NSDictionary *ownerInfo = sciOwnerInfoForView(self);
NSString *ownerPK = ownerInfo[@"pk"];
BOOL inList = ownerPK && [SCIExcludedStoryUsers isInList:ownerPK];
BOOL bs = [SCIExcludedStoryUsers isBlockSelectedMode];
// Block selected + not in list: tap to ADD to block list (with confirmation)
if (bs && !inList && ownerPK) {
UIViewController *host = [SCIUtils nearestViewControllerForView:self];
UIAlertController *alert = [UIAlertController
alertControllerWithTitle:SCILocalized(@"Add to block list?")
message:[NSString stringWithFormat:SCILocalized(@"Story seen receipts will be blocked for @%@."), ownerInfo[@"username"] ?: @""]
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Add") style:UIAlertActionStyleDefault handler:^(UIAlertAction *_) {
[SCIExcludedStoryUsers addOrUpdateEntry:@{
@"pk": ownerPK,
@"username": ownerInfo[@"username"] ?: @"",
@"fullName": ownerInfo[@"fullName"] ?: @""
}];
[SCIUtils showToastForDuration:2.0 title:SCILocalized(@"Added to block list")];
sciRefreshAllVisibleOverlays(sciActiveStoryViewerVC);
}]];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
[host presentViewController:alert animated:YES completion:nil];
return;
}
// Block selected + in list: blocked story, tap = mark seen (long-press to remove)
if (bs && inList) {
((void(*)(id, SEL, id))objc_msgSend)(self, @selector(sciMarkSeenTapped:), sender);
return;
}
// Block all + in list: tap to remove from exclude list
if (inList) {
UIViewController *host = [SCIUtils nearestViewControllerForView:self];
NSString *alertTitle = bs ? SCILocalized(@"Remove from block list?") : SCILocalized(@"Un-exclude story seen?");
NSString *alertMsg = bs ? [NSString stringWithFormat:@"@%@ will no longer have seen receipts blocked.", ownerInfo[@"username"] ?: @""]
: [NSString stringWithFormat:@"@%@ will resume normal story-seen blocking.", ownerInfo[@"username"] ?: @""];
UIAlertController *alert = [UIAlertController
alertControllerWithTitle:alertTitle message:alertMsg preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:bs ? SCILocalized(@"Unblock") : SCILocalized(@"Un-exclude") style:UIAlertActionStyleDestructive handler:^(UIAlertAction *_) {
[SCIExcludedStoryUsers removePK:ownerPK];
[SCIUtils showToastForDuration:2.0 title:bs ? SCILocalized(@"Unblocked") : SCILocalized(@"Un-excluded")];
if (bs) sciTriggerStoryMarkSeen(sciActiveStoryViewerVC);
sciRefreshAllVisibleOverlays(sciActiveStoryViewerVC);
}]];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
[host presentViewController:alert animated:YES completion:nil];
return;
}
// Toggle mode
if ([[SCIUtils getStringPref:@"story_seen_mode"] isEqualToString:@"toggle"]) {
sciStorySeenToggleEnabled = !sciStorySeenToggleEnabled;
@@ -0,0 +1,17 @@
// Story interaction pipeline — confirm gate + seen/advance per policy table.
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSInteger, SCIStoryInteraction) {
SCIStoryInteractionLike,
SCIStoryInteractionEmojiReaction,
SCIStoryInteractionTextReply,
};
void sciStoryInteraction(SCIStoryInteraction type,
void (^action)(void),
void (^_Nullable uiRevert)(void),
void (^_Nullable uiReapply)(void));
// Side-effects only (seen/advance). No confirm, no action.
void sciStoryInteractionSideEffects(SCIStoryInteraction type);
@@ -0,0 +1,131 @@
#import "SCIStoryInteractionPipeline.h"
#import "StoryHelpers.h"
#import "../../Utils.h"
#import <objc/message.h>
#import <mach/mach_time.h>
extern __weak UIViewController *sciActiveStoryVC;
extern BOOL sciAdvanceBypassActive;
#pragma mark - Policy table
typedef struct {
NSString *confirmPref;
NSString *seenPref;
NSString *advancePref;
NSTimeInterval advanceDelay;
} SCIStoryPolicy;
static SCIStoryPolicy sciPolicyForType(SCIStoryInteraction type) {
switch (type) {
case SCIStoryInteractionLike:
return (SCIStoryPolicy){
@"story_like_confirm",
@"seen_on_story_like",
@"advance_on_story_like",
0.3
};
case SCIStoryInteractionEmojiReaction:
return (SCIStoryPolicy){
@"emoji_reaction_confirm",
@"seen_on_story_reply",
@"advance_on_story_reply",
0.4
};
case SCIStoryInteractionTextReply:
return (SCIStoryPolicy){
nil,
@"seen_on_story_reply",
@"advance_on_story_reply",
0.4
};
}
return (SCIStoryPolicy){ nil, nil, nil, 0.3 };
}
#pragma mark - Side effects
static UIView *sciFindOverlay(UIViewController *vc) {
if (!vc) return nil;
Class cls = NSClassFromString(@"IGStoryFullscreenOverlayView");
if (!cls) return nil;
NSMutableArray *stack = [NSMutableArray arrayWithObject:vc.view];
while (stack.count) {
UIView *v = stack.lastObject;
[stack removeLastObject];
if ([v isKindOfClass:cls]) return v;
for (UIView *s in v.subviews) [stack addObject:s];
}
return nil;
}
static void sciMarkSeen(NSString *prefKey) {
if (!prefKey || ![SCIUtils getBoolPref:prefKey]) return;
UIView *overlay = sciFindOverlay(sciActiveStoryVC);
if (!overlay) return;
SEL sel = NSSelectorFromString(@"sciMarkSeenTapped:");
if ([overlay respondsToSelector:sel])
((void(*)(id, SEL, id))objc_msgSend)(overlay, sel, nil);
}
static uint64_t sciLastAdvanceTime = 0;
static void sciAdvance(NSString *prefKey, NSTimeInterval delay) {
if (!prefKey || ![SCIUtils getBoolPref:prefKey]) return;
UIViewController *vc = sciActiveStoryVC;
if (!vc) return;
id ctrl = sciFindSectionController(vc);
if (!ctrl) return;
uint64_t now = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
if (now - sciLastAdvanceTime < 500000000ULL) return;
sciLastAdvanceTime = now;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
sciAdvanceBypassActive = YES;
SEL advSel = NSSelectorFromString(@"advanceToNextItemWithNavigationAction:");
if ([ctrl respondsToSelector:advSel])
((void(*)(id, SEL, NSInteger))objc_msgSend)(ctrl, advSel, 1);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
id c2 = vc ? sciFindSectionController(vc) : nil;
if (c2) {
SEL resumeSel = NSSelectorFromString(@"tryResumePlaybackWithReason:");
if ([c2 respondsToSelector:resumeSel])
((void(*)(id, SEL, NSInteger))objc_msgSend)(c2, resumeSel, 0);
}
sciAdvanceBypassActive = NO;
});
});
}
static void sciFireSideEffects(SCIStoryPolicy policy) {
sciMarkSeen(policy.seenPref);
sciAdvance(policy.advancePref, policy.advanceDelay);
}
#pragma mark - Pipeline
void sciStoryInteraction(SCIStoryInteraction type,
void (^action)(void),
void (^_Nullable uiRevert)(void),
void (^_Nullable uiReapply)(void)) {
SCIStoryPolicy policy = sciPolicyForType(type);
if (policy.confirmPref && [SCIUtils getBoolPref:policy.confirmPref]) {
if (uiRevert) uiRevert();
[SCIUtils showConfirmation:^{
if (uiReapply) uiReapply();
if (action) action();
sciFireSideEffects(policy);
}];
return;
}
if (action) action();
sciFireSideEffects(policy);
}
void sciStoryInteractionSideEffects(SCIStoryInteraction type) {
sciFireSideEffects(sciPolicyForType(type));
}
@@ -197,7 +197,7 @@ static UIMenu *sciBuildThreadActionsMenu(UIView *anchor, NSString *threadId, UIW
if (w.isKeyWindow) { win = w; break; }
}
}
[SCIUtils showSettingsVC:win atTopLevelEntry:@"Messages"];
[SCIUtils showSettingsVC:win atTopLevelEntry:SCILocalized(@"Messages")];
}];
[items addObject:openSettings];
@@ -1,5 +1,6 @@
// Mark seen + advance when replying or reacting to a story.
// Story reply + emoji reaction hooks. Routes through the interaction pipeline.
#import "SCIStoryInteractionPipeline.h"
#import "../../Utils.h"
#import "StoryHelpers.h"
#import <objc/message.h>
@@ -9,106 +10,43 @@
extern __weak UIViewController *sciActiveStoryVC;
extern BOOL sciAdvanceBypassActive;
static UIView *sciFindOverlayForStoryVC(UIViewController *vc) {
if (!vc) return nil;
Class overlayCls = NSClassFromString(@"IGStoryFullscreenOverlayView");
if (!overlayCls) return nil;
NSMutableArray *stack = [NSMutableArray arrayWithObject:vc.view];
while (stack.count) {
UIView *v = stack.lastObject;
[stack removeLastObject];
if ([v isKindOfClass:overlayCls]) return v;
for (UIView *s in v.subviews) [stack addObject:s];
}
return nil;
}
static void sciMarkSeenOnReply(void) {
if (![SCIUtils getBoolPref:@"seen_on_story_reply"]) return;
UIView *overlay = sciFindOverlayForStoryVC(sciActiveStoryVC);
if (!overlay) return;
SEL sel = @selector(sciMarkSeenTapped:);
if ([overlay respondsToSelector:sel])
((void(*)(id, SEL, id))objc_msgSend)(overlay, sel, nil);
}
static uint64_t sciLastReplyAdvanceTime = 0;
static void sciAdvanceOnReply(void) {
if (![SCIUtils getBoolPref:@"advance_on_story_reply"]) return;
UIViewController *storyVC = sciActiveStoryVC;
if (!storyVC) return;
id sectionCtrl = sciFindSectionController(storyVC);
if (!sectionCtrl) return;
// Dedup across multiple hooks firing for the same event
uint64_t now = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
if (now - sciLastReplyAdvanceTime < 500000000ULL) return;
sciLastReplyAdvanceTime = now;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
sciAdvanceBypassActive = YES;
SEL advSel = NSSelectorFromString(@"advanceToNextItemWithNavigationAction:");
if ([sectionCtrl respondsToSelector:advSel])
((void(*)(id, SEL, NSInteger))objc_msgSend)(sectionCtrl, advSel, 1);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
id sc2 = storyVC ? sciFindSectionController(storyVC) : nil;
if (sc2) {
SEL resumeSel = NSSelectorFromString(@"tryResumePlaybackWithReason:");
if ([sc2 respondsToSelector:resumeSel])
((void(*)(id, SEL, NSInteger))objc_msgSend)(sc2, resumeSel, 0);
}
sciAdvanceBypassActive = NO;
});
});
}
static void sciOnStoryReply(void) {
sciMarkSeenOnReply();
sciAdvanceOnReply();
}
// Text reply — IGDirectComposer is shared with DMs, gate by active story VC.
%hook IGDirectComposer
- (void)_didTapSend:(id)arg {
%orig;
if (sciActiveStoryVC) sciOnStoryReply();
if (sciActiveStoryVC) sciStoryInteraction(SCIStoryInteractionTextReply, nil, nil, nil);
}
- (void)_send {
%orig;
if (sciActiveStoryVC) sciOnStoryReply();
if (sciActiveStoryVC) sciStoryInteraction(SCIStoryInteractionTextReply, nil, nil, nil);
}
%end
// Composer emoji reaction buttons (forwarded to the Swift footer delegate)
// Composer emoji reaction buttons
static void (*orig_footerEmojiQuick)(id, SEL, id, id);
static void new_footerEmojiQuick(id self, SEL _cmd, id inputView, id btn) {
orig_footerEmojiQuick(self, _cmd, inputView, btn);
sciOnStoryReply();
sciStoryInteraction(SCIStoryInteractionEmojiReaction,
^{ orig_footerEmojiQuick(self, _cmd, inputView, btn); }, nil, nil);
}
static void (*orig_footerEmojiReaction)(id, SEL, id, id);
static void new_footerEmojiReaction(id self, SEL _cmd, id inputView, id btn) {
orig_footerEmojiReaction(self, _cmd, inputView, btn);
sciOnStoryReply();
sciStoryInteraction(SCIStoryInteractionEmojiReaction,
^{ orig_footerEmojiReaction(self, _cmd, inputView, btn); }, nil, nil);
}
// Swipe-up quick reactions tray
// Swipe-up quick reactions. qrCtrl → qrDelegate internally, gate only qrCtrl.
static void (*orig_qrCtrlDidTapEmoji)(id, SEL, id, id, id);
static void new_qrCtrlDidTapEmoji(id self, SEL _cmd, id view, id sourceBtn, id emoji) {
orig_qrCtrlDidTapEmoji(self, _cmd, view, sourceBtn, emoji);
sciOnStoryReply();
sciStoryInteraction(SCIStoryInteractionEmojiReaction,
^{ orig_qrCtrlDidTapEmoji(self, _cmd, view, sourceBtn, emoji); }, nil, nil);
}
static void (*orig_qrDelegateDidTapEmoji)(id, SEL, id, id, id);
static void new_qrDelegateDidTapEmoji(id self, SEL _cmd, id ctrl, id sourceBtn, id emoji) {
orig_qrDelegateDidTapEmoji(self, _cmd, ctrl, sourceBtn, emoji);
sciOnStoryReply();
}
// Swift classes aren't guaranteed to be registered at %ctor time — install
// lazily on first overlay appearance as a fallback.
static void sciInstallReplyHooks(void) {
static BOOL installed = NO;
if (installed) return;
@@ -0,0 +1,39 @@
// Story like button hook. Routes through the interaction pipeline.
#import "SCIStoryInteractionPipeline.h"
#import "../../Utils.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <substrate.h>
static void (*orig_sciStoryLikeTap)(id, SEL, id);
static void new_sciStoryLikeTap(id self, SEL _cmd, id button) {
BOOL isSelected = [button isKindOfClass:[UIButton class]] ? [(UIButton *)button isSelected] : NO;
if (!isSelected) { orig_sciStoryLikeTap(self, _cmd, button); return; }
UIButton *btn = (UIButton *)button;
SEL setLiked = NSSelectorFromString(@"setIsLiked:animated:");
sciStoryInteraction(SCIStoryInteractionLike,
^{ orig_sciStoryLikeTap(self, _cmd, button); },
^{
[UIView performWithoutAnimation:^{
[btn setSelected:NO];
if ([btn respondsToSelector:setLiked])
((void(*)(id, SEL, BOOL, BOOL))objc_msgSend)(btn, setLiked, NO, NO);
}];
},
^{
[btn setSelected:YES];
if ([btn respondsToSelector:setLiked])
((void(*)(id, SEL, BOOL, BOOL))objc_msgSend)(btn, setLiked, YES, YES);
});
}
%ctor {
Class cls = NSClassFromString(@"IGStoryLikesInteractionControllingImpl");
if (!cls) return;
SEL sel = NSSelectorFromString(@"handleStoryLikeTapWithButton:");
if (!class_getInstanceMethod(cls, sel)) return;
MSHookMessageEx(cls, sel, (IMP)new_sciStoryLikeTap, (IMP *)&orig_sciStoryLikeTap);
}
@@ -229,6 +229,10 @@
"Adds a RyukGram action button above the reel sidebar with view-cover/download/share/copy/expand/repost entries. Tap opens the menu by default; change the tap behavior below." = "Adds a RyukGram action button above the reel sidebar with view-cover/download/share/copy/expand/repost entries. Tap opens the menu by default; change the tap behavior below.";
"Always show progress scrubber" = "Always show progress scrubber";
"Auto-scroll reels" = "Auto-scroll reels";
"IG default: native behavior. RyukGram: re-advances after swiping back." = "IG default: native behavior. RyukGram: re-advances after swiping back.";
"IG default" = "IG default";
"RyukGram" = "RyukGram";
"Change what happens when you tap on a reel" = "Change what happens when you tap on a reel";
"Confirm reel refresh" = "Confirm reel refresh";
"Disable auto-unmuting reels" = "Disable auto-unmuting reels";
@@ -315,7 +319,7 @@
"Hides the functionality to create/send instants" = "Hides the functionality to create/send instants";
"Hides the notification for others when you view their story" = "Hides the notification for others when you view their story";
"Inserts a button next to the seen/eye button on story overlays" = "Inserts a button next to the seen/eye button on story overlays";
"Keep stories visually unseen" = "Keep stories visually unseen";
"Keep stories visually seen locally" = "Keep stories visually seen locally";
"Liking a story automatically advances to the next one after a short delay" = "Liking a story automatically advances to the next one after a short delay";
"Manage list" = "Manage list";
"Manage list (%lu)" = "Manage list (%lu)";
@@ -327,7 +331,7 @@
"Master toggle. When off, the list is ignored" = "Master toggle. When off, the list is ignored";
"Other" = "Other";
"Playback" = "Playback";
"Prevents stories from visually marking as seen in the tray (keeps colorful ring)" = "Prevents stories from visually marking as seen in the tray (keeps colorful ring)";
"Marks stories as seen locally (grey ring) while still blocking the seen receipt on the server" = "Marks stories as seen locally (grey ring) while still blocking the seen receipt on the server";
"Quick list button in stories" = "Quick list button in stories";
"Search, sort, swipe to remove" = "Search, sort, swipe to remove";
"Seen receipts" = "Seen receipts";
@@ -466,13 +470,18 @@
"Confirm changing theme" = "Confirm changing theme";
"Confirm follow" = "Confirm follow";
"Confirm follow requests" = "Confirm follow requests";
"Confirm like: Posts/Stories" = "Confirm like: Posts/Stories";
"Confirm like: Posts" = "Confirm like: Posts";
"Confirm like: Reels" = "Confirm like: Reels";
"Confirm posting comment" = "Confirm posting comment";
"Confirm repost" = "Confirm repost";
"Confirm shh mode" = "Confirm shh mode";
"Confirm sticker interaction" = "Confirm sticker interaction";
"Confirm story emoji reaction" = "Confirm story emoji reaction";
"Confirm story like" = "Confirm story like";
"Confirm unfollow" = "Confirm unfollow";
"Shows an alert before sending an emoji reaction on a story" = "Shows an alert before sending an emoji reaction on a story";
"Shows an alert when you click the like button on posts to confirm the like" = "Shows an alert when you click the like button on posts to confirm the like";
"Shows an alert when you click the like button on stories to confirm the like" = "Shows an alert when you click the like button on stories to confirm the like";
"Confirm voice messages" = "Confirm voice messages";
"Shows an alert to confirm before sending a voice message" = "Shows an alert to confirm before sending a voice message";
"Shows an alert to confirm before toggling disappearing messages" = "Shows an alert to confirm before toggling disappearing messages";
@@ -739,15 +748,21 @@
"Set location" = "Set location";
"Settings…" = "Settings…";
"Type emoji..." = "Type emoji...";
"direct-inbox-tab" = "direct-inbox-tab";
"mainfeed-tab" = "mainfeed-tab";
//////////////////////////////////////////////////////////////////////////////
// SETTINGS VIEWS & DIALOGS //
// Excluded-lists managers, backup/restore flows, in-picker labels. //
//////////////////////////////////////////////////////////////////////////////
"Add chat" = "Add chat";
"Add custom domain" = "Add custom domain";
"Add to list?" = "Add to list?";
"Add user" = "Add user";
"Could not resolve user ID" = "Could not resolve user ID";
"Enter username" = "Enter username";
"Enter username of the DM thread" = "Enter username of the DM thread";
"No DM thread found with @%@" = "No DM thread found with @%@";
"User '%@' not found" = "User '%@' not found";
"Add preset…" = "Add preset…";
"All RyukGram settings will be reset to defaults and the imported values applied. The app will need to restart for some changes to take effect." = "All RyukGram settings will be reset to defaults and the imported values applied. The app will need to restart for some changes to take effect.";
"Apply" = "Apply";
@@ -902,4 +917,10 @@
//////////////////////////////////////////////////////////////////////////////
"Navigation Cell" = "Navigation Cell";
"Localization" = "Localization";
"Update localization file" = "Update localization file";
"Import a .strings file for a language" = "Import a .strings file for a language";
"Import a .strings file to update a translation. Pick a language, select the file, restart." = "Import a .strings file to update a translation. Pick a language, select the file, restart.";
"Export English strings" = "Export English strings";
"Share the base English .strings file for translating" = "Share the base English .strings file for translating";
@@ -0,0 +1,930 @@
// Spanish translation by Furamako (@furamako)
//
/*
* RyukGram — Localizable.strings (Spanish)
* -------------------------------------------------------------------------
*
* Every user-facing string in RyukGram goes through the macro
* SCILocalized(@"English text here")
* in the Objective-C source. The argument is BOTH the lookup key and the
* English fallback, so if a translation is missing the user still sees
* clean English — nothing ever breaks.
*
*
* HOW TO ADD A NEW LANGUAGE
* -------------------------------------------------------------------------
*
* 1. Copy this file into a new folder named after the language code:
* src/Localization/Resources/<code>.lproj/Localizable.strings
* e.g. ar.lproj (Arabic)
* es.lproj (Spanish)
* fr.lproj (French)
* 2. Translate the RIGHT-hand side of every `"key" = "value";` line.
* Do NOT touch the left-hand side — that is the lookup key and must
* stay identical to the English version, otherwise the app will never
* find your translation.
* 3. Keep every format specifier (%@, %lu, %d, %lld, %1$@, …) exactly
* as-is, in the same order. If you need to reorder them, switch to
* positional specifiers (%1$@ %2$lu).
* 4. Keep embedded quotes escaped with a backslash: \" — and newlines
* as \n.
* 5. Open a pull request at https://github.com/faroukbmiled/RyukGram/pulls
* so we can ship the language in the next release.
*
*
* HOW TO ADD A NEW STRING IN CODE
* -------------------------------------------------------------------------
*
* Just wrap the English text with SCILocalized(...) in the .m / .x / .xm
* file — the helper resolves to the English text automatically when no
* translation exists. Then add the same English text as BOTH the key and
* the value inside the matching section below, e.g.
*
* "Download all items" = "Download all items";
*
* Translators copy that line into their own .lproj and translate only the
* right-hand side.
*
*
* FILE FORMAT NOTES
* -------------------------------------------------------------------------
*
* - UTF-8, LF line endings.
* - Slash-star block comments and double-slash line comments both work.
* - DO NOT nest one slash-star block comment inside another — the
* parser will close the outer block at the first inner close marker
* and every lookup in the file will silently fail.
* - Keys and values are both quoted; every line ends with a semicolon.
*/
//////////////////////////////////////////////////////////////////////////////
// CHROME — TOP BAR, LANGUAGE PICKER, FIRST-RUN //
// Shown on the root Settings screen: title, search bar, the globe language //
// menu, and the one-time welcome alert. These use dotted keys (settings.*) //
// and are hand-authored rather than extracted from English source. //
//////////////////////////////////////////////////////////////////////////////
"settings.firstrun.message" = "Para el futuro: Mantener pulsadas las tres líneas en la parte superior derecha en la página de perfil, para volver a abrir la configuración de RyukGram";
"settings.firstrun.ok" = "Entiendo!";
"settings.firstrun.title" = "Información de configuración de RyukGram";
"settings.language.system" = "Por defecto del sistema";
"settings.language.title" = "Idioma";
/* [ADDED_BY_DEV] */ "settings.language.english_only" = "Por el momento, RyukGram solo está disponible en Inglés. ¡Las traducciones son bienvenidas!";
/* [ADDED_BY_DEV] */ "settings.language.help_translate" = "Ayudar a traducir";
/* [ADDED_BY_DEV] */ "settings.language.ok" = "OK";
"settings.results.many" = "%lu resultados";
"settings.results.none" = "Sin resultados";
"settings.results.one" = "%lu resultado";
"settings.search.placeholder" = "Configuración de búsqueda";
"settings.title" = "Configuración de RyukGram";
//////////////////////////////////////////////////////////////////////////////
// GENERAL //
// Settings → General tab //
//////////////////////////////////////////////////////////////////////////////
"Adds a copy option to the comment long-press menu" = "Añade la opción de copiar en el menú que aparece al mantener pulsado un comentario";
"Adds a download option for GIF comments" = "Añade la opción de descargar los GIF en comentarios";
"Browser" = "Navegador";
"Comments" = "Comentarios";
"Copy comment text" = "Copiar texto del comentario";
"Copy description" = "Copiar descripción";
"Copy description text fields by long-pressing on them" = "Copia la descripción al mantenerla pulsada";
"Date format" = "Formato de fecha";
"Disable app haptics" = "Deshabilitar respuesta háptica de la aplicación";
"Disables haptics/vibrations within the app" = "Deshabilita la respuesta háptica y vibraciones dentro de la aplicación";
"Do not save recent searches" = "No guardar búsquedas recientes";
/* [ADDED_BY_DEV] */ "Search bars will no longer save your recent searches" = "Las barras de búsqueda ya no guardarán tus búsquedas recientes";
"Download GIF comments" = "Descargar GIF en comentarios";
"Embed domain" = "Dominio embebido";
"Embed domain: %@" = "Dominio embebido: %@";
"Enable liquid glass buttons" = "Habilitar botones Liquid Glass";
"Enable liquid glass surfaces" = "Habilitar superficies Liquid Glass";
"Enable teen app icons" = "Habilitar íconos para adolescentes";
"Enables experimental liquid glass buttons" = "Habilita botones experimentales Liquid Glass";
"Enables liquid glass tab bar, floating navigation, and other UI elements" = "Habilita Liquid Glass en la barra de pestañas, navegación flotante y otros elementos de la interfaz";
"Experimental features" = "Funciones experimentales";
"Focus/distractions" = "Concentración/Distracciones";
"General" = "General";
"Hide Meta AI" = "Ocultar Meta AI";
"Hide ads" = "Ocultar anuncios";
"Hide explore posts grid" = "Ocultar la cuadrícula de publicaciones";
"Hide friends map" = "Ocultar el mapa de amigos";
"Hide metrics" = "Ocultar métricas";
"Hide notes tray" = "Ocultar bandeja de notas";
"Hide trending searches" = "Ocultar búsquedas en tendencia";
"Hides all suggested users for you to follow, outside your feed" = "Oculta 'Sugerencias para ti' en tu Feed (Inicio)";
"Hides like/comment/share counts on posts and reels" = "Oculta el contador de me gusta, comentarios y compartidos en publicaciones y reels";
"Hides the friends map icon in the notes tray" = "Oculta el ícono de mapa de amigos en la bandeja de notas";
"Hides the grid of suggested posts on the explore/search tab" = "Oculta la cuadrícula de publicaciones sugeridas en la pestaña de exploración/búsqueda";
"Hides the meta ai buttons/functionality within the app" = "Oculta los botones y funcionalidad de Meta AI dentro de la aplicación";
"Hides the notes tray in the DM inbox" = "Oculta la bandeja de notas en la pestaña Mensajes";
"Hides the suggested broadcast channels in direct messages" = "Oculta los canales sugeridos en mensajes";
"Hides the trending searches under the explore search bar" = "Oculta las búsquedas en tendencia debajo de la barra de búsqueda";
"Hold down on the Instagram logo to change the app icon" = "Mantén pulsado el logo de Instagram para cambiar el ícono de la aplicación";
"Long press on the eyedropper tool in stories to customize the text color more precisely" = "Mantener pulsada la herramienta de selección de color en historias para seleccionar el color del texto de manera más precisa";
"No suggested chats" = "Ocultar conversaciones sugeridas";
"No suggested users" = "Ocultar usuarios sugeridos";
"Notes" = "Notas";
"Open links in external browser" = "Abrir enlaces en navegador externo";
"Opens links in Safari instead of Instagram's in-app browser" = "Abrir enlaces en Safari en vez del navegador interno de Instagram";
"Removes Instagram tracking wrappers (l.instagram.com) and UTM/fbclid params from URLs" = "Elimina intermediarios de rastreo de Instagram (l.instagram.com) y los parámetros UTM/fbclid de los enlaces";
"Removes all ads from the Instagram app" = "Elimina todos los anuncios de la aplicación de Instagram";
"Removes igsh, utm_source, and other tracking parameters from shared links" = "Elimina igsh, utm_source, y otros parámetros de rastreo de los enlaces compartidos";
"Replace IG's relative timestamps (\"3d ago\") with a custom format. Toggle which surfaces it applies to inside the picker." = "Reemplaza las marcas de tiempo relativas de Instagram (\"Hace 3d\") con un formato personalizado. Escoge sobre cuales superficies se aplica dentro del selector.";
"Replace domain in shared links" = "Reemplazar dominio en enlaces compartidos";
"Rewrites copied/shared links to use an embed-friendly domain for previews in Discord, Telegram, etc." = "Reescribe enlaces copiados/compartidos para utilizar un dominio compatible con vistas previas embebidas en Discord, Telegram, etc.";
"Sharing" = "Compartir";
"Strip tracking from links" = "Eliminar rastreo de los enlaces";
"Strip tracking params" = "Eliminar parámetros de rastreo";
"These features rely on hidden Instagram flags and may not work on all accounts or versions.\nExperimental flags research by @euoradan (Radan)." = "Estas funciones se basan en opciones ocultas de Instagram y es posible que no funcionen en todas las cuentas o versiones.\nInvestigación sobre opciones experimentales por @euoradan (Radan).";
"Use detailed color picker" = "Usar selector de color detallado";
//////////////////////////////////////////////////////////////////////////////
// DATE FORMAT //
// Settings → Date format tab //
//////////////////////////////////////////////////////////////////////////////
"Alternate" = "Alternativo";
"Always ask" = "Siempre preguntar";
"Balanced" = "Balanceada";
"Block all" = "Bloquear todo";
"Block selected" = "Bloquear seleccionado";
"Button" = "Botón";
"Classic" = "Clásico";
"Date format — %@" = "Formato de fecha — %@";
"Default" = "Predeterminado";
"Disabled" = "Desactivado";
"Download and share" = "Descargar y compartir";
"Download to Photos" = "Descargar a Fotos";
"Enabled" = "Activado";
"Expand" = "Ampliar";
"Explore" = "Explorar";
"Fast" = "Rápida";
"Feed" = "Feed (Inicio)";
"High" = "Alta";
"Inbox" = "Bandeja de entrada";
"Low" = "Baja";
"Max" = "Máxima";
"Medium" = "Media";
"Mute/Unmute" = "Silencio/Sonido";
"Open menu" = "Abrir menú";
"Pause/Play" = "Pausar/Reproducir";
"Profile" = "Perfil";
"Quality" = "Calidad";
"Reels" = "Reels";
"Requires restart" = "Requiere reiniciar";
"Save to Photos" = "Guardar en Fotos";
"Share sheet" = "Menú de compartir";
"Standard" = "Estándar";
"Toggle" = "Interruptor";
//////////////////////////////////////////////////////////////////////////////
// FEED //
// Settings → Feed tab //
//////////////////////////////////////////////////////////////////////////////
"Action button" = "Botón de acción";
"Adds 'View profile picture' and 'View cover' to story tray long-press menus" = "Añade las opciones 'Ver foto de perfil' y 'Ver portada' al menú que aparece al mantener pulsado en la bandeja de historias";
"Adds a RyukGram action button under each feed post with download/share/copy/expand/repost entries. Tap opens the menu by default; change the tap behavior below." = "Añade un botón de acción de RyukGram debajo de cada publicación en el Feed (Inicio), con las opciones descargar, compartir, copiar, ampliar y repost. Tocar abre el menú de forma predeterminada.\nArriba puedes cambiar el comportamiento al tocar.";
"Controls when and how the feed refreshes. Background refresh occurs when returning to the app after ~10 minutes. Home button refresh occurs when tapping the Home tab while already on it." = "Controla cuando y como se actualiza el Feed (Inicio). La actualización en segundo plano ocurre cuando vuelves a la aplicación después de unos 10 minutos. La actualización al pulsar el botón de Inicio se produce al tocar el botón mientras te encuentras en el Feed (Inicio)";
"Default tap action" = "Acción al tocar";
"Disable background refresh" = "Deshabilitar actualización en segundo plano";
"Disable home button refresh" = "Deshabilitar actualización con botón de Inicio";
"Disable home button scroll" = "Deshabilitar desplazamiento con botón de Inicio";
"Disable video autoplay" = "Deshabilitar reproducción automática de video";
"Hide" = "Ocultar";
"Hide entire feed" = "Ocultar todo el Feed (Inicio)";
"Hide repost button" = "Ocultar botón de repost";
"Hide stories tray" = "Ocultar bandeja de historias";
"Hide suggested stories" = "Ocultar historias sugeridas";
"Hides suggested accounts" = "Oculta las cuentas sugeridas";
"Hides suggested reels" = "Oculta los reels sugeridos";
"Hides suggested threads posts" = "Oculta los hilos sugeridos de Threads";
"Hides the repost button on feed posts" = "Oculta el botón de repost en las publicaciones del Feed (Inicio)";
"Hides the story tray at the top" = "Oculta la bandeja de historias en la parte superior";
"Inserts a button row below like/comment/share on each post" = "Inserta un botón en la fila de los botones me gusta, comentar y compartir en cada publicación";
"Long press on media to expand in full-screen viewer" = "Mantener pulsado el contenido multimedia para ver en pantalla completa";
"Media" = "Contenido multimedia";
"Media zoom" = "Ampliar contenido multimedia";
"No suggested for you" = "Sin 'Sugerencias para ti'";
"No suggested posts" = "Sin 'Publicaciones sugeridas'";
"No suggested reels" = "Sin 'Reels sugeridos'";
"No suggested threads" = "Sin'Hilos sugeridos'";
"Prevents feed from reloading when returning from background" = "Evita que el Feed (Inicio) se actualice cuando se regrese de segundo plano";
"Prevents videos from playing automatically" = "Evita que los videos se reproduzcan automáticamente";
"Refresh" = "Actualizar";
"Removes all content from your home feed" = "Elimina todo el contenido de tu Feed (Inicio)";
"Removes suggested accounts from the stories tray" = "Elimina las cuentas sugeridas de la bandeja de historias";
"Removes suggested posts" = "Elimina las publicaciones sugeridas";
"Scroll to top without refreshing when tapping Home" = "Desplazarse hacia arriba sin actualizar al pulsar el botón de Inicio";
"Show action button" = "Mostrar botón de acción";
"Stories tray" = "Bandeja de historias";
"Tapping Home does nothing when already on feed" = "Pulsar botón de Inicio no hace nada cuando te encuentres en la pestaña de Feed (Inicio)";
"Tray long-press actions" = "Acciones al mantener pulsado en la bandeja";
"What happens on a single tap. Long-press always opens the full menu" = "Lo que ocurre con un solo toque. Mantener pulsado siempre abre el menú completo";
//////////////////////////////////////////////////////////////////////////////
// REELS //
// Settings → Reels tab //
//////////////////////////////////////////////////////////////////////////////
"Adds a RyukGram action button above the reel sidebar with view-cover/download/share/copy/expand/repost entries. Tap opens the menu by default; change the tap behavior below." = "Añade un botón de acción de RyukGram sobre la barra lateral del reel con las opciones ver portada, descargar, compartir, copiar, ampliar y repost. Tocar abre el menú de forma predeterminada.\nArriba puedes cambiar el comportamiento al tocar.";
"Always show progress scrubber" = "Siempre mostrar el indicador de progreso";
"Auto-scroll reels" = "Desplazamiento automático de reels";
"IG default: native behavior. RyukGram: re-advances after swiping back." = "IG por defecto: comportamiento nativo. RyukGram: vuelve a avanzar después de deslizar hacia atrás.";
"IG default" = "IG por defecto";
"RyukGram" = "RyukGram";
"Change what happens when you tap on a reel" = "Cambia lo que ocurre cuando tocas en un reel";
"Confirm reel refresh" = "Confirmar actualización de reels";
"Disable auto-unmuting reels" = "Deshabilitar el reactivado automático del sonido en los reels";
"Disable scrolling reels" = "Deshabilitar desplazamiento en reels";
"Disable tab button refresh" = "Deshabilitar actualización con el botón de la pestaña";
"Doom scrolling limit" = "Límite de doom scrolling";
"Forces the progress bar to appear on every reel" = "Fuerza la barra de progreso a aparecer en todos los reels";
"Hide reels header" = "Ocultar el encabezado de los reels";
"Hides the repost button on the reels sidebar" = "Oculta el botón repost en la barra lateral de los reels";
"Hides the top navigation bar when watching reels" = "Oculta la barra de navegación superior al ver reels";
"Hiding" = "Ocultar";
"Limits" = "Límites";
"Limits the amount of reels available to scroll at any given time, and prevents refreshing" = "Limita la cantidad de reels disponibles para desplazar en cualquier momento, y evita que se actualice";
"Only loads %@ %@" = "Solo cargar %@ %@";
"Places a button above the like/comment/share column on each reel" = "Coloca un botón sobre la columna de botones me gusta, comentar y compartir en cada reel";
"Prevent doom scrolling" = "Evitar doom scrolling";
"Prevents reels from being scrolled to the next video" = "Evita que los reels se desplacen al siguiente video";
"Prevents reels from unmuting when the volume/silent button is pressed" = "Evita que los reels dejen de estar silenciados cuando se presionan los botones de volumen o silencio";
"Shows an alert when you trigger a reels refresh" = "Muestra una alerta al solicitar una actualización de reels";
"Shows buttons to reveal and auto-fill the password on locked reels" = "Muestra botones para revelar y auto-completar la contraseña en reels bloqueados";
"Tap Controls" = "Controles táctiles";
"Tapping the Reels tab while on reels does nothing" = "Pulsar el botón de reels no hace nada cuando te encuentres en la pestaña de Reels";
"Unlock password-locked reels" = "Desbloquea reels bloqueados por contraseña";
//////////////////////////////////////////////////////////////////////////////
// PROFILE //
// Settings → Profile tab //
//////////////////////////////////////////////////////////////////////////////
"Adds a button next to the burger menu on profiles to copy username, name or bio" = "Añade un botón junto al menú de hamburguesa (☰) en los perfiles para copiar nombre de usuario, nombre o presentación";
"Adds a view option to the highlight long-press menu to open the cover in full-screen" = "Añade una opción de visualización en el menú que aparece al mantener pulsado sobre la historia destacada para abrir la portada en pantalla completa";
"Copy note on long press" = "Copia la nota al mantener pulsado";
"Follow indicator" = "Indicador de seguido";
"Long press a profile picture to open it in full-screen with zoom, share, and save" = "Mantener pulsado en una foto de perfil para abrirla en pantalla completa para ampliar, compartir y guardar";
"Long press the note bubble on a profile to copy the text" = "Mantén pulsado la burbuja de una nota en un perfil para copiar el texto";
"Long press to download directly (ignored when zoom is on)" = "Mantén pulsado para descargar directamente (Se ignora cuando la foto está ampliada)";
"Long-press gestures on profile elements — kept separate from the per-feature action buttons." = "Los gestos al mantener pulsado en los elementos del perfil, se mantienen separados de los botones de acción específicos de cada función.";
"Profile copy button" = "Botón de copiar perfil";
"Save profile picture" = "Guardar foto de perfil";
"Shows whether the profile user follows you" = "Muestra si el usuario del perfil te sigue";
"View highlight cover" = "Ver portada de la historia destacada";
"Zoom profile photo" = "Ampliar foto de perfil";
//////////////////////////////////////////////////////////////////////////////
// SAVING & DOWNLOADS //
// Settings → Saving tab //
//////////////////////////////////////////////////////////////////////////////
"Confirm before download" = "Confirmar antes de descargar";
"Deprecated. The RyukGram action button (configured per feature in Feed/Reels/Stories) is the new way to download media. Enable this master toggle only if you prefer the old multi-finger long-press directly on the media." = "Obsoleto. El botón de acción de RyukGram (configurado por cada por cada tipo de contenido en Feed (Inicio)/Reels/Historias) es la nueva forma de descargar contenido multimedia. Habilita este control general solo si prefieres el antiguo método de mantener pulsado con varios dedos directamente sobre el contenido multimedia.";
"Downloads" = "Descargas";
"Downloads with %@ %@" = "Descargar con %@ %@";
"Enable long-press gesture" = "Habilitar gesto de mantener pulsado";
"Finger count for long-press" = "Cantidad de dedos a mantener pulsados";
"Legacy long-press gesture" = "Gesto antiguo de mantener pulsado";
"Long-press hold time" = "Tiempo a mantener pulsado";
"Master toggle for the deprecated gesture workflow (off by default)" = "Control general para el gesto antiguo (Desactivado por defecto)";
"Press finger(s) for %@ %@" = "Pulsar dedo(s) por %@ %@";
"Route saves into a dedicated album in Photos instead of the camera roll root" = "Guarda en un álbum dedicado en Fotos, en vez de la Fototeca";
"Save action" = "Acción después de guardar";
"Save to RyukGram album" = "Guardar en álbum RyukGram";
"Saving" = "Descargar";
"Show a confirmation dialog before starting a download" = "Muestra un diálogo de confirmación antes de iniciar una descarga";
"What happens after the gesture downloads" = "Lo que ocurre después de que termina la descarga";
"When \"Save to RyukGram album\" is on, downloads and share-sheet \"Save to Photos\" picks are routed into a dedicated \"RyukGram\" album in your Photos library." = "Cuando \"Guardar en álbum RyukGram\" está activado, las descargas y elementos seleccionados en \"Guardar en Fotos\" se dirigen a un álbum llamado \"RyukGram\" en tu Fototeca.";
//////////////////////////////////////////////////////////////////////////////
// STORIES //
// Settings → Stories tab //
//////////////////////////////////////////////////////////////////////////////
"Adds a RyukGram action button next to the eye button on stories with download/share/copy/expand/repost/view-mentions entries. Tap opens the menu by default; change the tap behavior below." = "Añade un botón de acción de RyukGram junto al botón con forma de ojo (👁) en las historias con las opciones descargar, compartir, copiar, ampliar, repost y ver menciones. Tocar abre el menú de forma predeterminada.\nArriba puedes cambiar el comportamiento al tocar.";
"Adds a speaker button to the story overlay to unmute/mute audio. Also available in the 3-dot menu" = "Añade un botón de altavoz a la superposición de las historias para alternar el sonido. También disponible en el menú de los 3 puntos";
"Advance on story like" = "Avanzar historia al dar me gusta";
"Advance on story reply" = "Avanzar historia al responder";
"Advance when marking as seen" = "Avanzar historia cuando se marque como vista";
"Audio" = "Sonido";
"Block all: all stories blocked — listed users are exceptions.\nBlock selected: only listed users are blocked — everything else is normal.\nBoth lists are saved independently." = "Bloquear todo: Todas las historias bloqueadas — Usuarios en la lista son excepciones.\nBloquear seleccionadas: Solo los usuarios en la lista son bloqueados — Todo lo demás permanece normal.\nAmbas listas son guardadas independientemente.";
"Blocking mode" = "Modo de bloqueo";
"Button = single-tap mark seen. Toggle = tap toggles story read receipts on/off (eye fills blue when on)" = "Botón = Un toque marca como vista. Interruptor = Tocar el interruptor alterna el aviso de visualización de las historias (👁 se vuelve azul cuando se activa)";
"Disable instants creation" = "Deshabilitar creación de instantáneas";
"Disable story seen receipt" = "Deshabilitar aviso de visualización de historias";
"Enable story user list" = "Habilitar lista de usuarios para historias";
"Hides the functionality to create/send instants" = "Oculta la funcionalidad de crear/enviar instantáneas";
"Hides the notification for others when you view their story" = "Oculta la notificación a los demás cuando ves sus historias";
"Inserts a button next to the seen/eye button on story overlays" = "Inserta un botón junto al botón con forma de ojo (👁) en la superposición de las historias";
"Keep stories visually seen locally" = "Marcar historias como vistas localmente";
"Liking a story automatically advances to the next one after a short delay" = "Darle me gusta a una historia avanza automáticamente a la siguiente después de un breve período";
"Manage list" = "Gestionar lista";
"Manage list (%lu)" = "Gestionar lista (%lu)";
"Manual seen button mode" = "Modo visualización manual";
"Mark seen on story like" = "Marcar visualización de historia al darle me gusta";
"Mark seen on story reply" = "Marcar visualización de historia al responder";
"Marks a story as seen the moment you tap the heart, even with seen blocking on" = "Marca una historia como vista en el momento en que tocas el corazón, incluso con el bloqueo de aviso de visualización activado";
"Marks a story as seen when you send a reply or emoji reaction, even with seen blocking on" = "Marca una historia como vista cuando envías una respuesta o una reacción con emoji, incluso con el bloqueo de aviso de visualización activado";
"Master toggle. When off, the list is ignored" = "Control general. Cuando está desactivado, la lista es ignorada";
"Other" = "Otros";
"Playback" = "Reproducción";
"Marks stories as seen locally (grey ring) while still blocking the seen receipt on the server" = "Marca las historias como vistas localmente (círculo gris) mientras sigue bloqueando el recibo de visto en el servidor";
"Quick list button in stories" = "Botón de lista rápida en historias";
"Search, sort, swipe to remove" = "Buscar y ordenar. Desliza para eliminar";
"Seen receipts" = "Confirmación de visualización";
"Sending a reply or emoji reaction automatically advances to the next story" = "Enviar una respuesta o una reacción con emoji automáticamente avanza a la siguiente historia";
"Show mentioned users in eye button and story menu" = "Mostrar usuarios mencionados en el botón con forma de ojo (👁) y el menú de la historia";
"Shows an eye button on stories to add/remove users from the list. Off = use the 3-dot menu or long-press only" = "Muestra un botón con forma de ojo (👁) en las historias para añadir o eliminar usuarios de la lista. Desactivado = Usar el menú de los 3 puntos o solo mantener pulsado";
"Stop story auto-advance" = "Detener avance automático de las historias";
"Stories" = "Historias";
"Stories won't auto-skip to the next one when the timer ends. Tap to advance manually" = "Historias no avanzan automáticamente a la siguiente cuando el temporizador termina. Toca para avanzar manualmente";
"Story audio toggle" = "Alternar sonido de la historia";
"Story user list" = "Lista de usuarios para historias";
"Tapping the eye button to mark a story as seen advances to the next story automatically" = "Tocar el botón con forma de ojo para marcar una historia como vista avanza a la siguiente historia automáticamente";
"View story mentions" = "Ver mencionados en la historia";
"Which stories get seen-receipt blocking" = "Cuales historias tienen bloqueado el aviso de visualización";
//////////////////////////////////////////////////////////////////////////////
// MESSAGES — READ RECEIPTS //
// Settings → Read receipts tab //
//////////////////////////////////////////////////////////////////////////////
"Adds a button to DM threads to mark messages as seen" = "Añade un botón en las conversaciones para marcar los mensajes como vistos";
"Auto mark seen on interact" = "Marcar automáticamente como visto al interactuar";
"Auto mark seen on typing" = "Marcar automáticamente como visto al escribir";
"Control when messages are marked as seen" = "Controla cuando los mensajes son marcados como vistos";
"How the seen button behaves" = "Como el botón de visto se comporta";
"Manually mark messages as seen" = "Marcar manualmente los mensajes como vistos";
"Marks messages as seen when you send any message" = "Marca los mensajes como vistos cuando envías cualquier mensaje";
"Marks messages as seen when you start typing" = "Marca los mensajes como vistos cuando comienzas a escribir";
"Read receipt mode" = "Modo de confirmación de lectura";
"Read receipts" = "Confirmación de lectura";
//////////////////////////////////////////////////////////////////////////////
// MESSAGES — KEEP DELETED //
// Settings → Keep deleted messages tab //
//////////////////////////////////////////////////////////////////////////////
"Activity" = "Actividad";
"Adds a 'Download' option to the long-press menu on voice messages to save them as M4A audio" = "Añade una opción 'Descargar' al menú que aparece al mantener pulsado sobre los mensajes de voz para guardarlos como archivos M4A";
"Adds a 'Send File' option to the plus menu in DMs. Supported file types may be limited by Instagram" = "Añade una opción 'Enviar archivo' al menú ⊕ en las conversaciones.\nTipos de archivos soportados puede estar limitado por Instagram";
"Adds an 'Audio File' option to the plus menu in DMs to send audio files as voice messages" = "Añade una opción 'Archivo de audio' al menú ⊕ en las conversaciones para enviar archivos de audio como mensajes de voz";
"Adds copy text, download GIF/audio to the note long-press menu" = "Añade las opciones copiar texto, descargar GIF/Audio al menú que aparece al mantener pulsada una nota";
"Block all: all chats blocked — listed chats are exceptions.\nBlock selected: only listed chats are blocked — everything else is normal.\nBoth lists are saved independently. Long-press a chat in the inbox to add or remove." = "Bloquear todo: Todas las conversaciones bloqueadas — Conversaciones en la lista son excepciones.\nBloquear seleccionadas: Solo las conversaciones en la lista son bloqueadas — Todo lo demás permanece normal.\nAmbas listas son guardadas independientemente. Mantén pulsada una conversación en la bandeja de entrada para añadir o eliminar";
"Block keep-deleted for excluded chats" = "Bloquea mantener mensajes eliminados para conversaciones excluidas";
"Block keep-deleted for unlisted chats" = "Bloquear mantener eliminados para conversaciones sin listar";
"Chat list" = "Lista de conversaciones";
"Confirmation dialog before clearing preserved messages" = "Muestra un diálogo de confirmación antes de borrar mensajes guardados";
"Copies note text directly on long press without opening the menu" = "Copia el texto de la nota directamente al mantener pulsado sin abrir el menú";
"Copy text on hold" = "Copiar texto al mantener pulsado";
"Custom emojis and background/text colors" = "Emojis, color de fondo y texto personalizado";
"Custom note themes" = "Tema de notas personalizado";
"Disable disappearing mode swipe" = "Deshabilitar deslizamiento para mensajes temporales";
"Disable screenshot detection" = "Deshabilitar detección de capturas de pantalla";
"Disable typing status" = "Deshabilitar estado de escritura";
"Disable view-once limitations" = "Deshabilitar limitaciones de ver una vez";
"Download voice messages" = "Descargar mensajes de voz";
"Enable chat list" = "Habilitar lista de conversaciones";
"Enable note theming" = "Habilitar temas en notas";
"Enables the notes theme picker" = "Habilita el selector de temas para notas";
"Files" = "Archivos";
"Full last active date" = "Fecha de última vez completa";
"Hide reels blend button" = "Ocultar el botón de blend";
"Hide video call button" = "Ocultar botón de video llamada";
"Hide voice call button" = "Ocultar botón de llamada";
"Hides the blend button in DMs" = "Elimina el botón de blend en las conversaciones";
"Hides typing indicator from others" = "Oculta el indicador de escribiendo para los demás";
"Indicate unsent messages" = "Indicar eliminación de mensaje";
"Keep deleted messages" = "Mantener mensajes eliminados";
"Makes view-once messages behave like normal visual messages (loopable/pauseable)" = "Hace que los mensajes para ver una vez se comporten como mensajes visuales normales (Búcle/Pausa)";
"Note actions" = "Acciones en notas";
"Preserve messages that others unsend" = "Guardar los mensajes que los demás eliminen";
"Preserves messages that others unsend" = "Guarda los mensajes que los demás eliminen";
"Prevents accidental swipe-up activation of disappearing mode" = "Evita la activación accidental de los mensajes temporales al deslizar hacia arriba";
"Quick list button in chats" = "Botón de lista rápida en conversaciones";
"Removes the audio call button from DM thread header" = "Elimina el botón de llamada en las conversaciones";
"Removes the screenshot-prevention features for visual messages in DMs" = "Elimina las funciones que impiden hacer capturas de pantalla para mensajes visuales en las conversaciones";
"Removes the video call button from DM thread header" = "Elimina el botón de video llamada en las conversaciones";
"Replay visual messages without expiring. Toggle in the eye button menu, or as a standalone button when the eye button is disabled" = "Reproducir mensajes visuales sin expiración. Alterna en el menú del botón ojo (👁), o como un botón por separado cuando el botón ojo (👁) está deshabilitado";
"Search, sort, swipe to remove or toggle keep-deleted" = "Buscar y ordenar. Desliza para eliminar o alterna mantener eliminados";
"Send audio as file" = "Enviar audio como archivo";
"Send files (experimental)" = "Enviar archivos (Experimental)";
"Show full date instead of \"Active 2h ago\"" = "Muestra la fecha completa en vez de \"Activo hace 2 horas\"";
"Shows a button in DM threads to add/remove chats from the list. Long-press for more options" = "Muestra un botón en la pestaña Mensajes para añadir o eliminar conversaciones de la lista. Mantén pulsado para más opciones";
"Shows a notification pill when a message is unsent" = "Muestra una notificación cuando se elimine un mensaje";
"Shows an \"Unsent\" label on preserved messages" = "Muestra una etiqueta \"Mensaje eliminado\" en mensajes guardados";
"Unlimited replay of visual messages" = "Reproducción ilimitada de mensajes visuales";
"Unsent message notification" = "Notificación de eliminación de mensaje";
"Visual messages" = "Mensajes visuales";
"Voice messages" = "Mensajes de voz";
"Warn before clearing on refresh" = "Mostrar un aviso antes de actualizar";
"Which chats get read-receipt blocking" = "Cuales conversaciones tienen bloqueada la confirmación de lectura";
"⚠️ Pull-to-refresh in the DMs tab clears all preserved messages. Enable the warning below to get a confirmation dialog." = "⚠️ Deslizar para actualizar en la pestaña Mensajes borra todos los mensajes guardados. Activa la advertencia que aparece arriba para que se muestre diálogo de confirmación.";
//////////////////////////////////////////////////////////////////////////////
// MESSAGES //
// Settings → Messages tab //
//////////////////////////////////////////////////////////////////////////////
"Messages" = "Mensajes";
"Threads" = "Hilos";
//////////////////////////////////////////////////////////////////////////////
// NAVIGATION //
// Settings → Navigation tab //
//////////////////////////////////////////////////////////////////////////////
"Hide create tab" = "Ocultar pestaña Crear";
"Hide explore tab" = "Ocultar pestaña Explorar";
"Hide feed tab" = "Ocultar pestaña Feed (Inicio)";
"Hide messages tab" = "Ocultar pestaña Mensajes";
"Hide reels tab" = "Ocultar pestaña Reels";
"Hides every tab except DM inbox + profile and forces launch into the inbox. Settings shortcut moves to long-press on the inbox tab." = "Oculta todas las pestañas excepto Mensajes y Perfil. Inicia la aplicación en la pestaña Mensajes. El acceso rápido a la configuración se cambia a mantener pulsada la pestaña Mensajes.";
"Hides the create tab on the bottom navigation bar" = "Oculta la pestaña Crear en la barra de navegación inferior";
"Hides the direct messages tab on the bottom navigation bar" = "Oculta la pestaña Mensajes en la barra de navegación inferior";
"Hides the explore/search tab on the bottom navigation bar" = "Oculta la pestaña Explorar (Busca) en la barra de navegación inferior";
"Hides the feed/home tab on the bottom navigation bar" = "Oculta la pestaña Feed (Inicio) en la barra de navegación inferior";
"Hides the reels tab on the bottom navigation bar" = "Oculta la pestaña Reels en la barra de navegación inferior";
"Hiding tabs" = "Ocultar pestañas";
"Icon order" = "Orden de los íconos";
"Launch tab" = "Pestaña inicial";
"Lets you swipe to switch between navigation bar tabs" = "Permite deslizar para cambiar entre las pestañas de la barra de navegación";
"Messages only" = "Únicamente Mensajes";
"Messages-only mode" = "Modo mensajería";
"Navigation" = "Navegación";
"Swipe between tabs" = "Deslizar entre pestañas";
"Tab the app opens to. Ignored when Messages-only is on" = "Pestaña en la que inicia la aplicación. Se ignora cuando el modo mensajería está activado";
"The order of the icons on the bottom navigation bar" = "Orden de los íconos en la barra de navegación inferior";
"Turn IG into a DM-only client" = "Convierte Instagram en una aplicación de mensajería instantánea";
//////////////////////////////////////////////////////////////////////////////
// CONFIRM ACTIONS //
// Settings → Confirm actions tab //
//////////////////////////////////////////////////////////////////////////////
"Confirm actions" = "Confirmar acciones";
"Confirm call" = "Confirmar llamada";
"Confirm changing theme" = "Confirmar cambiar el tema";
"Confirm follow" = "Confirmar seguir";
"Confirm follow requests" = "Confirmar solicitud de seguimiento";
/* [ADDED_BY_DEV] */ "Confirm like: Posts" = "Confirmar me gusta en publicaciones";
/* [ADDED_BY_DEV] */ "Confirm story like" = "Confirmar me gusta en historias";
/* [ADDED_BY_DEV] */ "Confirm story emoji reaction" = "Confirmar reacción con emojis en historias";
"Confirm like: Reels" = "Confirmar me gusta en reels";
"Confirm posting comment" = "Confirmar publicar comentario";
"Confirm repost" = "Confirmar repost";
"Confirm shh mode" = "Confirmar mensajes temporales";
"Confirm sticker interaction" = "Confirma interacción con stickers";
"Confirm unfollow" = "Confirmar dejar de seguir";
"Confirm voice messages" = "Confirmar mensaje de voz";
"Shows an alert to confirm before sending a voice message" = "Muestra una alerta para confirmar antes de enviar un mensaje de voz";
"Shows an alert to confirm before toggling disappearing messages" = "Muestra una alerta para confirmar antes de activar los mensajes temporales";
"Shows an alert when you accept/decline a follow request" = "Muestra una alerta cuando aceptas o rechazas una solicitud de seguimiento";
"Shows an alert when you change a chat theme to confirm" = "Muestra una alerta para confirmar cuando cambias el tema en una conversación";
"Shows an alert when you click a sticker on someone's story to confirm the action" = "Muestra una alerta para confirmar la acción cuando tocas un sticker en la historia de alguien";
"Shows an alert when you click the audio/video call button to confirm before calling" = "Muestra una alerta cuando tocas los botones de llamada y video llamada, antes de llamar";
"Shows an alert when you click the follow button to confirm the follow" = "Muestra una alerta para confirmar cuando tocas el botón de seguir";
/* [ADDED_BY_DEV] */ "Shows an alert when you click the like button on posts to confirm the like" = "Muestra una alerta para confirmar cuando tocas el botón de me gusta en publicaciones";
"Shows an alert when you click the like button on reels to confirm the like" = "Muestra una alerta para confirmar cuando tocas el botón de me gusta en reels";
/* [ADDED_BY_DEV] */ "Shows an alert when you click the like button on stories to confirm the like" = "Muestra una alerta para confirmar cuando tocas el botón de me gusta en historias";
/* [ADDED_BY_DEV] */ "Shows an alert before sending an emoji reaction on a story" = "Muestra una alerta antes de enviar una reacción con emojis en una historia";
"Shows an alert when you click the post comment button to confirm" = "Muestra una alerta para confirmar cuando tocas el botón de publicar comentario";
"Shows an alert when you click the repost button to confirm before resposting" = "Muestra una alerta para confirmar cuando tocas el botón de repost";
"Shows an alert when you click the unfollow button to confirm" = "Muestra una alerta para confirmar cuando tocas el botón de dejar de seguir";
"Shows an alert when you click the like button on posts or stories to confirm the like" = "Muestra una alerta para confirmar cuando tocas el botón de me gusta en publicaciones o historias";
//////////////////////////////////////////////////////////////////////////////
// BACKUP & RESTORE //
// Settings → Backup & Restore tab //
//////////////////////////////////////////////////////////////////////////////
"Backup & Restore" = "Copia de seguridad & restauración";
"Export settings" = "Exportar configuración";
"Export your RyukGram settings to a JSON file and import them later. Importing resets all settings to defaults before applying the imported values, and shows a preview before anything changes." = "Exporta tu configuración de RyukGram a un archivo JSON para importarlos mas tarde. Al importar, se restaurarán los valores predeterminados antes de aplicar la nueva configuración. Podrás ver una vista previa antes de confirmar los cambios.";
"Import settings" = "Importar configuración";
"Load settings from a JSON file" = "Cargar configuración desde un archivo JSON";
"Reset to defaults" = "Restablecer los valores predeterminados";
"Revert every RyukGram preference" = "Restablecer todas las preferencias de RyukGram";
"Save settings as a JSON file" = "Guarda configuración como un archivo JSON";
//////////////////////////////////////////////////////////////////////////////
// EXPERIMENTAL //
// Settings → Experimental tab //
//////////////////////////////////////////////////////////////////////////////
"Experimental" = "Experimental";
"These features are unstable and cause the Instagram app to crash unexpectedly.\n\nUse at your own risk!" = "Estas funciones son inestables y provocan que la aplicación de Instagram se cierre inesperadamente.\n\n¡Úsalas bajo tu propia responsabilidad!";
"Warning" = "Advertencia";
//////////////////////////////////////////////////////////////////////////////
// ADVANCED //
// Settings → Advanced tab //
//////////////////////////////////////////////////////////////////////////////
"Advanced" = "Avanzado";
"Automatically opens settings when the app launches" = "Abre la configuración automáticamente cuando se inicia la aplicación";
"Disable safe mode" = "Deshabilitar modo seguro";
"Enable tweak settings quick-access" = "Habilitar acceso rápido a la configuración del Tweak";
"Hold on the home tab to open RyukGram settings" = "Mantén pulsada la pestaña Feed (Inicio) para abrir la configuración de RyukGram";
"Instagram" = "Instagram";
"Pause playback when opening settings" = "Pausa la reproducción al abrir la configuración";
"Pauses any playing video/audio when settings opens" = "Pausa cualquier reproducción de video o audio cuando se abre la configuración";
"Prevents Instagram from resetting settings after crashes (at your own risk)" = "Evita que Instagram restablezca la configuración después de un cierre inesperado\n(¡Bajo tu propia responsabilidad!)";
"Reset onboarding state" = "Restablecer estado onboarding"; // Verify onboarding - Verificar onboarding
"Settings" = "Configuración";
"Show tweak settings on app launch" = "Muestra la configuración del Tweak cuando se inicia la aplicación";
//////////////////////////////////////////////////////////////////////////////
// DEBUG //
// Settings → Debug tab //
//////////////////////////////////////////////////////////////////////////////
"Button Cell" = "Celda de botón";
"Change the value on the right" = "Cambia el valor a la derecha";
"Debug" = "Debug";
"Enable FLEX gesture" = "Habilitar gesto FLEX";
"Hold 5 fingers on the screen to open FLEX" = "Mantén pulsados 5 dedos en la pantalla para abrir FLEX";
"I have %@%@" = "Tengo %@%@";
"Link Cell" = "Celda de enlace";
"Menu Cell" = "Celda de menú";
"Open FLEX on app focus" = "Abrir FLEX al enfocar la aplicación";
"Open FLEX on app launch" = "Abrir FLEX al iniciar la aplicación";
"Opens FLEX when the app is focused" = "Abre FLEX cuando la aplicación es enfocada";
"Opens FLEX when the app launches" = "Abre FLEX cuando la aplicación se inicia";
"Static Cell" = "Celda estática";
"Stepper cell" = "Celda de paso";
"Switch Cell" = "Celda interruptor";
"Switch Cell (Restart)" = "Cambiar celda (Reinicio)";
"Tap the switch" = "Toca el interruptor";
"Using icon" = "Usar ícono";
"Using image" = "Usar imagen";
"_ Example" = "_ Ejemplo";
//////////////////////////////////////////////////////////////////////////////
// DOWNLOADS & MEDIA ACTIONS //
// Action button menus, download/share/copy toasts, quality picker pills. //
//////////////////////////////////////////////////////////////////////////////
"%@ settings" = "Configuración de %@";
"Cancelled" = "Cancelado";
"Copied %lu URLs" = "Copiados %lu enlaces";
"Copied caption" = "Descripción copiada";
"Copied download URL" = "Enlace de descarga copiado";
"Copy all URLs" = "Copiar todos los enlaces";
"Copy caption" = "Copiar descripción";
"Copy download URL" = "Copiar enlace de descarga";
"Could not extract any URLs" = "No se logró extraer ningún enlace";
"Could not extract media URL" = "No se logró extraer enlace de medios";
"Could not extract photo URL" = "No se logró extraer enlace de fotos";
"Could not extract video URL" = "No se logró extraer enlace de videos";
"Done" = "Finalizado";
"Download all (%lu)" = "Descargar todos (%lu)";
"Download all stories and share?" = "¿Descargar todas las historias y compartir?";
"Download all to Photos" = "Descargar todo en Fotos";
"Download and share all" = "Descargar y compartir todo";
"Download and share?" = "¿Descargar y compartir?";
"Download failed" = "Descarga fallida";
"Downloaded %lu items" = "Descargando %lu elementos";
"Downloading %@..." = "Descargando %@...";
"Downloading..." = "Descargando...";
"Failed to save" = "Error al guardar";
"HD download complete" = "Descarga HD completada";
"Mute audio" = "Silenciar sonido";
"No URLs" = "Sin enlaces";
"No URLs found" = "Sin enlaces encontrados";
"No caption on this post" = "No hay descripción en esta publicación";
"No carousel children" = "Sin carrusel hijo";
"No cover image" = "Sin imagen de portada";
"No files downloaded" = "No se descargaron archivos";
"No media" = "Sin medios";
"No media URL" = "Sin enlace de medios";
"No media to expand" = "Sin medios para ampliar";
"No media to show" = "Sin medios para mostrar";
"No video URL" = "Sin enlace de video";
"Not a carousel" = "No es un carrusel";
"Nothing to save" = "Nada para guardar";
"Nothing to share" = "Nada para compartir";
"Opening creator..." = "Abriendo creador...";
"Photo library access denied" = "Acceso a la Fototeca denegado";
"Photos access denied" = "Acceso a Fotos denegado";
"Preparing repost..." = "Preparando repost...";
"Repost" = "Repost";
"Repost unavailable" = "Repost no disponible";
"Save all stories to Photos?" = "¿Guardar todas las historias en Fotos?";
"Save failed" = "Error al guardar";
"Save to Photos?" = "¿Guardar en Fotos?";
"Saved %lu items" = "Guardados %lu elementos";
"Saved to Photos" = "Guardado en Fotos";
"Saved to RyukGram" = "Guardado en RyukGram";
"Tap to cancel" = "Toca para cancelar";
"Unmute audio" = "Activar sonido";
"View cover" = "Ver portada";
"View mentions" = "Ver menciones";
//////////////////////////////////////////////////////////////////////////////
// STORIES & MESSAGES (FEATURES) //
// Buttons, menu entries, toasts and alerts shown while watching stories or //
// inside DM threads. //
//////////////////////////////////////////////////////////////////////////////
"A message was unsent" = "Se anuló el envío de un mensaje";
"Add" = "Añadir";
"Add to block list" = "Añadir a la lista de bloqueo";
"Add to block list?" = "¿Añadir a la lista de bloqueo?";
"Added to block list" = "Añadido a la lista de bloqueo";
"Audio not loaded yet. Play the message first and try again." = "Audio aún no cargado. Reproduce el mensaje primero y vuelve a intentar.";
"Audio sent" = "Audio enviado";
"Audio/Video from Files" = "Audio/Video desde Archivos";
"Blocked" = "Bloqueado";
"Cancel" = "Cancelar";
"Clear preserved messages?" = "¿Borrar mensajes guardados?";
"Converting..." = "Convirtiendo...";
"Copy text" = "Copiar texto";
"Could not find media" = "No se logró encontrar medios";
"Could not find story media" = "No se logró encontrar medios para la historia";
"Could not get audio data. Try again after refreshing the chat." = "No se encontró datos de audio. Intenta nuevamente luego de actualizar la conversación.";
"Could not get video URL" = "No se encontró enlace al video";
"Disable read receipts" = "Deshabilitar confirmación de lectura";
"Done!" = "¡Finalizado!";
"Download audio" = "Descargar audio";
"Downloading audio..." = "Descargando audio...";
"Enable read receipts" = "Habilitar confirmación de lectura";
"Error: %@" = "Error: %@";
"Exclude chat" = "Excluir chat";
"Exclude story seen" = "Excluir de visualización de historia";
"Excluded" = "Excluído";
"Extracting audio..." = "Extrayendo audio...";
"Failed to encode GIF" = "No se logró codificar el GIF";
"File sending not supported" = "Enviar archivos no soportado";
"Follow" = "Seguir";
"Following" = "Siguiendo";
"Mark messages as seen" = "Marcar mensajes como vistos";
"Mark seen" = "Marcar como vista";
"Marked as seen" = "Marcado como visto";
"Marked as viewed" = "Marcado como visto";
"Marked messages as seen" = "Mensajes marcados como vistos";
"Mentions" = "Menciones";
"Message sender not found" = "No se encontró remitente del mensaje";
"Messages settings" = "Configuración de Mensajes";
"Mute story audio" = "Silenciar sonido de historia";
"No audio URL found. Try again after refreshing the chat." = "No se encontró enlace de audio. Intenta nuevamente luego de actualizar la conversación.";
"No mentions in this story" = "Sin menciones en esta historia";
"No thread key" = "Sin llave";
"No voice send method found" = "No se encontró método para envío de voz";
"Note not found" = "Nota no encontrada";
"Note text copied" = "Texto de la nota copiado";
"Open GitHub" = "Abrir GitHub";
"Read receipts disabled" = "Confirmación de lectura DESACTIVADA";
"Read receipts enabled" = "Confirmación de lectura ACTIVADA";
"Read receipts will be blocked for this chat." = "Confirmación de lectura estará bloqueado para esta conversación.";
"Read receipts will no longer be blocked for this chat." = "Confirmación de lectura ya no estará bloqueado para este chat.";
"Remove" = "Eliminar";
"Remove from block list" = "Eliminar de la lista de bloqueo";
"Remove from block list?" = "¿Eliminar de la lista de bloqueo?";
"Removed" = "Eliminado";
"Save GIF" = "Guardar GIF";
"Selection too short (min 0.5s)" = "Selección demasiado corta (min 0.5s)";
"Send Audio" = "Enviar audio";
"Send anyway" = "Enviar de todos modos";
"Send failed: %@" = "Envío fallido: %@";
"Send service not found" = "Servicio de envío no encontrado";
"Share" = "Compartir";
"Story read receipts disabled" = "Aviso de visualización de historia DESACTIVADO";
"Story read receipts enabled" = "Aviso de visualización de historia ACTIVADO";
"Story seen receipts will be blocked for @%@." = "Aviso de visualización de historia será bloqueado para @%@.";
"This chat will resume normal read-receipt behavior." = "Este chat volverá a funcionar con el sistema habitual de confirmaciones de lectura.";
"Total: %@" = "Total: %@";
"Un-exclude" = "No excluir";
"Un-exclude chat" = "No excluir conversación";
"Un-exclude chat?" = "¿No excluir conversación?";
"Un-exclude story seen" = "No excluir de visualización de la historia";
"Un-exclude story seen?" = "¿No excluir de visualización de la historia?";
"Un-excluded" = "No excluído";
"Unblock" = "Desbloquear";
"Unblocked" = "Desbloqueado";
"Unlimited replay enabled" = "Reproducción ilimitada ACTIVADA";
"Unmute story audio" = "Activar sonido de la historia";
"Unsent" = "Mensaje eliminado";
"Upload Audio" = "Subir Audio";
"VC not found" = "VC no encontrado"; // Verify - Verificar
"Video from Library" = "Video desde Fototeca";
"Visual messages will expire" = "Mensajes visuales expirarán";
"Visual messages: expiring" = "Mensajes visuales: Expirando";
"Visual messages: unlimited replay" = "Mensajes visuales: Reproducción ilimitada";
"Will sync when leaving stories" = "Se sincronizará al salir de las historias";
//////////////////////////////////////////////////////////////////////////////
// GENERAL FEATURES //
// Strings inside per-feature overlays: fake location, color picker, notes //
// customization, profile copy, etc. //
//////////////////////////////////////////////////////////////////////////////
"Add location" = "Añadir ubicación";
"Add preset" = "Añadir ajuste preestablecido";
"Change location" = "Cambiar ubicación";
"Click the Apply button after this to see the emoji" = "Toca el botón Aplicar después de esto para ver el emoji";
"Copied text to clipboard" = "Texto copiado al portapapeles";
"Copy" = "Copiar";
"Copy all" = "Copiar todo";
"Copy bio" = "Copiar presentación";
"Copy from profile" = "Copiar desde perfil";
"Copy name" = "Copiar nombre";
"Could not find cover image" = "No se encontró imagen de portada";
"Current: %@" = "Actual: %@";
"Disable" = "Desactivado";
"Download GIF" = "Descargar GIF";
"Enable" = "Activado";
"Enter Emoji Text" = "Introduce Texto con Emoji";
"Fake location" = "Ubicación falsa";
"Name" = "Nombre";
"Nothing to copy" = "Nada para copiar";
"Save" = "Guardar";
"Save preset" = "Guardar ajuste preestablecido";
"Saved locations" = "Ubicaciones guardadas";
"Select color" = "Escoger color";
"Set location" = "Establecer ubicación";
"Settings…" = "Configuración…";
"Type emoji..." = "Introduce emoji...";
//////////////////////////////////////////////////////////////////////////////
// SETTINGS VIEWS & DIALOGS //
// Excluded-lists managers, backup/restore flows, in-picker labels. //
//////////////////////////////////////////////////////////////////////////////
"Add chat" = "Añadir conversación";
"Add custom domain" = "Añadir dominio personalizado";
"Add preset…" = "Añadir ajuste preestablecido";
"Add to list?" = "¿Añadir a la lista?";
"Add user" = "Añadir usuario";
"Could not resolve user ID" = "No se pudo resolver el ID del usuario";
"Enter username" = "Introducir nombre de usuario";
"Enter username of the DM thread" = "Introducir nombre de usuario de la conversación";
"No DM thread found with @%@" = "No se encontró conversación con @%@";
"User '%@' not found" = "Usuario '%@' no encontrado";
"All RyukGram settings will be reset to defaults and the imported values applied. The app will need to restart for some changes to take effect." = "Todas las configuraciones de RyukGram se restablecerán a los valores predeterminados y se aplicarán los valores importados. Será necesario reiniciar la aplicación para que algunos cambios surtan efecto.";
"Apply" = "Aplicar";
"Apply imported settings?" = "¿Aplicar configuración importada?";
"Apply to" = "Aplicar a";
"Chats" = "Conversaciones";
"Could not read file." = "No se logró leer el archivo.";
"Could not write temporary file." = "No se logró escribir el archivo temporal.";
"Current location" = "Ubicación actual";
"Custom" = "Personalizada";
"Date Format" = "Formato de Fecha";
"Delete" = "Eliminar";
"Done editing" = "Finalizar edición";
"Edit values" = "Editar valores";
"Enable fake location" = "Habilitar ubicación falsa";
"Every RyukGram preference will revert to its built-in default. This can't be undone." = "Todas las preferencias de RyukGram volverán a los valores predeterminados. Esto no se puede deshacer.";
"Excluded chats" = "Conversaciones excluídas";
"Excluded users" = "Usuarios excluídos";
"File is not a valid RyukGram settings export." = "Archivo no es una exportación válida de la configuración de RyukGram.";
"Follow default" = "Seguir predeterminada";
"Force OFF (allow unsends)" = "Forzar DESACTIVADO (Permite anular envío)";
"Force ON (preserve unsends)" = "Forzar ACTIVADO (Mantiene eliminados)";
"Form view" = "Vista de forma";
"Format" = "Formato";
"Import failed" = "Importación fallida";
"Import preview" = "Previsualizar importación";
"Included chats" = "Conversaciones incluidas";
"Included users" = "Usuarios incluidos";
"KD: ON" = "KD: ACTIVADO";
"KD: default" = "ME: Predeterminado";
"Keep-deleted" = "Mantener eliminados";
"Keep-deleted override" = "Anular mantener eliminados";
"Off" = "DESACTIVADO";
"On" = "ACTIVADO";
"Presets" = "Preajustes";
"Raw JSON view" = "Ver JSON sin formato";
"Remove Selected" = "Eliminar Seleccionados";
"Remove from list" = "Eliminar de la lista";
"Reset" = "Restablecer";
"Reset all settings?" = "¿Restablecer todas las configuraciones?";
"Saved presets are reusable. Tap a preset to make it the active location." = "Los ajustes preestablecidos guardados se pueden reutilizar. Toca un ajuste preestablecido para convertirlo en la ubicación activa.";
"Search address or place" = "Buscar dirección o lugar";
"Search by name or username" = "Buscar por nombre o nombre de usuario";
"Search by username or name" = "Buscar por nombre de usuario o nombre";
"Search settings" = "Buscar en configuración";
"Select" = "Seleccionar";
"Select location on map" = "Seleccionar ubicación en el mapa";
"Set current location" = "Establecer ubicación actual";
"Set keep-deleted override" = "Establecer anulación de mantener eliminados";
"Settings exported" = "Configuración exportada";
"Settings imported" = "Configuración importada";
"Show seconds" = "Mostrar segundos";
"Sort by" = "Ordenar por";
"Story users" = "Usuarios de historias";
"Toggle each NSDate formatter IG uses. Different surfaces (feed, comments, stories, DMs) go through different methods — enable the ones you want the custom format applied to." = "Alterna cada formato NSDate que utiliza Instagram. Las distintas secciones (Feed (Inicio), comentarios, historias, mensajes) utilizan métodos diferentes: activa aquellos a los que quieras aplicar el formato personalizado.";
"Use this location" = "Usar esta ubicación";
"When on, all CoreLocation requests inside Instagram return the location below." = "Cuando está activada, todas las solicitudes de CoreLocation dentro de Instagram devuelven la ubicación que se indica a continuación.";
"Show map button" = "Botón de mostrar mapa";
"When on, all CoreLocation requests inside Instagram return the location below. Toggle the map button to show or hide the quick toggle on the Friends Map view." = "Cuando está activado, todas las solicitudes de CoreLocation dentro de Instagram devuelven la ubicación que se muestra a continuación. Pulsa el botón del mapa para mostrar u ocultar el control rápido en la vista mapa de amigos.";
//////////////////////////////////////////////////////////////////////////////
// REELS (FEATURES) //
// Strings from Reels. //
//////////////////////////////////////////////////////////////////////////////
"Copied!" = "¡Copiado!";
"No password found" = "No se encontró contraseña";
"No text field found" = "No se encontró campo de texto";
"Password" = "Contraseña";
"Refresh Reels?" = "¿Actualizar Reels?";
//////////////////////////////////////////////////////////////////////////////
// PROFILE (FEATURES) //
// Strings from Profile. //
//////////////////////////////////////////////////////////////////////////////
"Doesn't follow you" = "No te sigue";
"Follows you" = "Te sigue";
"Note copied" = "Nota copiada";
//////////////////////////////////////////////////////////////////////////////
// CONFIRM DIALOGS (IN-FEATURE) //
// Strings from Confirm dialogs. //
//////////////////////////////////////////////////////////////////////////////
"Unfollow?" = "¿Dejar de seguir?";
//////////////////////////////////////////////////////////////////////////////
// MISC //
// Anything that didn't fit a named section. Usually short labels. //
//////////////////////////////////////////////////////////////////////////////
"720p • progressive • fastest" = "720p • Progresivo • Más rápido";
"Are you sure?" = "¿Estás seguro?";
"Copy audio URL" = "Copiar enlace de audio";
"Copy quality info" = "Copiar información sobre la calidad";
"Copy video URL" = "Copiar enlace de video";
"Could not access reel media" = "No se logró acceder a los medios del reel";
"Could not access reel photo" = "No se logró acceder a la foto del reel";
"Could not extract photo url from post" = "No se logró extraer foto de la publicación";
"Could not extract photo url from reel" = "No se logró extraer enlace de la foto del reel";
"Could not extract photo url from story" = "No se logró extraer enlace de la foto de la historia";
"Could not extract video url from post" = "No se logró extraer enlace del video de la publicación";
"Could not extract video url from reel" = "No se logró extraer enlace del video del reel";
"Could not extract video url from story" = "No se logró extraer enlace del video de la historia";
"Download Quality" = "Calidad de descarga";
"FFmpegKit Debug" = "FFmpegKit Debug";
"Later" = "Mas tarde";
"No!" = "¡No!";
"Restart" = "Reiniciar";
"Restart required" = "Reinicio requerido";
"Yes" = "Si";
"You must restart the app to apply this change" = "Debes reiniciar la aplicación para aplicar este cambio";
//////////////////////////////////////////////////////////////////////////////
// ABOUT / CREDITS //
// Settings → Credits footer. //
//////////////////////////////////////////////////////////////////////////////
"%@ — view source, report issues, see releases" = "%@ — ver código fuente, reportar problemas y ver lanzamientos";
"Credits" = "Créditos";
"Developer" = "Desarrollador";
"Donate to SoCuul" = "Donar a SoCuul";
"Original SCInsta developer" = "Desarrollador Original SCInsta";
"Ryuk" = "Ryuk";
"RyukGram %@\n\nInstagram v%@\n\nBased on SCInsta by SoCuul" = "RyukGram %@\n\nInstagram v%@\n\nBasado en SCInsta por SoCuul";
"RyukGram on GitHub" = "RyukGram en GitHub";
"SoCuul" = "SoCuul";
"Support the original developer" = "Apoyar al desarrollador original";
"View Repo" = "Ver Repo";
"View the source code on GitHub" = "Ver el código fuente en GitHub";
/* [ADDED_BY_TRANSLATOR] */ "Translator" = "Traductor";
/* [ADDED_BY_TRANSLATOR] */ "Flamako" = "Flamako";
//////////////////////////////////////////////////////////////////////////////
// HD DOWNLOADS //
// Enhanced / HD downloads settings (DASH + FFmpegKit encoding). //
//////////////////////////////////////////////////////////////////////////////
"Download video at the highest available quality" = "Descargar video en la mejor calidad disponible";
"Downloads HD video via DASH streams and encodes to H.264. Requires FFmpegKit." = "Descarga el video en HD mediante transmisión DASH y lo codifica en H.264. Requiere FFmpegKit.";
"Encoding speed" = "Velocidad de codificación";
"Enhanced downloads" = "Mejorar descargas";
"FFmpegKit is not available. Install the sideloaded IPA or the _ffmpeg .deb variant to enable." = "FFmpegKit no está disponible. Instala el archivo IPA descargado o la variante .deb de _ffmpeg para activarlo.";
"Faster = lower quality" = "Más rápido = Menor calidad";
"Photo quality" = "Calidad de imagen";
"Use highest resolution available" = "Usar la resolución mas alta disponible";
"Video quality" = "Calidad de video";
"Which quality to download" = "En qué calidad descargar";
//////////////////////////////////////////////////////////////////////////////
// EXPERIMENTAL / DEBUG //
// Placeholder rows only shown in the experimental settings sandbox. //
//////////////////////////////////////////////////////////////////////////////
"Navigation Cell" = "Celda de navegación";
/* [ADDED_BY_DEV] */ "Localization" = "Traducción";
/* [ADDED_BY_DEV] */ "Update localization file" = "Actualizar traducción";
/* [ADDED_BY_DEV] */ "Import a .strings file for a language" = "Importa un archivo .strings para añadir un idioma";
/* [ADDED_BY_DEV] */ "Import a .strings file to update a translation. Pick a language, select the file, restart." = "Importa un archivo .strings para actualizar una traducción. Escoge un idioma, selecciona el archivo y reinicia";
/* [ADDED_BY_DEV] */ "Export English strings" = "Exportar archivo .strings en Inglés";
/* [ADDED_BY_DEV] */ "Share the base English .strings file for translating" = "Comparte el archivo .strings en Inglés para traducir";
+3
View File
@@ -27,6 +27,9 @@ NSString *SCIResolvedLanguageCode(void);
// Invalidate cached bundles/strings after a language switch.
void SCILocalizationReset(void);
// Writable path for user-imported lproj overrides (Library/RyukGram.bundle/).
NSString *SCILocalizationOverridePath(void);
#ifdef __cplusplus
}
#endif
+49 -8
View File
@@ -8,6 +8,11 @@ static NSBundle *gLanguageBundle = nil;
static NSString *gLanguageBundleCode = nil;
static dispatch_once_t gResourceOnce;
NSString *SCILocalizationOverridePath(void) {
NSString *lib = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;
return [lib stringByAppendingPathComponent:@"RyukGram.bundle"];
}
static NSBundle *resolveResourceBundle(void) {
// 1) Sideload: cyan copies RyukGram.bundle into the app's resource root.
NSString *path = [[NSBundle mainBundle] pathForResource:@"RyukGram" ofType:@"bundle"];
@@ -66,9 +71,17 @@ static NSBundle *activeLanguageBundle(void) {
NSString *code = preferredLanguageCode(resource);
if (gLanguageBundle && [code isEqualToString:gLanguageBundleCode]) return gLanguageBundle;
NSString *lprojPath = [resource pathForResource:code ofType:@"lproj"];
if (!lprojPath) lprojPath = [resource pathForResource:@"en" ofType:@"lproj"];
gLanguageBundle = lprojPath ? [NSBundle bundleWithPath:lprojPath] : resource;
// User-imported overrides take priority (writable Library dir).
NSString *overrideLproj = [[SCILocalizationOverridePath()
stringByAppendingPathComponent:[code stringByAppendingString:@".lproj"]]
stringByAppendingPathComponent:@"Localizable.strings"];
if ([[NSFileManager defaultManager] fileExistsAtPath:overrideLproj]) {
gLanguageBundle = [NSBundle bundleWithPath:[overrideLproj stringByDeletingLastPathComponent]];
} else {
NSString *lprojPath = [resource pathForResource:code ofType:@"lproj"];
if (!lprojPath) lprojPath = [resource pathForResource:@"en" ofType:@"lproj"];
gLanguageBundle = lprojPath ? [NSBundle bundleWithPath:lprojPath] : resource;
}
gLanguageBundleCode = [code copy];
return gLanguageBundle;
}
@@ -86,11 +99,39 @@ NSString *SCILocalizedString(NSString *key, NSString *fallback) {
}
NSArray<NSDictionary<NSString *, NSString *> *> *SCIAvailableLanguages(void) {
// `code` is what we persist; `native` is shown in the picker (endonyms read best).
return @[
@{ @"code": @"system", @"native": @"System", @"english": @"System default" },
@{ @"code": @"en", @"native": @"English", @"english": @"English" },
];
NSMutableArray *result = [NSMutableArray array];
[result addObject:@{@"code": @"system", @"native": @"System"}];
[result addObject:@{@"code": @"en", @"native": @"English"}];
NSFileManager *fm = [NSFileManager defaultManager];
NSMutableSet *seen = [NSMutableSet setWithObject:@"en"];
// Scan both shipped bundle + writable override dir for .lproj dirs.
NSMutableArray *searchPaths = [NSMutableArray array];
NSBundle *res = SCILocalizationBundle();
if (res) [searchPaths addObject:res.bundlePath];
NSString *overrides = SCILocalizationOverridePath();
if ([fm fileExistsAtPath:overrides]) [searchPaths addObject:overrides];
for (NSString *base in searchPaths) {
NSArray *contents = [fm contentsOfDirectoryAtPath:base error:nil];
for (NSString *name in [contents sortedArrayUsingSelector:@selector(compare:)]) {
if (![name hasSuffix:@".lproj"]) continue;
NSString *code = [name stringByDeletingPathExtension];
if ([code isEqualToString:@"Base"] || [seen containsObject:code]) continue;
NSString *stringsPath = [[base stringByAppendingPathComponent:name]
stringByAppendingPathComponent:@"Localizable.strings"];
if (![fm fileExistsAtPath:stringsPath]) continue;
[seen addObject:code];
NSLocale *loc = [NSLocale localeWithLocaleIdentifier:code];
NSString *native = [loc localizedStringForLanguageCode:code] ?: code;
if (native.length) native = [[[native substringToIndex:1] uppercaseString]
stringByAppendingString:[native substringFromIndex:1]];
[result addObject:@{@"code": code, @"native": native}];
}
}
return result;
}
void SCILocalizationReset(void) {
+64 -1
View File
@@ -1,5 +1,7 @@
#import "SCIExcludedChatsViewController.h"
#import "../Features/StoriesAndMessages/SCIExcludedThreads.h"
#import "../Networking/SCIInstagramAPI.h"
#import "../Utils.h"
@interface SCIExcludedChatsViewController ()
@property (nonatomic, strong) UITableView *tableView;
@@ -52,7 +54,9 @@
style:UIBarButtonItemStylePlain target:self action:@selector(toggleSort)];
self.editBtn = [[UIBarButtonItem alloc]
initWithTitle:SCILocalized(@"Select") style:UIBarButtonItemStylePlain target:self action:@selector(toggleEdit)];
self.navigationItem.rightBarButtonItems = @[self.editBtn, self.sortBtn];
UIBarButtonItem *addBtn = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addUserTapped)];
self.navigationItem.rightBarButtonItems = @[self.editBtn, self.sortBtn, addBtn];
[self reload];
}
@@ -111,6 +115,65 @@
[self presentViewController:sheet animated:YES completion:nil];
}
- (void)addUserTapped {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:SCILocalized(@"Add chat")
message:SCILocalized(@"Enter username of the DM thread")
preferredStyle:UIAlertControllerStyleAlert];
[alert addTextFieldWithConfigurationHandler:^(UITextField *tf) { tf.placeholder = @"username"; tf.autocapitalizationType = UITextAutocapitalizationTypeNone; }];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Search") style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) {
NSString *q = [alert.textFields.firstObject.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (!q.length) return;
[self lookupUsername:q];
}]];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)lookupUsername:(NSString *)username {
// Step 1: resolve user info.
[SCIInstagramAPI sendRequestWithMethod:@"GET"
path:[NSString stringWithFormat:@"users/web_profile_info/?username=%@", username]
body:nil completion:^(NSDictionary *resp, NSError *err) {
NSDictionary *user = resp[@"data"][@"user"];
if (!user || err) {
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:SCILocalized(@"User '%@' not found"), username]];
return;
}
NSString *pk = [user[@"id"] description] ?: @"";
NSString *uname = user[@"username"] ?: username;
NSString *fullName = user[@"full_name"] ?: @"";
if (!pk.length) { [SCIUtils showErrorHUDWithDescription:SCILocalized(@"Could not resolve user ID")]; return; }
// Step 2: resolve DM thread with this user.
[SCIInstagramAPI sendRequestWithMethod:@"GET"
path:[NSString stringWithFormat:@"direct_v2/threads/get_by_participants/?recipient_users=[%@]", pk]
body:nil completion:^(NSDictionary *threadResp, NSError *tErr) {
NSString *threadId = threadResp[@"thread"][@"thread_id"];
NSString *threadName = threadResp[@"thread"][@"thread_title"] ?: uname;
if (!threadId.length || tErr) {
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:SCILocalized(@"No DM thread found with @%@"), uname]];
return;
}
NSString *msg = [NSString stringWithFormat:@"@%@%@", uname, fullName.length ? [NSString stringWithFormat:@" (%@)", fullName] : @""];
UIAlertController *confirm = [UIAlertController alertControllerWithTitle:SCILocalized(@"Add to list?")
message:msg
preferredStyle:UIAlertControllerStyleAlert];
[confirm addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
[confirm addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Add") style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) {
[SCIExcludedThreads addOrUpdateEntry:@{
@"threadId": threadId,
@"threadName": threadName,
@"isGroup": @NO,
@"users": @[@{@"pk": pk, @"username": uname, @"fullName": fullName}],
}];
[self reload];
}]];
[self presentViewController:confirm animated:YES completion:nil];
}];
}];
}
- (void)toggleSort {
UIAlertController *sheet = [UIAlertController alertControllerWithTitle:SCILocalized(@"Sort by")
message:nil
@@ -1,5 +1,7 @@
#import "SCIExcludedStoryUsersViewController.h"
#import "../Features/StoriesAndMessages/SCIExcludedStoryUsers.h"
#import "../Networking/SCIInstagramAPI.h"
#import "../Utils.h"
@interface SCIExcludedStoryUsersViewController ()
@property (nonatomic, strong) UITableView *tableView;
@@ -52,7 +54,9 @@
style:UIBarButtonItemStylePlain target:self action:@selector(toggleSort)];
self.editBtn = [[UIBarButtonItem alloc]
initWithTitle:SCILocalized(@"Select") style:UIBarButtonItemStylePlain target:self action:@selector(toggleEdit)];
self.navigationItem.rightBarButtonItems = @[self.editBtn, self.sortBtn];
UIBarButtonItem *addBtn = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addUserTapped)];
self.navigationItem.rightBarButtonItems = @[self.editBtn, self.sortBtn, addBtn];
[self reload];
}
@@ -82,6 +86,47 @@
[self reload];
}
- (void)addUserTapped {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:SCILocalized(@"Add user")
message:SCILocalized(@"Enter username")
preferredStyle:UIAlertControllerStyleAlert];
[alert addTextFieldWithConfigurationHandler:^(UITextField *tf) { tf.placeholder = @"username"; tf.autocapitalizationType = UITextAutocapitalizationTypeNone; }];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Search") style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) {
NSString *q = [alert.textFields.firstObject.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (!q.length) return;
[self lookupUsername:q];
}]];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)lookupUsername:(NSString *)username {
[SCIInstagramAPI sendRequestWithMethod:@"GET"
path:[NSString stringWithFormat:@"users/web_profile_info/?username=%@", username]
body:nil completion:^(NSDictionary *resp, NSError *err) {
NSDictionary *user = resp[@"data"][@"user"];
if (!user || err) {
[SCIUtils showErrorHUDWithDescription:[NSString stringWithFormat:SCILocalized(@"User '%@' not found"), username]];
return;
}
NSString *pk = [user[@"id"] description] ?: @"";
NSString *uname = user[@"username"] ?: username;
NSString *fullName = user[@"full_name"] ?: @"";
if (!pk.length) { [SCIUtils showErrorHUDWithDescription:SCILocalized(@"Could not resolve user ID")]; return; }
NSString *msg = [NSString stringWithFormat:@"@%@%@", uname, fullName.length ? [NSString stringWithFormat:@" (%@)", fullName] : @""];
UIAlertController *confirm = [UIAlertController alertControllerWithTitle:SCILocalized(@"Add to list?")
message:msg
preferredStyle:UIAlertControllerStyleAlert];
[confirm addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Cancel") style:UIAlertActionStyleCancel handler:nil]];
[confirm addAction:[UIAlertAction actionWithTitle:SCILocalized(@"Add") style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) {
[SCIExcludedStoryUsers addOrUpdateEntry:@{@"pk": pk, @"username": uname, @"fullName": fullName}];
[self reload];
}]];
[self presentViewController:confirm animated:YES completion:nil];
}];
}
- (void)toggleSort {
UIAlertController *sheet = [UIAlertController alertControllerWithTitle:SCILocalized(@"Sort by")
message:nil
+13 -6
View File
@@ -88,14 +88,12 @@ static char rowStaticRef[] = "row";
initWithBarButtonSystemItem:UIBarButtonSystemItemClose
target:self action:@selector(sciDismissSettings)];
// Compact globe button English is the only shipped language for now,
// so the tap shows an info alert instead of a picker. Re-enable the
// menu below once additional translations land.
UIImage *globe = [UIImage systemImageNamed:@"globe"];
UIBarButtonItem *langItem = [[UIBarButtonItem alloc] initWithImage:globe
style:UIBarButtonItemStylePlain
target:self
action:@selector(sciShowLanguageInfo)];
target:nil
action:nil];
langItem.menu = [self sciBuildLanguageMenu];
self.navigationItem.rightBarButtonItem = langItem;
}
}
@@ -141,7 +139,16 @@ static char rowStaticRef[] = "row";
[actions addObject:action];
}
return [UIMenu menuWithTitle:SCILocalized(@"settings.language.title") children:actions];
UIAction *help = [UIAction actionWithTitle:[NSString stringWithFormat:@"❤️ %@", SCILocalized(@"settings.language.help_translate")]
image:nil
identifier:nil
handler:^(__unused UIAction *a) {
NSURL *url = [NSURL URLWithString:@"https://github.com/faroukbmiled/RyukGram#translating-ryukgram"];
if (url) [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}];
return [UIMenu menuWithTitle:SCILocalized(@"settings.language.title")
children:[actions arrayByAddingObject:help]];
}
- (void)sciApplyLanguageChange {
+174 -3
View File
@@ -8,6 +8,53 @@
#import "SCIEmbedDomainViewController.h"
#import "SCIDateFormatPickerVC.h"
#import "../SCIFFmpeg.h"
#import <objc/runtime.h>
// Copies imported .strings into the writable override dir.
@interface SCILocImportHelper : NSObject <UIDocumentPickerDelegate>
@end
@implementation SCILocImportHelper
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
if (!urls.count) return;
NSURL *src = urls.firstObject;
NSString *code = objc_getAssociatedObject(controller, "sci_lang");
if (!code.length) return;
// Validate it parses
NSDictionary *test = [NSDictionary dictionaryWithContentsOfURL:src];
if (!test.count) {
UIAlertController *a = [UIAlertController alertControllerWithTitle:@"Error"
message:@"File is empty or not a valid .strings file." preferredStyle:UIAlertControllerStyleAlert];
[a addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
UIViewController *top = controller.presentingViewController ?: UIApplication.sharedApplication.keyWindow.rootViewController;
[top presentViewController:a animated:YES completion:nil];
return;
}
// Write to the writable override dir (Library/RyukGram.bundle/<code>.lproj/).
NSString *lproj = [NSString stringWithFormat:@"%@.lproj", code];
NSString *dir = [SCILocalizationOverridePath() stringByAppendingPathComponent:lproj];
NSFileManager *fm = [NSFileManager defaultManager];
[fm createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil];
NSString *dest = [dir stringByAppendingPathComponent:@"Localizable.strings"];
[fm removeItemAtPath:dest error:nil];
BOOL ok = [fm copyItemAtPath:src.path toPath:dest error:nil];
NSString *msg = ok
? [NSString stringWithFormat:@"Updated %@ (%ld keys). Restart to apply.", code, (long)test.count]
: @"Could not write file.";
UIAlertController *a = [UIAlertController alertControllerWithTitle:ok ? @"Done" : @"Error"
message:msg preferredStyle:UIAlertControllerStyleAlert];
if (ok) {
[a addAction:[UIAlertAction actionWithTitle:@"Restart now" style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *x) { [SCIUtils showRestartConfirmation]; }]];
}
[a addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
UIViewController *top = UIApplication.sharedApplication.keyWindow.rootViewController;
while (top.presentedViewController) top = top.presentedViewController;
[top presentViewController:a animated:YES completion:nil];
}
@end
@implementation SCITweakSettings
@@ -182,7 +229,7 @@
@"header": SCILocalized(@"Seen receipts"),
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Disable story seen receipt") subtitle:SCILocalized(@"Hides the notification for others when you view their story") defaultsKey:@"no_seen_receipt"],
[SCISetting switchCellWithTitle:SCILocalized(@"Keep stories visually unseen") subtitle:SCILocalized(@"Prevents stories from visually marking as seen in the tray (keeps colorful ring)") defaultsKey:@"no_seen_visual"],
[SCISetting switchCellWithTitle:SCILocalized(@"Keep stories visually seen locally") subtitle:SCILocalized(@"Marks stories as seen locally (grey ring) while still blocking the seen receipt on the server") defaultsKey:@"keep_seen_visual_local"],
[SCISetting switchCellWithTitle:SCILocalized(@"Mark seen on story like") subtitle:SCILocalized(@"Marks a story as seen the moment you tap the heart, even with seen blocking on") defaultsKey:@"seen_on_story_like"],
[SCISetting switchCellWithTitle:SCILocalized(@"Mark seen on story reply") subtitle:SCILocalized(@"Marks a story as seen when you send a reply or emoji reaction, even with seen blocking on") defaultsKey:@"seen_on_story_reply"],
[SCISetting menuCellWithTitle:SCILocalized(@"Manual seen button mode") subtitle:SCILocalized(@"Button = single-tap mark seen. Toggle = tap toggles story read receipts on/off (eye fills blue when on)") menu:[self menus][@"story_seen_mode"]],
@@ -255,6 +302,7 @@
@"header": @"",
@"rows": @[
[SCISetting menuCellWithTitle:SCILocalized(@"Tap Controls") subtitle:SCILocalized(@"Change what happens when you tap on a reel") menu:[self menus][@"reels_tap_control"]],
[SCISetting menuCellWithTitle:SCILocalized(@"Auto-scroll reels") subtitle:SCILocalized(@"IG default: native behavior. RyukGram: re-advances after swiping back.") menu:[self menus][@"auto_scroll_reels_mode"]],
[SCISetting switchCellWithTitle:SCILocalized(@"Always show progress scrubber") subtitle:SCILocalized(@"Forces the progress bar to appear on every reel") defaultsKey:@"reels_show_scrubber"],
[SCISetting switchCellWithTitle:SCILocalized(@"Disable auto-unmuting reels") subtitle:SCILocalized(@"Prevents reels from unmuting when the volume/silent button is pressed") defaultsKey:@"disable_auto_unmuting_reels" requiresRestart:YES],
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm reel refresh") subtitle:SCILocalized(@"Shows an alert when you trigger a reels refresh") defaultsKey:@"refresh_reel_confirm"],
@@ -468,8 +516,10 @@
navSections:@[@{
@"header": @"",
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm like: Posts/Stories") subtitle:SCILocalized(@"Shows an alert when you click the like button on posts or stories to confirm the like") defaultsKey:@"like_confirm"],
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm like: Reels") subtitle:SCILocalized(@"Shows an alert when you click the like button on reels to confirm the like") defaultsKey:@"like_confirm_reels"]
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm like: Posts") subtitle:SCILocalized(@"Shows an alert when you click the like button on posts to confirm the like") defaultsKey:@"like_confirm"],
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm like: Reels") subtitle:SCILocalized(@"Shows an alert when you click the like button on reels to confirm the like") defaultsKey:@"like_confirm_reels"],
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm story like") subtitle:SCILocalized(@"Shows an alert when you click the like button on stories to confirm the like") defaultsKey:@"story_like_confirm"],
[SCISetting switchCellWithTitle:SCILocalized(@"Confirm story emoji reaction") subtitle:SCILocalized(@"Shows an alert before sending an emoji reaction on a story") defaultsKey:@"emoji_reaction_confirm"]
]
},
@{
@@ -569,6 +619,22 @@
subtitle:@""
icon:[SCISymbol symbolWithName:@"ladybug"]
navSections:@[@{
@"header": SCILocalized(@"Localization"),
@"footer": SCILocalized(@"Import a .strings file to update a translation. Pick a language, select the file, restart."),
@"rows": @[
[SCISetting buttonCellWithTitle:SCILocalized(@"Update localization file")
subtitle:SCILocalized(@"Import a .strings file for a language")
icon:[SCISymbol symbolWithName:@"square.and.arrow.down"]
action:^(void) { [self presentLocalizationImport]; }
],
[SCISetting buttonCellWithTitle:SCILocalized(@"Export English strings")
subtitle:SCILocalized(@"Share the base English .strings file for translating")
icon:[SCISymbol symbolWithName:@"square.and.arrow.up"]
action:^(void) { [self exportEnglishStrings]; }
],
]
},
@{
@"header": @"FLEX",
@"rows": @[
[SCISetting switchCellWithTitle:SCILocalized(@"Enable FLEX gesture") subtitle:SCILocalized(@"Hold 5 fingers on the screen to open FLEX") defaultsKey:@"flex_instagram"],
@@ -682,6 +748,102 @@
return SCILocalized(@"settings.title");
}
// MARK: - Localization import
static UIViewController *sciTopVC(void) {
UIViewController *top = nil;
for (UIWindow *w in UIApplication.sharedApplication.windows) {
if (!w.isKeyWindow) continue;
top = w.rootViewController;
while (top.presentedViewController) top = top.presentedViewController;
}
return top;
}
+ (void)exportEnglishStrings {
NSBundle *res = SCILocalizationBundle();
NSString *path = [res pathForResource:@"en" ofType:@"lproj"];
if (path) path = [path stringByAppendingPathComponent:@"Localizable.strings"];
if (!path || ![[NSFileManager defaultManager] fileExistsAtPath:path]) {
[SCIUtils showErrorHUDWithDescription:@"English .strings file not found"];
return;
}
NSURL *url = [NSURL fileURLWithPath:path];
UIActivityViewController *ac = [[UIActivityViewController alloc] initWithActivityItems:@[url] applicationActivities:nil];
UIViewController *top = sciTopVC();
if (!top) return;
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
ac.popoverPresentationController.sourceView = top.view;
ac.popoverPresentationController.sourceRect = CGRectMake(CGRectGetMidX(top.view.bounds), CGRectGetMidY(top.view.bounds), 1, 1);
}
[top presentViewController:ac animated:YES completion:nil];
}
+ (void)presentLocalizationImport {
NSArray *langs = SCIAvailableLanguages();
UIAlertController *picker = [UIAlertController alertControllerWithTitle:@"Update localization"
message:@"Pick a language to update, or add a new one"
preferredStyle:UIAlertControllerStyleActionSheet];
for (NSDictionary *lang in langs) {
NSString *code = lang[@"code"];
if ([code isEqualToString:@"system"]) continue;
NSString *title = [NSString stringWithFormat:@"%@ (%@)", lang[@"native"], code];
[picker addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *a) {
[self importStringsForLanguage:code];
}]];
}
[picker addAction:[UIAlertAction actionWithTitle:@"+ Add new language"
style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *a) {
[self promptNewLanguageCode];
}]];
[picker addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[sciTopVC() presentViewController:picker animated:YES completion:nil];
}
+ (void)promptNewLanguageCode {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Add language"
message:@"Enter the language code (e.g. fr, de, ja)"
preferredStyle:UIAlertControllerStyleAlert];
[alert addTextFieldWithConfigurationHandler:^(UITextField *tf) { tf.placeholder = @"fr"; }];
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:@"Next" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *a) {
NSString *code = [alert.textFields.firstObject.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (code.length < 2 || code.length > 5) return;
[self importStringsForLanguage:code];
}]];
[sciTopVC() presentViewController:alert animated:YES completion:nil];
}
+ (void)importStringsForLanguage:(NSString *)langCode {
UIViewController *top = sciTopVC();
if (!top) return;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
UIDocumentPickerViewController *dp = [[UIDocumentPickerViewController alloc]
initWithDocumentTypes:@[@"public.plain-text", @"com.apple.xcode.strings-text", @"public.data"] inMode:UIDocumentPickerModeImport];
#pragma clang diagnostic pop
dp.allowsMultipleSelection = NO;
dp.delegate = (id<UIDocumentPickerDelegate>)[self sharedImportHelper];
objc_setAssociatedObject(dp, "sci_lang", [langCode copy], OBJC_ASSOCIATION_COPY_NONATOMIC);
[top presentViewController:dp animated:YES completion:nil];
}
+ (id)sharedImportHelper {
static id helper = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
helper = [SCILocImportHelper new];
});
return helper;
}
// MARK: - Menus
@@ -876,6 +1038,15 @@
]
]],
@"auto_scroll_reels_mode": [UIMenu menuWithChildren:@[
[UICommand commandWithTitle:SCILocalized(@"Off") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"auto_scroll_reels_mode", @"value": @"off"}],
[UICommand commandWithTitle:SCILocalized(@"IG default") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"auto_scroll_reels_mode", @"value": @"ig"}],
[UICommand commandWithTitle:SCILocalized(@"RyukGram") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"auto_scroll_reels_mode", @"value": @"custom"}],
]],
@"launch_tab": [UIMenu menuWithChildren:@[
[UICommand commandWithTitle:SCILocalized(@"Default") image:nil action:@selector(menuChanged:)
propertyList:@{@"defaultsKey": @"launch_tab", @"value": @"default"}],
+3 -2
View File
@@ -14,7 +14,7 @@
///////////////////////////////////////////////////////////
// * Tweak version *
NSString *SCIVersionString = @"v1.2.0";
NSString *SCIVersionString = @"v1.2.1";
// Variables that work across features
BOOL dmVisualMsgsViewedButtonEnabled = false;
@@ -86,9 +86,10 @@ BOOL dmVisualMsgsViewedButtonEnabled = false;
@"enable_notes_customization": @(YES),
@"custom_note_themes": @(YES),
@"disable_auto_unmuting_reels": @(NO),
@"auto_scroll_reels_mode": @"off",
@"settings_shortcut": @(YES),
@"doom_scrolling_reel_count": @(1),
@"no_seen_visual": @(YES),
@"keep_seen_visual_local": @(NO),
@"send_audio_as_file": @(YES),
@"download_audio_message": @(NO),
@"save_to_ryukgram_album": @(NO),