feat(live-room): 채팅창 얼리기 기능을 추가한다

채팅 입력 제어와 룸 상태 동기화를 통합해 지연 입장자도 동일 상태를 적용한다.
This commit is contained in:
Yu Sung
2026-03-19 18:20:13 +09:00
parent 0a22f87acc
commit 70003af82b
14 changed files with 466 additions and 29 deletions

View File

@@ -19,6 +19,7 @@ struct LiveRoomInfoHostView: View {
let isOnNotice: Bool
let isOnMenuPan: Bool
let isOnSignature: Bool
let isOnChatFreeze: Bool
let isShowMenuPanButton: Bool
let creatorId: Int
@@ -40,6 +41,7 @@ struct LiveRoomInfoHostView: View {
let onClickTotalHeart: () -> Void
let onClickTotalDonation: () -> Void
let onClickParticipants: () -> Void
let onClickToggleChatFreeze: () -> Void
let onClickToggleSignature: () -> Void
var body: some View {
@@ -55,6 +57,18 @@ struct LiveRoomInfoHostView: View {
) { onClickQuit() }
Spacer()
LiveRoomOverlayStrokeTextToggleButton(
isOn: isOnChatFreeze,
onText: I18n.LiveRoom.chatFreezeOn,
onTextColor: Color.button,
onStrokeColor: Color.button,
offText: I18n.LiveRoom.chatFreezeOff,
offTextColor: Color.graybb,
offStrokeColor: Color.graybb,
strokeWidth: 1,
strokeCornerRadius: 5.3
) { onClickToggleChatFreeze() }
LiveRoomOverlayStrokeTextToggleButton(
isOn: isOnSignature,
@@ -240,6 +254,7 @@ struct LiveRoomInfoHostView_Previews: PreviewProvider {
isOnNotice: true,
isOnMenuPan: false,
isOnSignature: false,
isOnChatFreeze: false,
isShowMenuPanButton: false,
creatorId: 1,
creatorNickname: "도화",
@@ -271,6 +286,7 @@ struct LiveRoomInfoHostView_Previews: PreviewProvider {
onClickTotalHeart: {},
onClickTotalDonation: {},
onClickParticipants: {},
onClickToggleChatFreeze: {},
onClickToggleSignature: {}
)
}

View File

@@ -11,16 +11,27 @@ struct LiveRoomInputChatView: View {
@State private var chatMessage = ""
let isInputDisabled: Bool
let sendMessage: (String) -> Bool
let onDisabledInputTap: () -> Void
var body: some View {
HStack(spacing: 6.7) {
ChatTextFieldView(text: $chatMessage, placeholder: "채팅을 입력하세요") {
ChatTextFieldView(text: $chatMessage, placeholder: "채팅을 입력하세요", isEnabled: !isInputDisabled) {
if sendMessage(chatMessage) {
chatMessage = ""
}
}
.allowsHitTesting(!isInputDisabled)
.overlay {
if isInputDisabled {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
onDisabledInputTap()
}
}
}
.padding(.vertical, 18.3)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity)
@@ -28,7 +39,13 @@ struct LiveRoomInputChatView: View {
Image("btn_message_send")
.resizable()
.frame(width: 35, height: 35)
.opacity(isInputDisabled ? 0.5 : 1)
.onTapGesture {
if isInputDisabled {
onDisabledInputTap()
return
}
if sendMessage(chatMessage) {
chatMessage = ""
}
@@ -43,12 +60,18 @@ struct LiveRoomInputChatView: View {
.strokeBorder(lineWidth: 1)
.foregroundColor(.gray77)
)
.onChange(of: isInputDisabled) { isDisabled in
if isDisabled {
hideKeyboard()
chatMessage = ""
}
}
.padding(13.3)
}
}
struct LiveRoomInputChatView_Previews: PreviewProvider {
static var previews: some View {
LiveRoomInputChatView(sendMessage: { _ in return true })
LiveRoomInputChatView(isInputDisabled: false, sendMessage: { _ in return true }, onDisabledInputTap: {})
}
}

View File

@@ -31,6 +31,14 @@ struct LiveRoomViewV2: View {
@State private var isShowFollowNotifyDialog: Bool = false
@State private var guestFollowButtonTypeOverride: FollowButtonImageType? = nil
let heartWaveTimer = Timer.publish(every: 1/60, on: .main, in: .common).autoconnect()
private var appliedKeyboardHeight: CGFloat {
guard !viewModel.isChatFrozenForCurrentUser else {
return 0
}
return max(keyboardHandler.keyboardHeight, 0)
}
var body: some View {
ZStack {
@@ -48,6 +56,7 @@ struct LiveRoomViewV2: View {
isOnNotice: viewModel.isShowNotice,
isOnMenuPan: viewModel.isShowMenuPan,
isOnSignature: viewModel.isSignatureOn,
isOnChatFreeze: viewModel.isChatFrozen,
isShowMenuPanButton: !liveRoomInfo.menuPan.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
creatorId: liveRoomInfo.creatorId,
creatorNickname: liveRoomInfo.creatorNickname,
@@ -88,6 +97,9 @@ struct LiveRoomViewV2: View {
onClickParticipants: {
viewModel.isShowProfileList = true
},
onClickToggleChatFreeze: {
viewModel.setChatFreeze(isChatFrozen: !viewModel.isChatFrozen)
},
onClickToggleSignature: {
viewModel.isSignatureOn.toggle()
}
@@ -343,14 +355,21 @@ struct LiveRoomViewV2: View {
.padding(.horizontal, 13.3)
}
LiveRoomInputChatView {
viewModel.sendMessage(chatMessage: $0) {
viewModel.isShowingNewChat = false
proxy.scrollTo(viewModel.messages.count - 1, anchor: .center)
LiveRoomInputChatView(
isInputDisabled: viewModel.isChatFrozenForCurrentUser,
sendMessage: {
viewModel.sendMessage(chatMessage: $0) {
viewModel.isShowingNewChat = false
proxy.scrollTo(viewModel.messages.count - 1, anchor: .center)
}
return true
},
onDisabledInputTap: {
viewModel.errorMessage = I18n.LiveRoom.chatFreezeBlockedMessage
viewModel.isShowErrorPopup = true
}
return true
}
)
.padding(.top, isV2VCaptionVisible ? -13.3 : 0)
.padding(.bottom, 10)
}
@@ -468,7 +487,7 @@ struct LiveRoomViewV2: View {
}
.sodaToast(isPresented: $viewModel.isShowErrorPopup, message: viewModel.errorMessage, autohideIn: 1.3)
.cornerRadius(16.7, corners: [.topLeft, .topRight])
.offset(y: -(keyboardHandler.keyboardHeight > 0 ? keyboardHandler.keyboardHeight : 0))
.offset(y: -appliedKeyboardHeight)
.onAppear {
UIApplication.shared.isIdleTimerDisabled = true
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
@@ -810,8 +829,8 @@ struct LiveRoomViewV2: View {
.drawingGroup(opaque: false, colorMode: .linear)
}
//
.offset(y: keyboardHandler.keyboardHeight > 0 ? -(keyboardHandler.keyboardHeight / 2 + 60) : 0)
.animation(.spring(response: 0.3, dampingFraction: 0.85), value: keyboardHandler.keyboardHeight)
.offset(y: appliedKeyboardHeight > 0 ? -(appliedKeyboardHeight / 2 + 60) : 0)
.animation(.spring(response: 0.3, dampingFraction: 0.85), value: appliedKeyboardHeight)
}
.onReceive(heartWaveTimer) { _ in
guard isLongPressingHeart else { return }
@@ -836,8 +855,13 @@ struct LiveRoomViewV2: View {
.onReceive(NotificationCenter.default.publisher(for: .requestLiveRoomQuitForExternalNavigation)) { _ in
viewModel.quitRoom()
}
.onChange(of: viewModel.isChatFrozenForCurrentUser) { isFrozen in
if isFrozen {
hideKeyboard()
}
}
.ignoresSafeArea(.keyboard)
.edgesIgnoringSafeArea(keyboardHandler.keyboardHeight > 0 ? .bottom : .init())
.edgesIgnoringSafeArea(appliedKeyboardHeight > 0 ? .bottom : .init())
.sheet(
isPresented: $viewModel.isShowShareView,
onDismiss: { viewModel.shareMessage = "" },