mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-03 02:30:19 +00:00
ios: improve chat list layout (#4528)
This commit is contained in:
@@ -14,10 +14,10 @@ struct CIFileView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let file: CIFile?
|
||||
let edited: Bool
|
||||
var smallView: Bool = false
|
||||
var smallViewSize: CGFloat?
|
||||
|
||||
var body: some View {
|
||||
if smallView {
|
||||
if smallViewSize != nil {
|
||||
fileIndicator()
|
||||
.onTapGesture(perform: fileAction)
|
||||
} else {
|
||||
@@ -201,21 +201,22 @@ struct CIFileView: View {
|
||||
}
|
||||
|
||||
private func fileIcon(_ icon: String, color: Color = Color(uiColor: .tertiaryLabel), innerIcon: String? = nil, innerIconSize: CGFloat? = nil) -> some View {
|
||||
ZStack(alignment: .center) {
|
||||
let size = smallViewSize ?? 30
|
||||
return ZStack(alignment: .center) {
|
||||
Image(systemName: icon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: smallView ? 36 : 30, height: smallView ? 36 : 30)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(color)
|
||||
if let innerIcon = innerIcon,
|
||||
let innerIconSize = innerIconSize, (!smallView || file?.showStatusIconInSmallView == true) {
|
||||
let innerIconSize = innerIconSize, (smallViewSize == nil || file?.showStatusIconInSmallView == true) {
|
||||
Image(systemName: innerIcon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxHeight: 16)
|
||||
.frame(width: innerIconSize, height: innerIconSize)
|
||||
.foregroundColor(.white)
|
||||
.padding(.top, smallView ? 15 : 12)
|
||||
.padding(.top, size / 2.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ struct CIVoiceView: View {
|
||||
@State var playbackTime: TimeInterval? = nil
|
||||
|
||||
@Binding var allowMenu: Bool
|
||||
var smallView: Bool = false
|
||||
var smallViewSize: CGFloat?
|
||||
@State private var seek: (TimeInterval) -> Void = { _ in }
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if smallView {
|
||||
if smallViewSize != nil {
|
||||
HStack(spacing: 10) {
|
||||
player()
|
||||
playerTime()
|
||||
@@ -65,7 +65,12 @@ struct CIVoiceView: View {
|
||||
}
|
||||
|
||||
private func player() -> some View {
|
||||
VoiceMessagePlayer(
|
||||
let sizeMultiplier: CGFloat = if let sz = smallViewSize {
|
||||
voiceMessageSizeBasedOnSquareSize(sz) / 56
|
||||
} else {
|
||||
1
|
||||
}
|
||||
return VoiceMessagePlayer(
|
||||
chat: chat,
|
||||
chatItem: chatItem,
|
||||
recordingFile: recordingFile,
|
||||
@@ -76,7 +81,7 @@ struct CIVoiceView: View {
|
||||
playbackState: $playbackState,
|
||||
playbackTime: $playbackTime,
|
||||
allowMenu: $allowMenu,
|
||||
sizeMultiplier: smallView ? voiceMessageSizeBasedOnSquareSize(36) / 56 : 1
|
||||
sizeMultiplier: sizeMultiplier
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,25 +9,34 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
private let rowHeights: [DynamicTypeSize: CGFloat] = [
|
||||
.xSmall: 68,
|
||||
.small: 72,
|
||||
.medium: 76,
|
||||
.large: 80,
|
||||
.xLarge: 88,
|
||||
.xxLarge: 94,
|
||||
.xxxLarge: 104,
|
||||
.accessibility1: 90,
|
||||
.accessibility2: 100,
|
||||
.accessibility3: 120,
|
||||
.accessibility4: 130,
|
||||
.accessibility5: 140
|
||||
let dynamicSizes: [
|
||||
DynamicTypeSize: (
|
||||
rowHeight: CGFloat,
|
||||
profileImageSize: CGFloat,
|
||||
mediaSize: CGFloat,
|
||||
chatInfoSize: CGFloat,
|
||||
unreadCorner: CGFloat,
|
||||
unreadPadding: CGFloat
|
||||
)
|
||||
] = [
|
||||
.xSmall: (68, 55, 33, 18, 9, 3),
|
||||
.small: (72, 57, 34, 18, 9, 3),
|
||||
.medium: (76, 60, 36, 18, 10, 4),
|
||||
.large: (80, 63, 38, 20, 10, 4),
|
||||
.xLarge: (88, 67, 41, 20, 10, 4),
|
||||
.xxLarge: (94, 71, 44, 22, 11, 4),
|
||||
.xxxLarge: (104, 75, 48, 24, 12, 5),
|
||||
.accessibility1: (104, 75, 48, 24, 12, 5),
|
||||
.accessibility2: (114, 75, 48, 24, 12, 5),
|
||||
.accessibility3: (124, 75, 48, 24, 12, 5),
|
||||
.accessibility4: (134, 75, 48, 24, 12, 5),
|
||||
.accessibility5: (144, 75, 48, 24, 12, 5)
|
||||
]
|
||||
|
||||
struct ChatListNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||
@ObservedObject var chat: Chat
|
||||
@State private var showContactRequestDialog = false
|
||||
@State private var showJoinGroupDialog = false
|
||||
@@ -38,6 +47,8 @@ struct ChatListNavLink: View {
|
||||
@State private var inProgress = false
|
||||
@State private var progressByTimeout = false
|
||||
|
||||
var dynamicRowHeight: CGFloat { dynamicSizes[userFont]?.rowHeight ?? 80 }
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch chat.chatInfo {
|
||||
@@ -70,7 +81,7 @@ struct ChatListNavLink: View {
|
||||
Group {
|
||||
if contact.activeConn == nil && contact.profile.contactLink != nil {
|
||||
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false))
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button {
|
||||
showDeleteContactActionSheet = true
|
||||
@@ -110,7 +121,7 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
}
|
||||
}
|
||||
.actionSheet(isPresented: $showDeleteContactActionSheet) {
|
||||
@@ -139,7 +150,7 @@ struct ChatListNavLink: View {
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memInvited:
|
||||
ChatPreviewView(chat: chat, progressByTimeout: $progressByTimeout)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
joinGroupButton()
|
||||
if groupInfo.canDelete {
|
||||
@@ -159,7 +170,7 @@ struct ChatListNavLink: View {
|
||||
.disabled(inProgress)
|
||||
case .memAccepted:
|
||||
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false))
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlert(groupInvitationAcceptedAlert())
|
||||
}
|
||||
@@ -178,7 +189,7 @@ struct ChatListNavLink: View {
|
||||
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) },
|
||||
disabled: !groupInfo.ready
|
||||
)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
markReadButton()
|
||||
toggleFavoriteButton()
|
||||
@@ -205,7 +216,7 @@ struct ChatListNavLink: View {
|
||||
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) },
|
||||
disabled: !noteFolder.ready
|
||||
)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
markReadButton()
|
||||
}
|
||||
@@ -321,7 +332,7 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.onTapGesture { showContactRequestDialog = true }
|
||||
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
|
||||
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } }
|
||||
@@ -349,7 +360,7 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.appSheet(isPresented: $showContactConnectionInfo) {
|
||||
Group {
|
||||
if case let .contactConnection(contactConnection) = chat.chatInfo {
|
||||
@@ -469,7 +480,7 @@ struct ChatListNavLink: View {
|
||||
Text("invalid chat data")
|
||||
.foregroundColor(.red)
|
||||
.padding(4)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.frame(height: dynamicRowHeight)
|
||||
.onTapGesture { showInvalidJSON = true }
|
||||
.appSheet(isPresented: $showInvalidJSON) {
|
||||
invalidJSONView(json)
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct ChatPreviewView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var progressByTimeout: Bool
|
||||
@State var deleting: Bool = false
|
||||
@@ -21,11 +22,14 @@ struct ChatPreviewView: View {
|
||||
|
||||
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
|
||||
|
||||
var dynamicMediaSize: CGFloat { dynamicSizes[userFont]?.mediaSize ?? 36 }
|
||||
var dynamicChatInfoSize: CGFloat { dynamicSizes[userFont]?.chatInfoSize ?? 18 }
|
||||
|
||||
var body: some View {
|
||||
let cItem = chat.chatItems.last
|
||||
return HStack(spacing: 8) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ChatInfoImage(chat: chat, size: 63)
|
||||
ChatInfoImage(chat: chat, size: dynamicSizes[userFont]?.profileImageSize ?? 63)
|
||||
chatPreviewImageOverlayIcon()
|
||||
.padding([.bottom, .trailing], 1)
|
||||
}
|
||||
@@ -172,7 +176,7 @@ struct ChatPreviewView: View {
|
||||
private func chatPreviewLayout(_ text: Text?, draft: Bool = false, _ hasFilePreview: Bool = false) -> some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
let t = text
|
||||
.lineLimit(2)
|
||||
.lineLimit(userFont <= .xxxLarge ? 2 : 1)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.padding(.leading, hasFilePreview ? 0 : 8)
|
||||
@@ -192,22 +196,25 @@ struct ChatPreviewView: View {
|
||||
let s = chat.chatStats
|
||||
if s.unreadCount > 0 || s.unreadChat {
|
||||
unreadCountText(s.unreadCount)
|
||||
.font(.caption)
|
||||
.font(userFont <= .xxxLarge ? .caption : .caption2)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 4)
|
||||
.frame(minWidth: 18, minHeight: 18)
|
||||
.padding(.horizontal, dynamicSizes[userFont]?.unreadPadding ?? 4)
|
||||
.frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize)
|
||||
.background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary)
|
||||
.cornerRadius(10)
|
||||
.cornerRadius(dynamicSizes[userFont]?.unreadCorner ?? 10)
|
||||
} else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local {
|
||||
Image(systemName: "speaker.slash.fill")
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} else if chat.chatInfo.chatSettings?.favorite ?? false {
|
||||
Image(systemName: "star.fill")
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 18, height: 18)
|
||||
.frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize)
|
||||
.padding(.trailing, 1)
|
||||
.foregroundColor(.secondary.opacity(0.65))
|
||||
.foregroundColor(theme.colors.secondary.opacity(0.65))
|
||||
} else {
|
||||
Color.clear.frame(width: 0)
|
||||
}
|
||||
@@ -293,12 +300,12 @@ struct ChatPreviewView: View {
|
||||
let mc = ci.content.msgContent
|
||||
switch mc {
|
||||
case let .link(_, preview):
|
||||
smallContentPreview(
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 36, height: 36)
|
||||
.frame(width: dynamicMediaSize, height: dynamicMediaSize)
|
||||
ZStack {
|
||||
Image(systemName: "arrow.up.right")
|
||||
.resizable()
|
||||
@@ -313,25 +320,25 @@ struct ChatPreviewView: View {
|
||||
.onTapGesture {
|
||||
UIApplication.shared.open(preview.uri)
|
||||
}
|
||||
)
|
||||
}
|
||||
case let .image(_, image):
|
||||
smallContentPreview(
|
||||
CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: 36, smallView: true, showFullScreenImage: $showFullscreenGallery)
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
|
||||
.environmentObject(ReverseListScrollModel<ChatItem>())
|
||||
)
|
||||
}
|
||||
case let .video(_,image, duration):
|
||||
smallContentPreview(
|
||||
CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: 36, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
|
||||
.environmentObject(ReverseListScrollModel<ChatItem>())
|
||||
)
|
||||
}
|
||||
case let .voice(_, duration):
|
||||
smallContentPreviewVoice(
|
||||
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: Binding.constant(true), smallView: true)
|
||||
)
|
||||
smallContentPreviewVoice(size: dynamicMediaSize) {
|
||||
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: Binding.constant(true), smallViewSize: dynamicMediaSize)
|
||||
}
|
||||
case .file:
|
||||
smallContentPreviewFile(
|
||||
CIFileView(file: ci.file, edited: ci.meta.itemEdited, smallView: true)
|
||||
)
|
||||
smallContentPreviewFile(size: dynamicMediaSize) {
|
||||
CIFileView(file: ci.file, edited: ci.meta.itemEdited, smallViewSize: dynamicMediaSize)
|
||||
}
|
||||
default: EmptyView()
|
||||
}
|
||||
}
|
||||
@@ -406,33 +413,28 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func smallContentPreview(_ view: some View) -> some View {
|
||||
ZStack {
|
||||
view
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
func smallContentPreview(size: CGFloat, _ view: @escaping () -> some View) -> some View {
|
||||
view()
|
||||
.frame(width: size, height: size)
|
||||
.cornerRadius(8)
|
||||
.overlay(RoundedRectangle(cornerSize: CGSize(width: 8, height: 8))
|
||||
.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true))
|
||||
.padding([.top, .leading], 3)
|
||||
.padding(.vertical, size / 6)
|
||||
.padding(.leading, 3)
|
||||
.offset(x: 6)
|
||||
}
|
||||
|
||||
func smallContentPreviewVoice(_ view: some View) -> some View {
|
||||
ZStack {
|
||||
view
|
||||
.frame(height: voiceMessageSizeBasedOnSquareSize(36))
|
||||
}
|
||||
func smallContentPreviewVoice(size: CGFloat, _ view: @escaping () -> some View) -> some View {
|
||||
view()
|
||||
.frame(height: voiceMessageSizeBasedOnSquareSize(size))
|
||||
.padding(.vertical, size / 6)
|
||||
.padding(.leading, 8)
|
||||
.padding(.top, 6)
|
||||
}
|
||||
|
||||
func smallContentPreviewFile(_ view: some View) -> some View {
|
||||
ZStack {
|
||||
view
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
.padding(.top, 2)
|
||||
func smallContentPreviewFile(size: CGFloat, _ view: @escaping () -> some View) -> some View {
|
||||
view()
|
||||
.frame(width: size, height: size)
|
||||
.padding(.vertical, size / 7)
|
||||
.padding(.leading, 5)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@ import SimpleXChat
|
||||
struct ContactRequestView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||
var contactRequest: UserContactRequest
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
ChatInfoImage(chat: chat, size: 63)
|
||||
ChatInfoImage(chat: chat, size: dynamicSizes[userFont]?.profileImageSize ?? 63)
|
||||
.padding(.leading, 4)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .top) {
|
||||
|
||||
Reference in New Issue
Block a user