159 lines
5.6 KiB
Swift
159 lines
5.6 KiB
Swift
//
|
|
// UserMessageItemView.swift
|
|
// SodaLive
|
|
//
|
|
// Created by klaus on 9/2/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct UserMessageBubbleShape: Shape {
|
|
func path(in rect: CGRect) -> Path {
|
|
let path = UIBezierPath()
|
|
|
|
// 시작점 (왼쪽 상단, 16px 반지름)
|
|
path.move(to: CGPoint(x: 16, y: 0))
|
|
|
|
// 상단 라인 (오른쪽 상단 4px 반지름까지)
|
|
path.addLine(to: CGPoint(x: rect.width - 4, y: 0))
|
|
|
|
// 오른쪽 상단 모서리 (4px 반지름)
|
|
path.addArc(withCenter: CGPoint(x: rect.width - 4, y: 4),
|
|
radius: 4,
|
|
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)
|
|
|
|
// 왼쪽 라인 (왼쪽 상단 16px 반지름까지)
|
|
path.addLine(to: CGPoint(x: 0, y: 16))
|
|
|
|
// 왼쪽 상단 모서리 (16px 반지름)
|
|
path.addArc(withCenter: CGPoint(x: 16, y: 16),
|
|
radius: 16,
|
|
startAngle: CGFloat.pi,
|
|
endAngle: -CGFloat.pi / 2,
|
|
clockwise: true)
|
|
|
|
path.close()
|
|
return Path(path.cgPath)
|
|
}
|
|
}
|
|
|
|
struct UserMessageItemView: View {
|
|
let message: ServerChatMessage
|
|
|
|
var body: some View {
|
|
HStack(alignment: .bottom, spacing: 4) {
|
|
Spacer()
|
|
|
|
// 시간 표시
|
|
VStack {
|
|
Text(formatTime(from: message.createdAt))
|
|
.font(.custom(Font.preRegular.rawValue, size: 10))
|
|
.foregroundColor(.white)
|
|
}
|
|
|
|
// 메시지 버블
|
|
HStack(spacing: 9) {
|
|
VStack(alignment: .trailing, spacing: 4) {
|
|
// 메시지 텍스트
|
|
HStack(spacing: 10) {
|
|
styledMessageText(message.message)
|
|
.lineLimit(nil)
|
|
.multilineTextAlignment(.leading)
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 8)
|
|
.background(Color.button)
|
|
.clipShape(UserMessageBubbleShape())
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
|
}
|
|
|
|
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: "333333"))
|
|
|
|
// 소괄호 뒤의 텍스트 (일반 스타일)
|
|
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 {
|
|
UserMessageItemView(
|
|
message: ServerChatMessage(
|
|
messageId: 1,
|
|
message: "(만약에) 멈춰 인프피",
|
|
profileImageUrl: "",
|
|
mine: true,
|
|
createdAt: Date().currentTimeMillis(),
|
|
messageType: "text",
|
|
imageUrl: nil,
|
|
price: nil,
|
|
hasAccess: true
|
|
)
|
|
)
|
|
.padding()
|
|
.background(Color.black)
|
|
}
|