import SwiftUI import UIKit struct UserCreatorChatRoomView: View { @StateObject private var viewModel = UserCreatorChatRoomViewModel() @State private var messageText = "" private let roomId: Int? private let creatorId: Int? init(roomId: Int) { self.roomId = roomId self.creatorId = nil } init(creatorId: Int) { self.roomId = nil self.creatorId = creatorId } var body: some View { BaseView(isLoading: $viewModel.isLoading) { VStack(spacing: 0) { headerView messageListView .frame(maxWidth: .infinity, maxHeight: .infinity) inputBarView } .background(Color.black.ignoresSafeArea()) } .onAppear(perform: enterRoom) .onDisappear { viewModel.leaveAndClose() } .sodaToast( isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2 ) } private var headerView: some View { HStack(spacing: SodaSpacing.s12) { Image("ic_back") .resizable() .frame(width: 24, height: 24) .onTapGesture { AppState.shared.back() } DownsampledKFImage( url: URL(string: viewModel.opponentProfileImageUrl ?? ""), size: CGSize(width: 36, height: 36) ) .clipShape(Circle()) Text(viewModel.opponentNickname) .appFont(size: 14, weight: .bold) .foregroundColor(.white) .lineLimit(1) .truncationMode(.tail) Spacer(minLength: 0) } .padding(.horizontal, SodaSpacing.s16) .padding(.vertical, SodaSpacing.s8) .frame(width: screenSize().width, height: 60) .background(Color.black) } private var messageListView: some View { GeometryReader { geometry in ScrollViewReader { proxy in ScrollView(.vertical, showsIndicators: false) { LazyVStack(spacing: SodaSpacing.s16) { if viewModel.isLoadingNextPage { ProgressView() .progressViewStyle(.circular) .tint(Color.soda400) .padding(.vertical, SodaSpacing.s12) } ForEach(textMessages) { message in UserCreatorChatTextMessageItemView(message: message) .id(message.id) .onAppear { if message.id == textMessages.first?.id { viewModel.loadMore() } } } } .padding(.horizontal, SodaSpacing.s24) .padding(.vertical, SodaSpacing.s12) .background( GeometryReader { contentGeometry in Color.clear.preference( key: UserCreatorChatContentHeightPreferenceKey.self, value: contentGeometry.size.height ) } ) .frame(minHeight: geometry.size.height, alignment: .bottom) } .onPreferenceChange(UserCreatorChatContentHeightPreferenceKey.self) { contentHeight in guard contentHeight <= geometry.size.height else { return } viewModel.loadMore() } .onChange(of: textMessages.last?.id) { _ in scrollToBottom(proxy) } .onChange(of: viewModel.socketState) { state in guard state == .joined else { return } scrollToBottom(proxy) } } } } private var inputBarView: some View { HStack(spacing: SodaSpacing.s8) { ZStack(alignment: .leading) { if messageText.isEmpty { Text(I18n.UserCreatorChat.messagePlaceholder) .appFont(size: 14, weight: .regular) .foregroundColor(Color.gray500) } TextField("", text: $messageText) .appFont(size: 14, weight: .regular) .foregroundColor(.white) .accentColor(Color.soda400) .disabled(isInputEnabled == false) .onSubmit { sendText() } } .padding(.horizontal, SodaSpacing.s16) .padding(.vertical, SodaSpacing.s12) .background(Color.gray900) .cornerRadius(999) Button(action: sendText) { Image("ic_message_send") .resizable() .frame(width: 24, height: 24) .opacity(isSendEnabled ? 1 : 0.4) } .buttonStyle(.plain) .disabled(isSendEnabled == false) } .padding(.horizontal, SodaSpacing.s12) .padding(.vertical, SodaSpacing.s12) .frame(width: screenSize().width) .background(Color.black) } private var textMessages: [UserCreatorChatDisplayMessage] { viewModel.messages.filter { $0.messageType == "TEXT" } } private var isInputEnabled: Bool { viewModel.socketState == .joined } private var isSendEnabled: Bool { isInputEnabled && messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false } private func enterRoom() { if let roomId { viewModel.enter(roomId: roomId) } else if let creatorId { viewModel.enter(creatorId: creatorId) } } private func sendText() { let trimmedText = messageText.trimmingCharacters(in: .whitespacesAndNewlines) guard isInputEnabled, trimmedText.isEmpty == false else { return } UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) viewModel.sendText(trimmedText) messageText = "" } private func scrollToBottom(_ proxy: ScrollViewProxy) { guard let lastMessage = textMessages.last else { return } withAnimation(.easeOut(duration: 0.3)) { proxy.scrollTo(lastMessage.id, anchor: .bottom) } } } private struct UserCreatorChatContentHeightPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } struct UserCreatorChatRoomView_Previews: PreviewProvider { static var previews: some View { UserCreatorChatRoomView(roomId: 1) } }