Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions build-system/Make/Make.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(self, bazel, override_bazel_version, override_xcode_version, bazel_
self.show_actions = False
self.enable_sandbox = False
self.disable_provisioning_profiles = False
self.disable_extensions = False
self.profile_swift = False
self.embed_watch_app = False
self.watch_api_id = None
Expand Down Expand Up @@ -138,6 +139,9 @@ def set_split_swiftmodules(self, value):
def set_disable_provisioning_profiles(self):
self.disable_provisioning_profiles = True

def set_disable_extensions(self):
self.disable_extensions = True

def set_profile_swift(self, value):
self.profile_swift = value

Expand Down Expand Up @@ -298,6 +302,8 @@ def invoke_build(self):

if self.disable_provisioning_profiles:
combined_arguments += ['--//Telegram:disableProvisioningProfiles']
if self.disable_extensions:
combined_arguments += ['--//Telegram:disableExtensions']

combined_arguments += self.common_args
combined_arguments += self.common_build_args
Expand Down Expand Up @@ -406,6 +412,8 @@ def invoke_spm_build(self):

if self.disable_provisioning_profiles:
combined_arguments += ['--//Telegram:disableProvisioningProfiles']
if self.disable_extensions:
combined_arguments += ['--//Telegram:disableExtensions']

combined_arguments += self.common_args
combined_arguments += self.common_build_args
Expand Down Expand Up @@ -693,6 +701,10 @@ def build(bazel, arguments):
else:
print('TelegramBuild: warning: --embedWatchApp requires a device configuration (debug_arm64 or release_arm64); ignored for simulator builds.')
bazel_command_line.set_build_number(arguments.buildNumber)
if arguments.disableExtensions:
bazel_command_line.set_disable_extensions()
if arguments.disableProvisioningProfiles:
bazel_command_line.set_disable_provisioning_profiles()
bazel_command_line.set_custom_target(arguments.target)
bazel_command_line.set_continue_on_error(arguments.continueOnError)
bazel_command_line.set_show_actions(arguments.showActions)
Expand Down Expand Up @@ -1038,6 +1050,22 @@ def add_project_and_build_common_arguments(current_parser: argparse.ArgumentPars
metavar='number'
)
add_project_and_build_common_arguments(buildParser)
buildParser.add_argument(
'--disableExtensions',
action='store_true',
default=False,
help='''
The build will not include app extensions.
'''
)
buildParser.add_argument(
'--disableProvisioningProfiles',
action='store_true',
default=False,
help='''
This allows to build the project for simulator without having any codesigning identities installed.
'''
)
buildParser.add_argument(
'--configuration',
choices=[
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/ipad-mini-compact-avatar-rail/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-30
47 changes: 47 additions & 0 deletions openspec/changes/ipad-mini-compact-avatar-rail/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## Context

On iPad mini and small tablet viewports, the master pane in `NavigationSplitContainer` can collapse to a compact sidebar (`masterWidth <= 160.0`, typically ~96pt).
Currently, the Telegram chat list implementation assumes full-width presentation, causing:
1. Gesture conflicts: `panRecognizer` (folder swiping) and `selectionRecognizer` (chat selection) steal touches from avatar items.
2. Inadvertent story triggers: overscroll pulling or upper region interactions trigger story creation or leave large blank gaps (`storiesInset`).
3. Scroll conflicts with static overlays: placing static touch blockers over a scrollable `ListView` causes scrolled-up items to become unclickable.

## Goals / Non-Goals

**Goals:**
- Eliminate all gesture conflicts by disabling extraneous recognizers (`panRecognizer`, `selectionRecognizer`, story gestures) directly at the recognizer level when `width <= 160.0`.
- Remove all artificial static overlay blockers and derive the top inset cleanly from `statusBarHeight` + standard padding.
- Guarantee that 100% of visible chat avatars are responsive to single taps across all scroll positions and across all folder tabs.
- Seamlessly restore standard full chat list controls (Search, Folder tabs, Story strips, Bottom Tab Bar, gestures) when expanding to regular width (`masterWidth > 160.0`).

**Non-Goals:**
- Creating custom rich folder navigation within the narrow avatar rail (folders remain accessible in full mode).
- Modifying chat list behavior on iPhone or standard large iPad viewports.

## Decisions

### Decision 1: Direct Recognizer Disabling instead of Static Touch Blockers
- **Choice**: When `isCompactAvatarRail` (`tablet && width <= 160.0`):
- Configure `panRecognizer.allowedDirections` to return `[]` and set `panRecognizer.isEnabled = false`.
- Set `isSelectionGestureEnabled = false` on `ChatListNode`.
- Remove `compactRailTopTouchBlockerNode` completely.
- **Rationale**: Static overlays break hit-testing when the list is scrolled. Disabling recognizers directly ensures touch events pass cleanly to the `ListView` items without unintended gesture cancellations (`touchesCancelled`).

### Decision 2: Clean Safe-Area-Derived Top Inset
- **Choice**: In `ChatListContainerItemNode`, set the top inset for compact rail to `layout.statusBarHeight` (or safe area top) plus a standard 12pt visual margin, avoiding arbitrary large hardcoded offsets.
- **Rationale**: Provides consistent, natural top spacing aligned with iOS system standards while keeping items accessible and scrollable.

### Decision 3: Total Story Mechanics Suppression
- **Choice**: In `ChatListControllerNode`, clamp `tempTopInset = 0.0`, `storiesInset = 0.0`, `startedScrollingAtUpperBound = false`, and supply empty `effectiveStorySubscriptions` for compact mode. Propagate `effectiveStoriesInset = 0.0` to all container item nodes across all tabs.
- **Rationale**: Completely eliminates story creation triggers, story header expansions, and residual top gaps on overscroll or double-tap.

### Decision 4: Deterministic Hit-Test Frame Routing
- **Choice**: Verify that `ChatListItemNode` spans the full rail width (0..`masterWidth`) and that `NavigationSplitContainer.resizeHandle` hit-test is constrained to the boundary region (`+-6pt`).
- **Rationale**: Ensures every tap on a visible avatar row reliably triggers `interaction.peerSelected(...)`.

## Risks / Trade-offs

- **[Risk]** Disabling folder swiping prevents switching folders by dragging the sidebar.
→ **Mitigation**: In a 96pt rail, folder swiping was an accidental trigger hazard. Users switch folders by expanding to regular master width or using folder shortcuts.
- **[Risk]** Resizing master width back to regular must re-enable gestures cleanly.
→ **Mitigation**: All recognizers and insets are re-evaluated dynamically in `update(...)` and `containerLayoutUpdated(...)`.
41 changes: 41 additions & 0 deletions openspec/changes/ipad-mini-compact-avatar-rail/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## Why

On iPad mini (and narrow iPad split-view widths `<= 160pt`), collapsing the chat list into a compact sidebar currently leads to several severe UX, hit-testing, and gesture conflicts:
1. `panRecognizer` in `ChatListContainerNode` attempts horizontal folder transitions across the narrow 96pt rail, cancelling avatar touch events (`touchesCancelled` in `ListView`).
2. `selectionRecognizer` in `ChatListNode` remains enabled, allowing multi-touch or accidental gestures to inadvertently trigger selection/edit mode (checkboxes) instead of opening chats.
3. Full chat list story mechanics (overscroll expansion, story creation hooks, `tempTopInset`) leak into the collapsed sidebar, causing accidental story creation or residual empty gaps.
4. Static overlay blocker nodes introduce scroll collision defects (items scrolling under a static blocker lose tap responsiveness).

This change replaces makeshift overlays with an upstream-clean architectural solution: explicit gesture disabling at the recognizer level, safe-area-derived insets, and robust avatar hit-testing across all folder tabs.

## What Changes

- **Gesture Isolation**: When in compact avatar rail mode (`tablet && width <= 160pt`):
- Disable `panRecognizer` (folder swipe gesture) by returning empty allowed directions `[]` and setting `isEnabled = false`.
- Disable `selectionRecognizer` by setting `isSelectionGestureEnabled = false`.
- Disable all story posting / camera pan gestures and lock `tempTopInset = 0.0`, `storiesInset = 0.0`.
- **Clean Inset Hierarchy (No Static Overlays)**:
- Eliminate fake touch-blocker overlays (`compactRailTopTouchBlockerNode`).
- Derive top insets strictly from `statusBarHeight` + clean padding (e.g. `12pt`), ensuring list items remain fully responsive at all scroll positions.
- **Accurate Hit-Testing for Avatars**:
- Full item width (0..`masterWidth`) routes single tap events directly to `interaction.peerSelected(...)`.
- Ensure `resizeHandle` touch bounds do not interfere with avatar hit zones.
- **Consistent Folder Support**:
- Apply identical zero-story-inset and gesture-suppression guarantees uniformly across root chat list and all folder-filtered tabs.
- **Clean Full-List Restoration**:
- Seamlessly re-enable folder swiping, selection gestures, search bar, and story headers when the master pane expands to regular width (`> 160pt`).

## Capabilities

### New Capabilities
- `compact-avatar-rail`: Clean gesture suppression, safe-area insets, and deterministic avatar hit-testing for iPad compact sidebar.

### Modified Capabilities
<!-- None -->

## Impact

- `submodules/ChatListUI/Sources/ChatListControllerNode.swift`: Remove static touch blockers, disable `panRecognizer` in compact mode, clamp `tempTopInset` and `storiesInset` to 0.0.
- `submodules/ChatListUI/Sources/Node/ChatListNode.swift`: Disable `isSelectionGestureEnabled` when width `<= 160.0`.
- `submodules/ChatListUI/Sources/ChatListContainerItemNode.swift`: Clean safe-area-based top list insets without magic number offsets.
- `submodules/Display/Source/Navigation/NavigationSplitContainer.swift`: Tighten `resizeHandle` hit-test bounds.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## ADDED Requirements

### Requirement: Gesture Isolation in Compact Avatar Rail
In compact avatar rail mode on iPad (`tablet && width <= 160.0`), the system SHALL disable folder horizontal swipe gesture recognizers and selection mode gesture recognizers.

#### Scenario: User swipes or drags horizontally across the compact rail
- **WHEN** the master pane width is 160pt or less on an iPad device and a touch occurs on the avatar list
- **THEN** the `panRecognizer` does not intercept the gesture and does not cancel the item touch event

#### Scenario: User touches with multiple fingers or performs selection gestures
- **WHEN** the master pane width is 160pt or less on an iPad device
- **THEN** selection/edit mode (checkbox overlay) is not activated

### Requirement: Chat Avatar Hit-Testing and Responsiveness Across All Scroll Positions
In compact avatar rail mode, every visible chat avatar SHALL receive touch events and open the corresponding chat view on a single tap, without obstruction from static overlays.

#### Scenario: User taps any visible chat avatar
- **WHEN** the user taps any visible chat avatar in the compact rail (at initial scroll or after scrolling)
- **THEN** the system immediately opens the selected chat in the detail view pane

#### Scenario: User taps chat avatar inside a filtered folder
- **WHEN** the chat list is filtered by any folder and the user taps any visible avatar in the compact rail
- **THEN** the system opens the correct chat without activating story creation or misaligned touch targets

### Requirement: Suppression of Story Mechanics in Compact Mode
In compact avatar rail mode, the system SHALL strictly disable story header expansion, story posting availability, and overscroll top insets.

#### Scenario: User pulls down or double-taps upper area
- **WHEN** the user pulls down the chat list or taps the upper area in compact mode
- **THEN** no story creation screen opens and no residual `storiesInset` top gap remains

### Requirement: Clean Restoration on Expansion
When the master pane transitions from compact rail to regular width (`width > 160.0`), the system SHALL fully restore standard folder swipe gestures, selection gestures, search bar, and story headers.

#### Scenario: Master pane expands to regular width
- **WHEN** the user drags or triggers expansion of the master pane beyond 160pt
- **THEN** full search bar, folder tabs, folder swipe gestures, selection gestures, and story headers are restored with standard layout insets
29 changes: 29 additions & 0 deletions openspec/changes/ipad-mini-compact-avatar-rail/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## 1. Gesture Recognizer Isolation

- [x] 1.1 Disable `panRecognizer` in `ChatListContainerNode` when `isCompactAvatarRail` is true (allowed directions `[]` and `panRecognizer.isEnabled = false`).
- [x] 1.2 Disable `isSelectionGestureEnabled` on `ChatListNode` in compact rail mode to prevent unintended selection/edit mode triggers.
- [x] 1.3 Remove `compactRailTopTouchBlockerNode` static overlay to eliminate scroll collision defects.

## 2. Story Mechanics & Chrome Suppression

- [x] 2.1 Ensure `ChatListNavigationBar` view is completely hidden and non-interactive when `isCompactAvatarRail` is true.
- [x] 2.2 Enforce `tempTopInset = 0.0`, `storiesInset = 0.0`, and `startedScrollingAtUpperBound = false` in `ChatListControllerNode` and `ChatListContainerNode`.
- [x] 2.3 Verify bottom `TabBarComponent` is hidden during collapsed sidebar mode.

## 3. Hit-Testing, Insets, and Avatar Tap Verification

- [x] 3.1 Derive top list insets in `ChatListContainerItemNode` cleanly from `statusBarHeight` + margin without arbitrary large offsets.
- [x] 3.2 Verify hit-testing on all visible chat avatars at default position and after scrolling.
- [x] 3.3 Verify hit-testing across all folder-filtered lists so selecting any avatar opens the corresponding chat.
- [x] 3.4 Verify clean restoration of all controls and gesture recognizers when expanding master width back to normal (`> 160pt`).

## 4. Conformance and Build Verification

- [x] 4.1 Validate OpenSpec artifacts with `openspec validate --all --strict`.
- [x] 4.2 Perform simulator and device build verification to ensure clean compilation and stable runtime behavior.

## 5. Experimental Mode Integration

- [x] 5.1 Add `compactAvatarRail` experimental setting to `ExperimentalUISettings`.
- [x] 5.2 Implement compact mode toggling via Debug settings.
- [x] 5.3 Ensure `compactAvatarRail` setting persists through app restarts.
20 changes: 20 additions & 0 deletions openspec/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
schema: spec-driven

# Project context (optional)
# This is shown to AI when creating artifacts.
# Add your tech stack, conventions, style guides, domain knowledge, etc.
# Example:
# context: |
# Tech stack: TypeScript, React, Node.js
# We use conventional commits
# Domain: e-commerce platform

# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
# Example:
# rules:
# proposal:
# - Keep proposals under 500 words
# - Always include a "Non-goals" section
# tasks:
# - Break tasks into chunks of max 2 hours
4 changes: 4 additions & 0 deletions submodules/ChatListUI/Sources/ChatListContainerItemNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ final class ChatListContainerItemNode: ASDisplayNode {

var listInsets = insets
var additionalTopInset: CGFloat = 0.0
let isCompactAvatarRail = size.width <= 160.0
if isCompactAvatarRail {
listInsets.top = (insets.top > 0.0 ? insets.top : 24.0) + 12.0
}

if let chatFolderUpdates = self.chatFolderUpdates {
let topPanel: TopPanelItem
Expand Down
Loading