diff --git a/Info.plist b/Info.plist index 85f0971..bc11256 100644 --- a/Info.plist +++ b/Info.plist @@ -3,6 +3,6 @@ ITSAppUsesNonExemptEncryption - + diff --git a/Rosetta.xcodeproj/project.pbxproj b/Rosetta.xcodeproj/project.pbxproj index 51a165a..21571ad 100644 --- a/Rosetta.xcodeproj/project.pbxproj +++ b/Rosetta.xcodeproj/project.pbxproj @@ -264,7 +264,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = QN8Z263QGX; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -280,7 +280,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.6; + MARKETING_VERSION = 1.0.8; PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -302,7 +302,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = QN8Z263QGX; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -318,7 +318,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.6; + MARKETING_VERSION = 1.0.8; PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Rosetta.xcodeproj/xcuserdata/gaidartimirbaev.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Rosetta.xcodeproj/xcuserdata/gaidartimirbaev.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..1a43a02 --- /dev/null +++ b/Rosetta.xcodeproj/xcuserdata/gaidartimirbaev.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/Rosetta/Assets.xcassets/toolbar-add-chat.imageset/Contents.json b/Rosetta/Assets.xcassets/toolbar-add-chat.imageset/Contents.json new file mode 100644 index 0000000..8af6c98 --- /dev/null +++ b/Rosetta/Assets.xcassets/toolbar-add-chat.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "toolbar-add-chat.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Rosetta/Assets.xcassets/toolbar-add-chat.imageset/toolbar-add-chat.svg b/Rosetta/Assets.xcassets/toolbar-add-chat.imageset/toolbar-add-chat.svg new file mode 100644 index 0000000..83a0172 --- /dev/null +++ b/Rosetta/Assets.xcassets/toolbar-add-chat.imageset/toolbar-add-chat.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Rosetta/Assets.xcassets/toolbar-compose.imageset/Contents.json b/Rosetta/Assets.xcassets/toolbar-compose.imageset/Contents.json new file mode 100644 index 0000000..10ae537 --- /dev/null +++ b/Rosetta/Assets.xcassets/toolbar-compose.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "toolbar-compose.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Rosetta/Assets.xcassets/toolbar-compose.imageset/toolbar-compose.svg b/Rosetta/Assets.xcassets/toolbar-compose.imageset/toolbar-compose.svg new file mode 100644 index 0000000..b064f86 --- /dev/null +++ b/Rosetta/Assets.xcassets/toolbar-compose.imageset/toolbar-compose.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Rosetta/Assets.xcassets/toolbar-xmark.imageset/Contents.json b/Rosetta/Assets.xcassets/toolbar-xmark.imageset/Contents.json new file mode 100644 index 0000000..7140bfa --- /dev/null +++ b/Rosetta/Assets.xcassets/toolbar-xmark.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "toolbar-xmark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Rosetta/Assets.xcassets/toolbar-xmark.imageset/toolbar-xmark.svg b/Rosetta/Assets.xcassets/toolbar-xmark.imageset/toolbar-xmark.svg new file mode 100644 index 0000000..d8e3f9e --- /dev/null +++ b/Rosetta/Assets.xcassets/toolbar-xmark.imageset/toolbar-xmark.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Rosetta/DesignSystem/Components/GlassModifier.swift b/Rosetta/DesignSystem/Components/GlassModifier.swift index d5455da..5f27e25 100644 --- a/Rosetta/DesignSystem/Components/GlassModifier.swift +++ b/Rosetta/DesignSystem/Components/GlassModifier.swift @@ -52,4 +52,21 @@ extension View { } } } + + /// Glass circle — convenience for circular buttons. + @ViewBuilder + func glassCircle() -> some View { + if #available(iOS 26, *) { + background { + Circle().fill(.clear) + .glassEffect(.regular, in: .circle) + } + } else { + background { + Circle().fill(.thinMaterial) + .overlay { Circle().strokeBorder(Color.white.opacity(0.10), lineWidth: 0.5) } + .shadow(color: .black.opacity(0.10), radius: 16, y: 6) + } + } + } } diff --git a/Rosetta/Features/Chats/ChatDetail/ChatDetailView.swift b/Rosetta/Features/Chats/ChatDetail/ChatDetailView.swift index c6dd1c6..1b21c59 100644 --- a/Rosetta/Features/Chats/ChatDetail/ChatDetailView.swift +++ b/Rosetta/Features/Chats/ChatDetail/ChatDetailView.swift @@ -138,23 +138,22 @@ private extension ChatDetailView { @ToolbarContentBuilder var chatDetailToolbar: some ToolbarContent { ToolbarItem(placement: .navigationBarLeading) { - Button { dismiss() } label: { backCircleButtonLabel } - .frame(width: 36, height: 36) - .background { glass(shape: .circle, strokeOpacity: 0.22, strokeColor: .white) } + Button { dismiss() } label: { backCapsuleButtonLabel } + .buttonStyle(.plain) .accessibilityLabel("Back") } ToolbarItem(placement: .principal) { Button { dismiss() } label: { VStack(spacing: 1) { - HStack(spacing: 3) { + HStack(spacing: 4) { Text(titleText) .font(.system(size: 15, weight: .semibold)) .foregroundStyle(RosettaColors.Adaptive.text) .lineLimit(1) if !route.isSavedMessages && effectiveVerified > 0 { - VerifiedBadge(verified: effectiveVerified, size: 12) + VerifiedBadge(verified: effectiveVerified, size: 14) } } @@ -167,39 +166,43 @@ private extension ChatDetailView { ) .lineLimit(1) } - .padding(.horizontal, 12) + .padding(.horizontal, 16) .frame(height: 44) .background { glass(shape: .capsule, strokeOpacity: 0.22, strokeColor: .white) } } + .buttonStyle(.plain) } ToolbarItem(placement: .navigationBarTrailing) { AvatarView( initials: avatarInitials, colorIndex: avatarColorIndex, - size: 35, + size: 38, isOnline: false, isSavedMessages: route.isSavedMessages ) - .frame(width: 36, height: 36) + .frame(width: 44, height: 44) .background { glass(shape: .circle, strokeOpacity: 0.22, strokeColor: .white) } } } - private var backCircleButtonLabel: some View { - ZStack { - TelegramVectorIcon( - pathData: TelegramIconPath.backChevron, - viewBox: CGSize(width: 11, height: 20), - color: .white - ) - .frame(width: 11, height: 20) - .allowsHitTesting(false) + private var backCapsuleButtonLabel: some View { + TelegramVectorIcon( + pathData: TelegramIconPath.backChevron, + viewBox: CGSize(width: 11, height: 20), + color: .white + ) + .frame(width: 11, height: 20) + .allowsHitTesting(false) + .frame(width: 36, height: 36) + .frame(height: 44) + .padding(.horizontal, 4) + .background { + glass(shape: .capsule, strokeOpacity: 0.22, strokeColor: .white) } - .frame(width: 36, height: 36) // iOS hit-area } // MARK: - Existing helpers / UI diff --git a/Rosetta/Features/Chats/ChatList/ChatListSearchContent.swift b/Rosetta/Features/Chats/ChatList/ChatListSearchContent.swift index 0120715..6e5156b 100644 --- a/Rosetta/Features/Chats/ChatList/ChatListSearchContent.swift +++ b/Rosetta/Features/Chats/ChatList/ChatListSearchContent.swift @@ -96,11 +96,14 @@ private extension ChatListSearchContent { private extension ChatListSearchContent { @ViewBuilder var recentSearchesSection: some View { - if viewModel.recentSearches.isEmpty { - searchPlaceholder - } else { - ScrollView { - VStack(spacing: 0) { + ScrollView { + VStack(spacing: 0) { + // Horizontal scrollable favorite contacts row + FavoriteContactsRowSearch(onOpenDialog: onOpenDialog) + + if viewModel.recentSearches.isEmpty { + searchPlaceholderInline + } else { HStack { Text("RECENT") .font(.system(size: 13)) @@ -120,30 +123,33 @@ private extension ChatListSearchContent { recentRow(recent) } } + + Spacer().frame(height: 120) } - .scrollDismissesKeyboard(.immediately) } + .scrollDismissesKeyboard(.immediately) } - var searchPlaceholder: some View { - VStack(spacing: 20) { - Spacer() + var searchPlaceholderInline: some View { + VStack(spacing: 16) { LottieView( animationName: "search", animationSpeed: 1.0 ) .frame(width: 120, height: 120) + .padding(.top, 60) + Text("Search for users") - .font(.system(size: 17, weight: .medium)) - .foregroundStyle(RosettaColors.Adaptive.text) - Text("Find people by username or public key") - .font(.system(size: 14)) + .font(.system(size: 17, weight: .semibold)) .foregroundStyle(RosettaColors.Adaptive.textSecondary) + + Text("Find people by username or public key") + .font(.system(size: 15)) + .foregroundStyle(RosettaColors.Adaptive.textTertiary) .multilineTextAlignment(.center) .padding(.horizontal, 40) - Spacer() } - .frame(maxWidth: .infinity, maxHeight: .infinity) + .frame(maxWidth: .infinity) } func recentRow(_ user: RecentSearch) -> some View { @@ -157,22 +163,10 @@ private extension ChatListSearchContent { onSelectRecent(user.username.isEmpty ? user.publicKey : user.username) } label: { HStack(spacing: 10) { - ZStack(alignment: .topTrailing) { - AvatarView( - initials: initials, colorIndex: colorIdx, - size: 42, isSavedMessages: isSelf - ) - Button { - viewModel.removeRecentSearch(publicKey: user.publicKey) - } label: { - Image(systemName: "xmark") - .font(.system(size: 9, weight: .bold)) - .foregroundStyle(.white) - .frame(width: 18, height: 18) - .background(Circle().fill(RosettaColors.figmaBlue)) - } - .offset(x: 4, y: -4) - } + AvatarView( + initials: initials, colorIndex: colorIdx, + size: 42, isSavedMessages: isSelf + ) VStack(alignment: .leading, spacing: 1) { Text(isSelf ? "Saved Messages" : ( @@ -254,3 +248,48 @@ private extension ChatListSearchContent { .buttonStyle(.plain) } } + +// MARK: - Favorite Contacts Row (observation-isolated) + +/// Horizontal scrollable avatar row for active search state. +/// Isolated child view so that `DialogRepository.shared.sortedDialogs` observation +/// does NOT propagate to `ChatListSearchContent`'s parent. +private struct FavoriteContactsRowSearch: View { + var onOpenDialog: (ChatRoute) -> Void + + var body: some View { + let dialogs = DialogRepository.shared.sortedDialogs.prefix(10) + if !dialogs.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 4) { + ForEach(Array(dialogs), id: \.id) { dialog in + Button { + onOpenDialog(ChatRoute(dialog: dialog)) + } label: { + VStack(spacing: 4) { + AvatarView( + initials: dialog.initials, + colorIndex: dialog.avatarColorIndex, + size: 62, + isOnline: dialog.isOnline, + isSavedMessages: dialog.isSavedMessages + ) + + Text(dialog.isSavedMessages ? "Saved" : dialog.opponentTitle.components(separatedBy: " ").first ?? "") + .font(.system(size: 11)) + .tracking(0.06) + .foregroundStyle(RosettaColors.Adaptive.text) + .lineLimit(1) + .frame(width: 78) + } + .frame(width: 78) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal, 2) + } + .padding(.top, 12) + } + } +} diff --git a/Rosetta/Features/Chats/ChatList/ChatListView.swift b/Rosetta/Features/Chats/ChatList/ChatListView.swift index 49e31d9..e3f6d38 100644 --- a/Rosetta/Features/Chats/ChatList/ChatListView.swift +++ b/Rosetta/Features/Chats/ChatList/ChatListView.swift @@ -29,15 +29,19 @@ struct ChatListView: View { @StateObject private var viewModel = ChatListViewModel() @StateObject private var navigationState = ChatListNavigationState() @State private var searchText = "" + @FocusState private var isSearchFocused: Bool @MainActor static var _bodyCount = 0 var body: some View { let _ = Self._bodyCount += 1 let _ = print("🟡 ChatListView.body #\(Self._bodyCount)") NavigationStack(path: $navigationState.path) { - ZStack { - RosettaColors.Adaptive.background - .ignoresSafeArea() + VStack(spacing: 0) { + // Custom search bar + customSearchBar + .padding(.horizontal, 16) + .padding(.top, 6) + .padding(.bottom, 8) if isSearchActive { ChatListSearchContent( @@ -45,25 +49,26 @@ struct ChatListView: View { viewModel: viewModel, onSelectRecent: { searchText = $0 }, onOpenDialog: { route in - isSearchActive = false - searchText = "" navigationState.path.append(route) + // Delay search dismissal so NavigationStack processes + // the push before the search overlay is removed. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + isSearchActive = false + isSearchFocused = false + searchText = "" + viewModel.setSearchQuery("") + } } ) } else { normalContent } } + .background(RosettaColors.Adaptive.background.ignoresSafeArea()) .navigationBarTitleDisplayMode(.inline) + .toolbar(isSearchActive ? .hidden : .visible, for: .navigationBar) .toolbar { toolbarContent } - .toolbarBackground(.visible, for: .navigationBar) - .applyGlassNavBar() - .searchable( - text: $searchText, - isPresented: $isSearchActive, - placement: .navigationBarDrawer(displayMode: .always), - prompt: "Search" - ) + .toolbarBackground(.hidden, for: .navigationBar) .onChange(of: searchText) { _, newValue in viewModel.setSearchQuery(newValue) } @@ -84,6 +89,124 @@ struct ChatListView: View { } .tint(RosettaColors.figmaBlue) } + + // MARK: - Cancel Search + + private func cancelSearch() { + isSearchActive = false + isSearchFocused = false + searchText = "" + viewModel.setSearchQuery("") + } +} + +// MARK: - Custom Search Bar + +private extension ChatListView { + var customSearchBar: some View { + HStack(spacing: 10) { + // Search bar capsule + ZStack { + // Centered placeholder: magnifier + "Search" + if searchText.isEmpty && !isSearchActive { + HStack(spacing: 6) { + Image(systemName: "magnifyingglass") + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(Color.gray) + Text("Search") + .font(.system(size: 17)) + .foregroundStyle(Color.gray) + } + .allowsHitTesting(false) + } + + // Active: left-aligned magnifier + TextField + if isSearchActive { + HStack(spacing: 6) { + Image(systemName: "magnifyingglass") + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(Color.gray) + + TextField("Search", text: $searchText) + .font(.system(size: 17)) + .foregroundStyle(RosettaColors.Adaptive.text) + .focused($isSearchFocused) + .submitLabel(.search) + + if !searchText.isEmpty { + Button { + searchText = "" + } label: { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 15)) + .foregroundStyle(Color.gray) + } + } + } + .padding(.horizontal, 12) + } + } + .frame(height: 42) + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + if !isSearchActive { + withAnimation(.easeInOut(duration: 0.25)) { + isSearchActive = true + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + isSearchFocused = true + } + } + } + .background { + if isSearchActive { + RoundedRectangle(cornerRadius: 24, style: .continuous) + .fill(Color.white.opacity(0.08)) + .overlay { + RoundedRectangle(cornerRadius: 24, style: .continuous) + .strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5) + } + } else { + RoundedRectangle(cornerRadius: 24, style: .continuous) + .fill(RosettaColors.Adaptive.backgroundSecondary) + } + } + .onChange(of: isSearchFocused) { _, focused in + if focused && !isSearchActive { + withAnimation(.easeInOut(duration: 0.25)) { + isSearchActive = true + } + } + } + + // Circular X button (visible only when search is active) + if isSearchActive { + Button { + cancelSearch() + } label: { + Image("toolbar-xmark") + .renderingMode(.template) + .resizable() + .scaledToFit() + .frame(width: 19, height: 19) + .foregroundStyle(.white) + .frame(width: 36, height: 36) + .padding(3) + } + .buttonStyle(.plain) + .background { + Circle() + .fill(Color.white.opacity(0.08)) + .overlay { + Circle() + .strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5) + } + } + .transition(.opacity.combined(with: .scale(scale: 0.5))) + } + } + } } // MARK: - Normal Content @@ -111,40 +234,56 @@ private extension ChatListView { private extension ChatListView { @ToolbarContentBuilder var toolbarContent: some ToolbarContent { - ToolbarItem(placement: .navigationBarLeading) { - Button { } label: { - Text("Edit") - .font(.system(size: 17, weight: .medium)) - .foregroundStyle(RosettaColors.Adaptive.text) - } - } - - ToolbarItem(placement: .principal) { - HStack(spacing: 4) { - // Isolated view — reads AccountManager & SessionManager (@Observable) - // without polluting ChatListView's observation scope. - ToolbarStoriesAvatar() - Text("Chats") - .font(.system(size: 17, weight: .semibold)) - .foregroundStyle(RosettaColors.Adaptive.text) - } - } - - ToolbarItemGroup(placement: .navigationBarTrailing) { - HStack(spacing: 8) { + if !isSearchActive { + ToolbarItem(placement: .navigationBarLeading) { Button { } label: { - Image(systemName: "camera") - .font(.system(size: 16, weight: .regular)) + Text("Edit") + .font(.system(size: 17, weight: .medium)) + .foregroundStyle(RosettaColors.Adaptive.text) + .frame(height: 44) + .padding(.horizontal, 10) + } + .buttonStyle(.plain) + .glassCapsule() + } + + ToolbarItem(placement: .principal) { + HStack(spacing: 4) { + // Isolated view — reads AccountManager & SessionManager (@Observable) + // without polluting ChatListView's observation scope. + ToolbarStoriesAvatar() + Text("Chats") + .font(.system(size: 17, weight: .semibold)) .foregroundStyle(RosettaColors.Adaptive.text) } - .accessibilityLabel("Camera") - Button { } label: { - Image(systemName: "square.and.pencil") - .font(.system(size: 17, weight: .regular)) - .foregroundStyle(RosettaColors.Adaptive.text) + } + + ToolbarItem(placement: .navigationBarTrailing) { + HStack(spacing: 0) { + Button { } label: { + Image("toolbar-add-chat") + .renderingMode(.template) + .resizable() + .scaledToFit() + .frame(width: 22, height: 22) + .frame(width: 44, height: 44) + } + .buttonStyle(.plain) + .accessibilityLabel("Add chat") + + Button { } label: { + Image("toolbar-compose") + .renderingMode(.template) + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .frame(width: 44, height: 44) + } + .buttonStyle(.plain) + .accessibilityLabel("New chat") } - .padding(.bottom, 2) - .accessibilityLabel("New chat") + .foregroundStyle(RosettaColors.Adaptive.text) + .glassCapsule() } } } @@ -225,13 +364,13 @@ private struct ChatListDialogContent: View { } } else { if !viewModel.pinnedDialogs.isEmpty { - ForEach(viewModel.pinnedDialogs) { dialog in - chatRow(dialog) + ForEach(Array(viewModel.pinnedDialogs.enumerated()), id: \.element.id) { index, dialog in + chatRow(dialog, isFirst: index == 0) .listRowBackground(RosettaColors.Adaptive.backgroundSecondary) } } - ForEach(viewModel.unpinnedDialogs) { dialog in - chatRow(dialog) + ForEach(Array(viewModel.unpinnedDialogs.enumerated()), id: \.element.id) { index, dialog in + chatRow(dialog, isFirst: index == 0 && viewModel.pinnedDialogs.isEmpty) } } @@ -245,7 +384,7 @@ private struct ChatListDialogContent: View { .scrollDismissesKeyboard(.immediately) } - private func chatRow(_ dialog: Dialog) -> some View { + private func chatRow(_ dialog: Dialog, isFirst: Bool = false) -> some View { Button { navigationState.path.append(ChatRoute(dialog: dialog)) } label: { @@ -253,7 +392,8 @@ private struct ChatListDialogContent: View { } .buttonStyle(.plain) .listRowInsets(EdgeInsets()) - .listRowSeparator(.visible) + .listRowSeparator(isFirst ? .hidden : .visible, edges: .top) + .listRowSeparator(.visible, edges: .bottom) .listRowSeparatorTint(RosettaColors.Adaptive.divider) .alignmentGuide(.listRowSeparatorLeading) { _ in 82 } .swipeActions(edge: .trailing, allowsFullSwipe: false) { @@ -368,4 +508,12 @@ private struct DeviceApprovalBanner: View { } } -#Preview { ChatListView(isSearchActive: .constant(false), isDetailPresented: .constant(false)) } +#Preview("Chat List") { + ChatListView(isSearchActive: .constant(false), isDetailPresented: .constant(false)) + .preferredColorScheme(.dark) +} + +#Preview("Search Active") { + ChatListView(isSearchActive: .constant(true), isDetailPresented: .constant(false)) + .preferredColorScheme(.dark) +} diff --git a/Rosetta/Features/Chats/Search/SearchView.swift b/Rosetta/Features/Chats/Search/SearchView.swift index a7b8948..86cce7e 100644 --- a/Rosetta/Features/Chats/Search/SearchView.swift +++ b/Rosetta/Features/Chats/Search/SearchView.swift @@ -255,25 +255,12 @@ private struct RecentSection: View { navigationPath.append(ChatRoute(recent: user)) } label: { HStack(spacing: 12) { - ZStack(alignment: .topTrailing) { - AvatarView( - initials: initials, - colorIndex: colorIdx, - size: 42, - isSavedMessages: isSelf - ) - - Button { - viewModel.removeRecentSearch(publicKey: user.publicKey) - } label: { - Image(systemName: "xmark") - .font(.system(size: 9, weight: .bold)) - .foregroundStyle(.white) - .frame(width: 18, height: 18) - .background(Circle().fill(RosettaColors.figmaBlue)) - } - .offset(x: 4, y: -4) - } + AvatarView( + initials: initials, + colorIndex: colorIdx, + size: 42, + isSavedMessages: isSelf + ) VStack(alignment: .leading, spacing: 1) { Text(isSelf ? "Saved Messages" : (user.title.isEmpty ? String(user.publicKey.prefix(16)) + "..." : user.title)) diff --git a/docs/blur-materials.md b/docs/blur-materials.md new file mode 100644 index 0000000..dfc0662 --- /dev/null +++ b/docs/blur-materials.md @@ -0,0 +1,92 @@ +# iOS Blur Materials Reference (from Figma) + +All materials use `backdrop-blur: 50px` (equivalent to `UIBlurEffect` system radius). + +--- + +## Dark Mode Materials + +### Ultrathin — Dark +Most transparent. Use for subtle overlays, nav bar pills. + +| Layer | Property | +|-------|----------| +| Base fill | `rgba(255, 255, 255, 0.07)` | +| Blur layer | `backdrop-blur: 50px`, `rgba(255, 255, 255, 0.03)`, blend: `color-dodge` | + +SwiftUI equivalent: `.ultraThinMaterial` (note: adds slight grey chromatic tint on pure black) + +### Thin — Dark +Slight blur. Use for search bars, secondary panels. + +| Layer | Property | +|-------|----------| +| Base fill | `rgba(255, 255, 255, 0.05)` | +| Blur layer | `backdrop-blur: 50px`, `rgba(255, 255, 255, 0.40)`, blend: `color-dodge` | + +SwiftUI equivalent: `.thinMaterial` + +### Regular (Default) — Dark +Medium blur. Use for tab bars, cards, input bars. + +| Layer | Property | +|-------|----------| +| Base fill | `rgba(255, 255, 255, 0.25)`, blend: `plus-lighter` | +| Blur layer | `backdrop-blur: 50px`, `rgba(255, 255, 255, 0.60)`, blend: `color-dodge` | + +SwiftUI equivalent: `.regularMaterial` + +### Thick — Dark +Heavy, opaque blur. Use for modal overlays, sheets. + +| Layer | Property | +|-------|----------| +| Fill | `rgba(0, 0, 0, 0.60)` | +| Blur | `backdrop-blur: 50px` | + +SwiftUI equivalent: `.thickMaterial` + +--- + +## Dark Mode Vibrant Colors (on material backgrounds) + +### Labels (text over material) +| Token | Hex | Usage | +|-------|-----|-------| +| Vibrant Primary | `#FFFFFF` | Main text | +| Vibrant Secondary | `#999999` | Subtitle, caption | +| Vibrant Tertiary | `#404040` | Hint, placeholder | +| Vibrant Quaternary | `#262626` | Disabled text | + +### Fills (elements over material) +| Token | Hex | Usage | +|-------|-----|-------| +| Vibrant Primary | `#333333` | Main fill | +| Vibrant Secondary | `#121212` | Secondary fill | +| Vibrant Tertiary | `#121212` | Tertiary fill | + +### Separator +| Token | Hex | +|-------|-----| +| Separator | `#1A1A1A` | + +--- + +## Dark Theme — Practical Gotchas + +1. **`.thinMaterial` / `.ultraThinMaterial` look grey on dark backgrounds** — they add a light chromatic tint. For elements on pure black, use `Color.white.opacity(0.08)` or the Figma "Ultrathin" recipe: `rgba(255,255,255,0.07)` base fill. + +2. **Telegram-style dark fades** are `LinearGradient` with `Color.black` opacity stops, NOT material blurs. + +3. **`UIVisualEffectView` cannot blur SwiftUI content** — different rendering layers. + +--- + +## iOS Version Mapping + +| Figma Material | iOS < 26 (SwiftUI) | iOS 26+ | +|----------------|---------------------|---------| +| Ultrathin | `.ultraThinMaterial` or `Color.white.opacity(0.07)` | `.glassEffect(.ultraThin)` | +| Thin | `.thinMaterial` or `Color.white.opacity(0.05)` | `.glassEffect(.thin)` | +| Regular | `.regularMaterial` | `.glassEffect(.regular)` | +| Thick | `.thickMaterial` or `Color.black.opacity(0.6)` | `.glassEffect(.thick)` | diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3207db4..6deb38a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -65,16 +65,16 @@ platform :ios do xcargs: "SWIFT_OPTIMIZATION_LEVEL='-Onone'" ) - # Инкремент только после успешной сборки + upload_to_testflight( + skip_waiting_for_build_processing: true + ) + + # Инкремент только после успешной сборки И загрузки new_version = bump_patch(current_marketing_version) set_marketing_version(new_version) new_build = current_build_number + 1 set_build_number(new_build) - - upload_to_testflight( - skip_waiting_for_build_processing: true - ) end # ─── Release в App Store ─── @@ -89,16 +89,16 @@ platform :ios do xcargs: "SWIFT_OPTIMIZATION_LEVEL='-Onone'" ) + upload_to_app_store( + force: true, + skip_screenshots: true + ) + new_version = bump_patch(current_marketing_version) set_marketing_version(new_version) new_build = current_build_number + 1 set_build_number(new_build) - - upload_to_app_store( - force: true, - skip_screenshots: true - ) end end