diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index 26749ed3ce..8c9112a858 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -28,15 +28,17 @@ struct ChatInfoToolbar: View { color: Color(uiColor: .tertiaryLabel) ) .padding(.trailing, 4) - VStack { - let t = Text(cInfo.displayName).font(.headline) - (cInfo.contact?.verified == true ? contactVerifiedShield + t : t) - .lineLimit(1) - if cInfo.fullName != "" && cInfo.displayName != cInfo.fullName { - Text(cInfo.fullName).font(.subheadline) - .lineLimit(1) + let t = Text(cInfo.displayName).font(.headline) + (cInfo.contact?.verified == true ? contactVerifiedShield + t : t) + .lineLimit(1) + .if (cInfo.fullName != "" && cInfo.displayName != cInfo.fullName) { v in + VStack(spacing: 0) { + v + Text(cInfo.fullName).font(.subheadline) + .lineLimit(1) + .padding(.top, -2) + } } - } } .foregroundColor(theme.colors.onBackground) .frame(width: 220) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index d3f5242cac..1f3c04085b 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -45,9 +45,14 @@ struct ChatView: View { var body: some View { if #available(iOS 16.0, *) { - viewBody + let v = viewBody .scrollDismissesKeyboard(.immediately) .keyboardPadding() + if (searchMode) { + v.toolbarBackground(.thinMaterial, for: .navigationBar) + } else { + v.toolbarBackground(.visible, for: .navigationBar) + } } else { viewBody } @@ -81,7 +86,6 @@ struct ChatView: View { ) .disabled(!cInfo.sendMsgEnabled) } - .padding(.top, 1) .navigationTitle(cInfo.chatViewName) .background(theme.colors.background) .navigationBarTitleDisplayMode(.inline) @@ -310,8 +314,9 @@ struct ChatView: View { } .padding(.horizontal) .padding(.vertical, 8) + .background(.thinMaterial) } - + private func voiceWithoutFrame(_ ci: ChatItem) -> Bool { ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil && ci.meta.itemForwarded == nil } @@ -702,19 +707,19 @@ struct ChatView: View { chatItemWithMenu(ci, range, maxWidth) } } - .padding(.top, 5) + .padding(.bottom, 5) .padding(.trailing) .padding(.leading, 12) } else { chatItemWithMenu(ci, range, maxWidth) - .padding(.top, 5) + .padding(.bottom, 5) .padding(.trailing) .padding(.leading, memberImageSize + 8 + 12) } } else { chatItemWithMenu(ci, range, maxWidth) .padding(.horizontal) - .padding(.top, 5) + .padding(.bottom, 5) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift index 488fe0a65d..1ec46816f5 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift @@ -32,9 +32,8 @@ struct ComposeFileView: View { } .padding(.vertical, 1) .padding(.trailing, 12) - .frame(height: 50) + .frame(height: 54) .background(theme.appColors.sentMessage) .frame(maxWidth: .infinity) - .padding(.top, 8) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift index d9f63f6d33..df3a8caf55 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift @@ -46,8 +46,8 @@ struct ComposeImageView: View { .padding(.vertical, 1) .padding(.trailing, 12) .background(theme.appColors.sentMessage) + .frame(minHeight: 54) .frame(maxWidth: .infinity) - .padding(.top, 8) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift index 7df0a3037a..0fb48033d5 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift @@ -63,8 +63,8 @@ struct ComposeLinkView: View { .padding(.vertical, 1) .padding(.trailing, 12) .background(theme.appColors.sentMessage) + .frame(minHeight: 54) .frame(maxWidth: .infinity) - .padding(.top, 8) } private func linkPreviewView(_ linkPreview: LinkPreview) -> some View { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index c1b709fea5..e19672c883 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -284,8 +284,10 @@ struct ComposeView: View { var body: some View { VStack(spacing: 0) { + Divider() if chat.chatInfo.contact?.nextSendGrpInv ?? false { ContextInvitingContactMemberView() + Divider() } // preference checks should match checks in forwarding list let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks) @@ -293,10 +295,13 @@ struct ComposeView: View { let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice) if simplexLinkProhibited { msgNotAllowedView("SimpleX links not allowed", icon: "link") + Divider() } else if fileProhibited { msgNotAllowedView("Files and media not allowed", icon: "doc") + Divider() } else if voiceProhibited { msgNotAllowedView("Voice messages not allowed", icon: "mic") + Divider() } contextItemView() switch (composeState.editing, composeState.preview) { @@ -359,7 +364,6 @@ struct ComposeView: View { : theme.colors.primary ) .padding(.trailing, 12) - .background(theme.colors.background) .disabled(!chat.userCanSend) if chat.userIsObserver { @@ -377,6 +381,7 @@ struct ComposeView: View { } } } + .background(.thinMaterial) .onChange(of: composeState.message) { msg in if composeState.linkPreviewAllowed { if msg.count > 0 { @@ -625,6 +630,7 @@ struct ComposeView: View { cancelPreview: cancelLinkPreview, cancelEnabled: !composeState.inProgress ) + Divider() case let .mediaPreviews(mediaPreviews: media): ComposeImageView( images: media.map { (img, _) in img }, @@ -633,6 +639,7 @@ struct ComposeView: View { chosenMedia = [] }, cancelEnabled: !composeState.editing && !composeState.inProgress) + Divider() case let .voicePreview(recordingFileName, _): ComposeVoiceView( recordingFileName: recordingFileName, @@ -645,6 +652,7 @@ struct ComposeView: View { cancelEnabled: !composeState.editing && !composeState.inProgress, stopPlayback: $stopPlayback ) + Divider() case let .filePreview(fileName, _): ComposeFileView( fileName: fileName, @@ -652,6 +660,7 @@ struct ComposeView: View { composeState = composeState.copy(preview: .noPreview) }, cancelEnabled: !composeState.editing && !composeState.inProgress) + Divider() } } @@ -661,10 +670,9 @@ struct ComposeView: View { Text(reason).italic() } .padding(12) - .frame(minHeight: 50) + .frame(minHeight: 54) .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .padding(.top, 8) + .background(.thinMaterial) } @ViewBuilder private func contextItemView() -> some View { @@ -678,6 +686,7 @@ struct ComposeView: View { contextIcon: "arrowshape.turn.up.left", cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) } ) + Divider() case let .editingItem(chatItem: editingItem): ContextItemView( chat: chat, @@ -685,6 +694,7 @@ struct ComposeView: View { contextIcon: "pencil", cancelContextItem: { clearState() } ) + Divider() case let .forwardingItem(chatItem: forwardedItem, _): ContextItemView( chat: chat, @@ -693,6 +703,7 @@ struct ComposeView: View { cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }, showSender: false ) + Divider() } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift index 4b813d35cb..441a68fccb 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift @@ -51,8 +51,8 @@ struct ComposeVoiceView: View { .padding(.vertical, 1) .frame(height: ComposeVoiceView.previewHeight) .background(theme.appColors.sentMessage) + .frame(minHeight: 54) .frame(maxWidth: .infinity) - .padding(.top, 8) } private func recordingMode() -> some View { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift index cd6f5961cd..82090f312a 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift @@ -18,10 +18,9 @@ struct ContextInvitingContactMemberView: View { Text("Send direct message to connect") } .padding(12) - .frame(minHeight: 50) + .frame(minHeight: 54) .frame(maxWidth: .infinity, alignment: .leading) - .background(theme.appColors.sentMessage) - .padding(.top, 8) + .background(.thinMaterial) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index 3c89cbeb85..6245bbe21f 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -43,10 +43,9 @@ struct ContextItemView: View { .tint(theme.colors.primary) } .padding(12) - .frame(minHeight: 50) + .frame(minHeight: 54) .frame(maxWidth: .infinity) .background(chatItemFrameColor(contextItem, theme)) - .padding(.top, 8) } private func msgContentView(lines: Int) -> some View { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index a52cc7f71a..a720c3aaaf 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -44,6 +44,7 @@ struct SendMessageView: View { var body: some View { ZStack { + let composeShape = RoundedRectangle(cornerSize: CGSize(width: 20, height: 20)) HStack(alignment: .bottom) { ZStack(alignment: .leading) { if case .voicePreview = composeState.preview { @@ -84,10 +85,9 @@ struct SendMessageView: View { } } .padding(.vertical, 1) - .overlay( - RoundedRectangle(cornerSize: CGSize(width: 20, height: 20)) - .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true) - ) + .background(theme.colors.background) + .clipShape(composeShape) + .overlay(composeShape.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)) } .onChange(of: composeState.message, perform: { text in updateFont(text) }) .onChange(of: composeState.inProgress) { inProgress in diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index c04ac6ead9..3e4c3c9f6e 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -76,155 +76,153 @@ struct GroupMemberInfoView: View { private func groupMemberInfoView() -> some View { ZStack { - VStack { - let member = groupMember.wrapped - List { - groupMemberInfoHeader(member) - .listRowBackground(Color.clear) + let member = groupMember.wrapped + List { + groupMemberInfoHeader(member) + .listRowBackground(Color.clear) - if member.memberActive { - Section { - if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) { - knownDirectChatButton(chat) - } else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { - if let contactId = member.memberContactId { - newDirectChatButton(contactId) - } else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false { - createMemberContactButton() - } - } - if let code = connectionCode { verifyCodeButton(code) } - if let connStats = connectionStats, - connStats.ratchetSyncAllowed { - synchronizeConnectionButton() - } - // } else if developerTools { - // synchronizeConnectionButtonForce() - // } - } - } - - if let contactLink = member.contactLink { - Section { - SimpleXLinkQRCode(uri: contactLink) - Button { - showShareSheet(items: [simplexChatLink(contactLink)]) - } label: { - Label("Share address", systemImage: "square.and.arrow.up") - } + if member.memberActive { + Section { + if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) { + knownDirectChatButton(chat) + } else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { if let contactId = member.memberContactId { - if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { - connectViaAddressButton(contactLink) - } - } else { + newDirectChatButton(contactId) + } else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false { + createMemberContactButton() + } + } + if let code = connectionCode { verifyCodeButton(code) } + if let connStats = connectionStats, + connStats.ratchetSyncAllowed { + synchronizeConnectionButton() + } + // } else if developerTools { + // synchronizeConnectionButtonForce() + // } + } + } + + if let contactLink = member.contactLink { + Section { + SimpleXLinkQRCode(uri: contactLink) + Button { + showShareSheet(items: [simplexChatLink(contactLink)]) + } label: { + Label("Share address", systemImage: "square.and.arrow.up") + } + if let contactId = member.memberContactId { + if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { connectViaAddressButton(contactLink) } - } header: { - Text("Address") - .foregroundColor(theme.colors.secondary) - } footer: { - Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.") - .foregroundColor(theme.colors.secondary) - } - } - - Section(header: Text("Member").foregroundColor(theme.colors.secondary)) { - infoRow("Group", groupInfo.displayName) - - if let roles = member.canChangeRoleTo(groupInfo: groupInfo) { - Picker("Change role", selection: $newRole) { - ForEach(roles) { role in - Text(role.text) - } - } - .frame(height: 36) } else { - infoRow("Role", member.memberRole.text) + connectViaAddressButton(contactLink) } + } header: { + Text("Address") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.") + .foregroundColor(theme.colors.secondary) } + } - if let connStats = connectionStats { - Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) { - // TODO network connection status - Button("Change receiving address") { - alert = .switchAddressAlert + Section(header: Text("Member").foregroundColor(theme.colors.secondary)) { + infoRow("Group", groupInfo.displayName) + + if let roles = member.canChangeRoleTo(groupInfo: groupInfo) { + Picker("Change role", selection: $newRole) { + ForEach(roles) { role in + Text(role.text) + } + } + .frame(height: 36) + } else { + infoRow("Role", member.memberRole.text) + } + } + + if let connStats = connectionStats { + Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) { + // TODO network connection status + Button("Change receiving address") { + alert = .switchAddressAlert + } + .disabled( + connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } + || connStats.ratchetSyncSendProhibited + ) + if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) { + Button("Abort changing address") { + alert = .abortSwitchAddressAlert } .disabled( - connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } + connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch } || connStats.ratchetSyncSendProhibited ) - if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) { - Button("Abort changing address") { - alert = .abortSwitchAddressAlert - } - .disabled( - connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch } - || connStats.ratchetSyncSendProhibited - ) - } - smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary) - smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary) } + smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary) + smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary) } + } - if groupInfo.membership.memberRole >= .admin { - adminDestructiveSection(member) - } else { - nonAdminBlockSection(member) - } + if groupInfo.membership.memberRole >= .admin { + adminDestructiveSection(member) + } else { + nonAdminBlockSection(member) + } - if developerTools { - Section(header: Text("For console").foregroundColor(theme.colors.secondary)) { - infoRow("Local name", member.localDisplayName) - infoRow("Database ID", "\(member.groupMemberId)") - if let conn = member.activeConn { - let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel) - infoRow("Connection", connLevelDesc) - } - Button ("Debug delivery") { - Task { - do { - let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId)) - await MainActor.run { alert = .queueInfo(info: info) } - } catch let e { - logger.error("apiContactQueueInfo error: \(responseError(e))") - let a = getErrorAlert(e, "Error") - await MainActor.run { alert = .error(title: a.title, error: a.message) } - } + if developerTools { + Section(header: Text("For console").foregroundColor(theme.colors.secondary)) { + infoRow("Local name", member.localDisplayName) + infoRow("Database ID", "\(member.groupMemberId)") + if let conn = member.activeConn { + let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel) + infoRow("Connection", connLevelDesc) + } + Button ("Debug delivery") { + Task { + do { + let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId)) + await MainActor.run { alert = .queueInfo(info: info) } + } catch let e { + logger.error("apiContactQueueInfo error: \(responseError(e))") + let a = getErrorAlert(e, "Error") + await MainActor.run { alert = .error(title: a.title, error: a.message) } } } } } } - .navigationBarHidden(true) - .onAppear { - if #unavailable(iOS 16) { - // this condition prevents re-setting picker - if !justOpened { return } - } - justOpened = false - DispatchQueue.main.async { - newRole = member.memberRole - do { - let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) - let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil) - _ = chatModel.upsertGroupMember(groupInfo, mem) - connectionStats = stats - connectionCode = code - } catch let error { - logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))") - } + } + .navigationBarHidden(true) + .onAppear { + if #unavailable(iOS 16) { + // this condition prevents re-setting picker + if !justOpened { return } + } + justOpened = false + DispatchQueue.main.async { + newRole = member.memberRole + do { + let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) + let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil) + _ = chatModel.upsertGroupMember(groupInfo, mem) + connectionStats = stats + connectionCode = code + } catch let error { + logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))") } } - .onChange(of: newRole) { newRole in - if newRole != member.memberRole { - alert = .changeMemberRoleAlert(mem: member, role: newRole) - } - } - .onChange(of: member.memberRole) { role in - newRole = role + } + .onChange(of: newRole) { newRole in + if newRole != member.memberRole { + alert = .changeMemberRoleAlert(mem: member, role: newRole) } } + .onChange(of: member.memberRole) { role in + newRole = role + } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .alert(item: $alert) { alertItem in switch(alertItem) {