(IOS) Consistent component for upload item for share extension (#9030)

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Rajat Dabade 2025-08-11 16:56:46 +05:30 committed by GitHub
parent a102b6147e
commit abb8fc84e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 141 additions and 121 deletions

View file

@ -11,32 +11,47 @@ import SwiftUI
struct AttachmentInfoView: View {
var attachment: AttachmentModel
var hasError: Bool
var fullWidth: Bool = false
var body: some View {
HStack {
attachment.icon()
VStack(alignment: .leading) {
HStack(spacing: 12) {
VStack {
attachment.icon()
}
.frame(width: 48, height: 48)
.background(Color.clear)
VStack(alignment: .leading, spacing: 2) {
Text(attachment.fileName)
.foregroundColor(Color.theme.centerChannelColor)
.lineLimit(1)
.font(Font.custom("OpenSans-SemiBold", size: 16))
.frame(maxWidth: .infinity, alignment: .leading)
Text("\(attachment.fileUrl.pathExtension.uppercased()) \(attachment.formattedFileSize)")
.foregroundColor(Color.theme.centerChannelColor.opacity(0.64))
.lineLimit(1)
.font(Font.custom("OpenSans", size: 12))
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, alignment: .leading)
Spacer(minLength: 0)
}
.padding(.all, 12)
.frame(height: 64, alignment: .leading)
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 64, alignment: .leading)
.background(
.padding(.leading, 12)
.padding(.trailing, 16)
.padding(.vertical, 12)
.frame(height: 64)
.frame(maxWidth: fullWidth ? .infinity : 240)
.background(Color.theme.centerChannelBg)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(
hasError ? Color.theme.errorTextColor : Color.theme.centerChannelColor.opacity(0.16),
lineWidth: 1
)
.shadow(color: Color(red: 0, green: 0, blue: 0, opacity: 0.08), radius: 3, x: 0, y: 2)
.background(.background)
)
.cornerRadius(4)
.shadow(color: Color(red: 0, green: 0, blue: 0, opacity: 0.08), radius: 3, x: 0, y: 2)
}
}

View file

@ -46,14 +46,20 @@ struct AttachmentsView: View {
}
var body: some View {
VStack (spacing: 8) {
VStack(alignment: .leading, spacing: 0) {
if (attachments.count == 1) {
let attachment = attachments[0]
let isImageType = attachment.type == .image
SingleAttachmentView(
attachment: attachments[0],
hasError: attachments[0].sizeError(server: shareViewModel.server) ||
attachments[0].resolutionError(server: shareViewModel.server)
attachments: $attachments,
attachment: attachment,
index: 0,
hasError: attachment.sizeError(server: shareViewModel.server) ||
attachment.resolutionError(server: shareViewModel.server),
isSmall: isImageType
)
.padding(.horizontal, 20)
.padding(.horizontal, 16)
.transition(.opacity)
} else {
MultipleAttachmentView(attachments: $attachments)
@ -62,7 +68,8 @@ struct AttachmentsView: View {
if error != nil {
ErrorLabelView(error: error!)
.padding(.horizontal, 20)
.padding(.horizontal, 16)
.padding(.top, 8)
}
}
.animation(.linear(duration: 0.3))

View file

@ -8,31 +8,25 @@
import SwiftUI
struct FileThumbnail: View {
var small = false
var attachment: AttachmentModel
var hasError: Bool
var body: some View {
VStack {
attachment.icon()
Text(attachment.fileName)
.foregroundColor(Color.theme.centerChannelColor)
.lineLimit(1)
.font(Font.custom("OpenSans-SemiBold", size: 10))
.padding(.horizontal, 4)
Text("\(attachment.fileUrl.pathExtension.uppercased()) \(attachment.formattedFileSize)")
.foregroundColor(Color.theme.centerChannelColor.opacity(0.64))
.lineLimit(1)
.font(Font.custom("OpenSans", size: 12))
if small {
// Small mode: Use AttachmentInfoView with fixed width
AttachmentInfoView(
attachment: attachment,
hasError: hasError,
fullWidth: false
)
} else {
// Full mode: Use AttachmentInfoView with full width
AttachmentInfoView(
attachment: attachment,
hasError: hasError,
fullWidth: true
)
}
.background(
RoundedRectangle(cornerRadius: 4)
.stroke(
hasError ? Color.theme.errorTextColor : Color.theme.centerChannelColor.opacity(0.16),
lineWidth: 1
)
.shadow(color: Color(red: 0, green: 0, blue: 0, opacity: 0.08), radius: 3, x: 0, y: 2)
.frame(width: 104, height: 104)
)
.frame(width: 104, height: 104)
}
}

View file

@ -42,18 +42,21 @@ struct ImageThumbnail: View {
}
var body: some View {
Image(uiImage: downsample(imageAt: attachment.fileUrl, to: CGSize(width: small ? 104 : 0, height: small ? 104 : 156))!)
let imageSize: CGFloat = small ? 64 : 156
Image(uiImage: downsample(imageAt: attachment.fileUrl, to: CGSize(width: imageSize, height: imageSize))!)
.resizable()
.aspectRatio(contentMode: small ? .fill : .fit)
.frame(width: small ? 104 : nil, height: small ? 104 : 156)
.frame(width: small ? 64 : nil, height: small ? 64 : imageSize)
.clipped()
.cornerRadius(4)
.background(
.background(Color.theme.centerChannelBg)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(
hasError ? Color.theme.errorTextColor : Color.theme.centerChannelColor.opacity(0.16),
lineWidth: 1
)
.shadow(color: Color(red: 0, green: 0, blue: 0, opacity: 0.08), radius: 3, x: 0, y: 2)
)
}
}

View file

@ -13,55 +13,29 @@ struct MultipleAttachmentView: View {
@Binding var attachments: [AttachmentModel]
var body: some View {
VStack (alignment: .leading) {
VStack (alignment: .leading, spacing: 4) {
ScrollView (.horizontal, showsIndicators: true) {
HStack(spacing: 12) {
HStack(spacing: 8) {
ForEach(attachments.indices, id: \.self) { index in
let attachment = attachments[index]
let hasError = attachment.sizeError(server: shareViewModel.server) || attachment.resolutionError(server: shareViewModel.server)
let isFirst = index == 0
let isLast = index == attachments.count - 1
VStack {
if attachment.type == .image {
AttachmentWrapperView(isFirst: isFirst, isLast: isLast) {
ImageThumbnail(
small: true,
attachment: attachment,
hasError: hasError
)
.overlay(
RemoveAttachmentView(attachments: $attachments, index: index),
alignment: .topTrailing
)
}
} else if attachment.type == .video {
AttachmentWrapperView(isFirst: isFirst, isLast: isLast) {
VideoThumbnail(
small: true,
attachment: attachment,
hasError: hasError
)
.overlay(
RemoveAttachmentView(attachments: $attachments, index: index),
alignment: .topTrailing
)
}
} else if attachment.type == .file {
AttachmentWrapperView(isFirst: isFirst, isLast: isLast) {
FileThumbnail(
attachment: attachment,
hasError: hasError
)
.overlay(
RemoveAttachmentView(attachments: $attachments, index: index),
alignment: .topTrailing
)
}
}
}
SingleAttachmentView(
attachments: $attachments,
attachment: attachment,
index: index,
hasError: hasError,
isSmall: true
)
.padding(.top, 8)
.padding(.trailing, 8)
.padding(.leading, isFirst ? 16 : 0)
.padding(.trailing, isLast ? 16 : 0)
}
}
.frame(height: 116)
.frame(height: 80)
}
if (attachments.count > 1) {
@ -74,7 +48,8 @@ struct MultipleAttachmentView: View {
)
.foregroundColor(Color.theme.centerChannelColor.opacity(0.64))
.font(Font.custom("OpenSans", size: 12))
.padding(.leading, 20)
.padding(.leading, 16)
.padding(.top, 4)
}
}
}

View file

@ -17,20 +17,20 @@ struct RemoveAttachmentView: View {
}
var body: some View {
if attachments.count > 1 {
FontIcon.button(
.compassIcons(code: .closeCircle),
action: {
withAnimation {
removeAtIndex(index)
}
},
fontsize: 24,
color: Color.theme.centerChannelColor.opacity(0.56)
)
.background(.background)
.cornerRadius(12)
.offset(x: 5, y: -7)
}
// X button styled like Android RemoveButton
FontIcon.button(
.compassIcons(code: .closeCircle),
action: {
withAnimation {
removeAtIndex(index)
}
},
fontsize: 24,
color: Color.theme.centerChannelColor.opacity(0.64)
)
.frame(width: 24, height: 24)
.background(Color.theme.centerChannelBg)
.cornerRadius(12)
.shadow(color: Color(red: 0, green: 0, blue: 0, opacity: 0.08), radius: 2, x: 0, y: 1)
}
}

View file

@ -9,25 +9,54 @@
import SwiftUI
struct SingleAttachmentView: View {
@Binding var attachments: [AttachmentModel]
var attachment: AttachmentModel
var index: Int
var hasError: Bool
var isSmall: Bool = false
var body: some View {
if (attachment.type == .image) {
ImageThumbnail(
attachment: attachment,
hasError: hasError
)
} else if (attachment.type == .video) {
VideoThumbnail(
attachment: attachment,
hasError: hasError
)
} else {
AttachmentInfoView(
attachment: attachment,
hasError: hasError
)
ZStack {
Group {
if attachment.type == .image {
ImageThumbnail(
small: isSmall,
attachment: attachment,
hasError: hasError
)
} else if attachment.type == .video {
VideoThumbnail(
small: isSmall,
attachment: attachment,
hasError: hasError
)
} else {
if isSmall {
FileThumbnail(
small: true,
attachment: attachment,
hasError: hasError
)
} else {
AttachmentInfoView(
attachment: attachment,
hasError: hasError,
fullWidth: true
)
}
}
}
if isSmall && attachments.count > 1 {
VStack {
HStack {
Spacer()
RemoveAttachmentView(attachments: $attachments, index: index)
.offset(x: 8, y: -8)
}
Spacer()
}
}
}
}
}

View file

@ -38,9 +38,8 @@ struct VideoThumbnail: View {
Image(uiImage: thumb!)
.resizable()
.aspectRatio(contentMode: small ? .fill : .fit)
.frame(
maxWidth: small ? 104 : nil)
.frame(height: small ? 104 : 180)
.frame(width: small ? 64 : nil, height: small ? 64 : 180)
.clipped()
.cornerRadius(4)
.overlay(
RoundedRectangle(cornerRadius: 4)
@ -53,11 +52,11 @@ struct VideoThumbnail: View {
startPoint: .topTrailing, endPoint: .bottomLeading))
)
}
.background(Color.theme.centerChannelBg)
.overlay(
ZStack {
RoundedRectangle(cornerRadius: 4)
.stroke(hasError ? Color.theme.errorTextColor : borderColor, lineWidth: 1)
.shadow(color: borderColor, radius: 3, x: 0, y: 2)
FontIcon.text(
FontCode.compassIcons(code: .play),
fontsize: 24,
@ -66,7 +65,7 @@ struct VideoThumbnail: View {
.padding(.trailing, 6)
.padding(.bottom, 6)
.frame(
height: small ? 104 : 180,
height: small ? 64 : 180,
alignment: .bottomTrailing)
}
)

View file

@ -20,7 +20,7 @@ struct ContentView: View {
}
var body: some View {
VStack {
VStack(spacing: 12) {
if (!linkPreviewUrl.isEmpty) {
LinkPreview(
link: $linkPreviewUrl,
@ -30,12 +30,11 @@ struct ContentView: View {
if (!attachments.isEmpty) {
AttachmentsView(attachments: $attachments)
.padding(.horizontal, -20)
.padding(.horizontal, -16)
}
if (!linkPreviewUrl.isEmpty || !attachments.isEmpty) {
Divider()
.padding(.top, 10)
}
VStack (spacing: 0) {
@ -63,7 +62,6 @@ struct ContentView: View {
.padding(.all, 0)
Divider()
.padding(.bottom, 10)
if hasChannels {
FloatingTextField(