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