Files
sodalive-ios/SodaLive/Sources/Chat/Talk/Room/Message/AiMessageItemView.swift

240 lines
9.6 KiB
Swift

//
// AiMessageItemView.swift
// SodaLive
//
// Created by klaus on 9/2/25.
//
import SwiftUI
import Kingfisher
struct AiMessageBubbleShape: Shape {
func path(in rect: CGRect) -> Path {
let path = UIBezierPath()
// ( , 4px )
path.move(to: CGPoint(x: 4, y: 0))
// ( 16px )
path.addLine(to: CGPoint(x: rect.width - 16, y: 0))
// (16px )
path.addArc(withCenter: CGPoint(x: rect.width - 16, y: 16),
radius: 16,
startAngle: -CGFloat.pi / 2,
endAngle: 0,
clockwise: true)
// ( 16px )
path.addLine(to: CGPoint(x: rect.width, y: rect.height - 16))
// (16px )
path.addArc(withCenter: CGPoint(x: rect.width - 16, y: rect.height - 16),
radius: 16,
startAngle: 0,
endAngle: CGFloat.pi / 2,
clockwise: true)
// ( 16px )
path.addLine(to: CGPoint(x: 16, y: rect.height))
// (16px )
path.addArc(withCenter: CGPoint(x: 16, y: rect.height - 16),
radius: 16,
startAngle: CGFloat.pi / 2,
endAngle: CGFloat.pi,
clockwise: true)
// ( 4px )
path.addLine(to: CGPoint(x: 0, y: 4))
// (4px )
path.addArc(withCenter: CGPoint(x: 4, y: 4),
radius: 4,
startAngle: CGFloat.pi,
endAngle: -CGFloat.pi / 2,
clockwise: true)
path.close()
return Path(path.cgPath)
}
}
struct AiMessageItemView: View {
let message: ServerChatMessage
let characterName: String
let purchaseMessage: () -> Void
var body: some View {
HStack(alignment: .bottom, spacing: 4) {
//
HStack(alignment: .top, spacing: 9) {
//
KFImage(URL(string: message.profileImageUrl))
.placeholder {
Image(systemName: "person.crop.circle")
.resizable()
.scaledToFit()
}
.cancelOnDisappear(true)
.resizable()
.frame(width: 30, height: 30)
.clipShape(Circle())
//
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) {
Text(characterName)
.font(.custom(Font.preRegular.rawValue, size: 12))
.foregroundColor(.white)
}
// ( )
if message.messageType.lowercased() == "image",
let imageUrl = message.imageUrl,
!imageUrl.isEmpty
{
//
let maxWidth = (UIScreen.main.bounds.width - 48) * 0.7
let imageHeight = maxWidth * 5 / 4 // 4:5
ZStack {
KFImage(URL(string: imageUrl))
.cancelOnDisappear(true)
.resizable()
.scaledToFill() //
if let price = message.price, price > 0, !message.hasAccess {
Color.black.opacity(0.2)
.frame(width: maxWidth, height: imageHeight)
.cornerRadius(10)
VStack(spacing: 18) {
HStack(spacing: 4) {
Image("ic_can")
.resizable()
.frame(width: 24, height: 24)
Text("\(message.price ?? 5)")
.font(.custom(Font.preBold.rawValue, size: 16))
.foregroundColor(Color(hex: "263238"))
}
.padding(.horizontal, 10)
.padding(.vertical, 3)
.background(Color(hex: "B5E7FA"))
.cornerRadius(30)
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(lineWidth: 1)
.foregroundColor(.button)
}
Text("눌러서 잠금해제")
.font(.custom(Font.preBold.rawValue, size: 18))
.foregroundColor(.white)
}
.frame(width: maxWidth, height: imageHeight)
}
}
.frame(width: maxWidth, height: imageHeight)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.onTapGesture {
purchaseMessage()
}
} else {
//
HStack(spacing: 10) {
styledMessageText(message.message)
.lineLimit(nil)
.multilineTextAlignment(.leading)
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
.background(
Color.black.opacity(0.1)
)
.clipShape(AiMessageBubbleShape())
}
}
}
//
VStack {
Text(formatTime(from: message.createdAt))
.font(.custom(Font.preRegular.rawValue, size: 10))
.foregroundColor(.white)
}
Spacer()
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private func formatTime(from timestamp: Int64) -> String {
let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
return date.convertDateFormat(dateFormat: "a h:mm")
}
private func styledMessageText(_ message: String) -> Text {
var result = Text("")
let components = message.components(separatedBy: "(")
for (index, component) in components.enumerated() {
if index == 0 {
//
if !component.isEmpty {
result = result + Text(component)
.font(.custom(Font.preRegular.rawValue, size: 16))
.foregroundColor(.white)
}
} else {
// "("
if let closeIndex = component.firstIndex(of: ")") {
let beforeClose = String(component[..<closeIndex])
let afterClose = String(component[component.index(after: closeIndex)...])
// ( )
result = result + Text("(\(beforeClose))")
.font(.system(size: 16, design: .default).italic())
.foregroundColor(Color(hex: "e2e2e2").opacity(0.49))
// ( )
if !afterClose.isEmpty {
result = result + Text(afterClose)
.font(.custom(Font.preRegular.rawValue, size: 16))
.foregroundColor(.white)
}
} else {
//
result = result + Text("(\(component)")
.font(.custom(Font.preRegular.rawValue, size: 16))
.foregroundColor(.white)
}
}
}
return result
}
}
#Preview {
AiMessageItemView(
message: ServerChatMessage(
messageId: 1,
message: "",
profileImageUrl: "https://example.com/profile.jpg",
mine: false,
createdAt: Date().currentTimeMillis(),
messageType: "IMAGE",
imageUrl: "https://picsum.photos/1000",
price: nil,
hasAccess: true
),
characterName: "보라",
purchaseMessage: {}
)
.padding()
.background(Color.black)
}