Files
sodalive-ios/SodaLive/Sources/Chat/Talk/Room/Message/AiMessageItemView.swift
Yu Sung 1ec22717cb feat(chat-room) 채팅방
- 텍스트 메시지 UI 적용
2025-09-03 23:14:26 +09:00

182 lines
6.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
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()
}
.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)
}
//
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: "(언제부턴가) 너랑 노는게 제일 재밌고\n너랑 이야기 하는게 제일 신나더라,\n앞으로도 그럴 것 같아❤️",
profileImageUrl: "https://example.com/profile.jpg",
mine: false,
createdAt: Date().currentTimeMillis(),
messageType: "text",
imageUrl: nil,
price: nil,
hasAccess: true
),
characterName: "보라"
)
.padding()
.background(Color.black)
}