sodalive-ios/SodaLive/Sources/Live/Room/V2/LiveRoomViewV2.swift

724 lines
36 KiB
Swift

//
// LiveRoomViewV2.swift
// SodaLive
//
// Created by klaus on 2024/01/17.
//
import SwiftUI
import Kingfisher
import SDWebImageSwiftUI
struct LiveRoomViewV2: View {
@StateObject var keyboardHandler = KeyboardHandler()
@StateObject var viewModel = LiveRoomViewModel()
@State private var textHeight: CGFloat = .zero
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
VStack(spacing: 0) {
if let liveRoomInfo = viewModel.liveRoomInfo {
if liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) {
LiveRoomInfoHostView(
title: liveRoomInfo.title,
totalDonationCan: viewModel.totalDonationCan,
participantsCount: liveRoomInfo.participantsCount,
isOnBg: viewModel.isBgOn,
isOnNotice: viewModel.isShowNotice,
isOnMenuPan: viewModel.isShowMenuPan,
isShowMenuPanButton: !liveRoomInfo.menuPan.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
creatorId: liveRoomInfo.creatorId,
creatorNickname: liveRoomInfo.creatorNickname,
creatorProfileUrl: liveRoomInfo.creatorProfileUrl,
speakerList: liveRoomInfo.speakerList,
muteSpeakerList: viewModel.muteSpeakers,
activeSpeakerList: viewModel.activeSpeakers,
isAdult: liveRoomInfo.isAdult,
onClickQuit: {
viewModel.isShowLiveEndPopup = true
},
onClickToggleBg: {
viewModel.isBgOn.toggle()
},
onClickShare: {
viewModel.shareRoom()
},
onClickEdit: {
viewModel.isShowEditRoomInfoDialog = true
},
onClickProfile: {
if $0 != UserDefaults.int(forKey: .userId) {
viewModel.getUserProfile(userId: $0)
}
},
onClickNotice: {
viewModel.isShowNotice.toggle()
},
onClickMenuPan: {
viewModel.isShowMenuPan.toggle()
},
onClickTotalDonation: {
viewModel.isShowDonationRankingPopup = true
},
onClickParticipants: {
viewModel.isShowProfileList = true
}
)
} else {
LiveRoomInfoGuestView(
title: liveRoomInfo.title,
totalDonationCan: viewModel.totalDonationCan,
isOnBg: viewModel.isBgOn,
isOnNotice: viewModel.isShowNotice,
isOnMenuPan: viewModel.isShowMenuPan,
isShowMenuPanButton: !liveRoomInfo.menuPan.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
creatorId: liveRoomInfo.creatorId,
creatorNickname: liveRoomInfo.creatorNickname,
creatorProfileUrl: liveRoomInfo.creatorProfileUrl,
speakerList: liveRoomInfo.speakerList,
muteSpeakerList: viewModel.muteSpeakers,
activeSpeakerList: viewModel.activeSpeakers,
isFollowing: liveRoomInfo.isFollowing,
isAdult: liveRoomInfo.isAdult,
onClickQuit: {
viewModel.isShowQuitPopup = true
},
onClickToggleBg: {
viewModel.isBgOn.toggle()
},
onClickShare: {
viewModel.shareRoom()
},
onClickFollow: {
if $0 {
viewModel.creatorUnFollow()
} else {
viewModel.creatorFollow()
}
},
onClickProfile: {
if $0 != UserDefaults.int(forKey: .userId) {
viewModel.getUserProfile(userId: $0)
}
},
onClickNotice: {
viewModel.isShowNotice.toggle()
},
onClickMenuPan: {
viewModel.isShowMenuPan.toggle()
},
onClickTotalDonation: {
viewModel.isShowDonationRankingPopup = true
},
onClickChangeListener: {
viewModel.setListener()
}
)
}
ZStack(alignment: .topLeading) {
Rectangle()
.foregroundColor(.gray22)
.frame(height: 16)
.frame(maxWidth: .infinity)
ScrollViewReader { proxy in
ZStack(alignment: .bottom) {
ZStack {
if viewModel.isBgOn {
KFImage(URL(string: liveRoomInfo.coverImageUrl))
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
Rectangle()
.foregroundColor(.black.opacity(0.25))
.frame(maxWidth: .infinity)
ScrollView(.vertical, showsIndicators: false) {
scrollObservableView
LiveRoomChatView(messages: viewModel.messages) {
if $0 != UserDefaults.int(forKey: .userId) {
viewModel.getUserProfile(userId: $0)
}
}
.frame(width: screenSize().width)
.rotationEffect(Angle(degrees: 180))
.valueChanged(value: viewModel.messageChangeFlag) { _ in
if viewModel.offset - viewModel.originOffset > (56.7 * 2) {
viewModel.isShowingNewChat = true
}
}
}
.rotationEffect(Angle(degrees: 180))
.onTapGesture { hideKeyboard() }
.onPreferenceChange(ScrollOffsetKey.self) {
viewModel.setOffset($0)
}
.padding(.bottom, 70)
}
.padding(.top, 16)
VStack(alignment: .trailing, spacing: 0) {
Spacer()
VStack(spacing: 13.3) {
if liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) {
Image("ic_roulette_settings")
.resizable()
.frame(width: 26.7, height: 26.7)
.padding(11)
.background(Color(hex: "525252").opacity(0.6))
.cornerRadius(10)
.onTapGesture {
viewModel.isShowRouletteSettings = true
}
} else if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) && viewModel.isActiveRoulette {
Image("ic_roulette")
.resizable()
.frame(width: 26.7, height: 26.7)
.padding(11)
.background(Color(hex: "525252").opacity(0.6))
.cornerRadius(10)
.onTapGesture {
viewModel.showRoulette()
}
}
if viewModel.role == .SPEAKER {
Image(viewModel.isMute ? "ic_mic_off" : "ic_mic_on")
.resizable()
.frame(width: 26.7, height: 26.7)
.padding(11)
.background(Color(hex: "525252").opacity(0.6))
.cornerRadius(10)
.onTapGesture {
viewModel.toggleMute()
}
}
Image(viewModel.isSpeakerMute ? "ic_speaker_off" : "ic_speaker_on")
.resizable()
.frame(width: 26.7, height: 26.7)
.padding(11)
.background(Color(hex: "525252").opacity(0.6))
.cornerRadius(10)
.onTapGesture {
viewModel.toggleSpeakerMute()
}
if liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) &&
UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue {
Image("ic_donation_message_list")
.resizable()
.frame(width: 26.7, height: 26.7)
.padding(11)
.background(Color(hex: "525252").opacity(0.6))
.cornerRadius(10)
.onTapGesture {
viewModel.isShowDonationMessagePopup = true
}
} else {
Image("ic_donation")
.resizable()
.frame(width: 26.7, height: 26.7)
.padding(11)
.background(Color(hex: "525252").opacity(0.6))
.cornerRadius(10)
.onTapGesture {
viewModel.isShowDonationPopup = true
}
}
}
.padding(.trailing, 13.3)
LiveRoomInputChatView {
viewModel.sendMessage(chatMessage: $0) {
viewModel.isShowingNewChat = false
proxy.scrollTo(viewModel.messages.count - 1, anchor: .center)
}
return true
}
.padding(.bottom, 10)
}
if viewModel.isShowingNewChat {
LiveRoomNewChatView{
viewModel.isShowingNewChat = false
proxy.scrollTo(viewModel.messages.count - 1, anchor: .center)
}.padding(.bottom, 70)
}
if viewModel.signatureImageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
VStack(spacing: 0) {
Spacer()
AnimatedImage(url: URL(string: viewModel.signatureImageUrl))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 300)
.frame(maxWidth: .infinity)
.padding(.horizontal, 20)
.padding(.bottom, 65)
}
}
}
}
if viewModel.isShowNotice {
VStack(alignment: .leading, spacing: 0) {
Image("ic_notice_triangle")
.padding(.leading, 13.3)
VStack(alignment: .leading, spacing: 8) {
Text("[방송공지]")
.font(.custom(Font.bold.rawValue, size: 11.3))
.foregroundColor(.white)
DetectableTextView(text: liveRoomInfo.notice)
.frame(
width: 280,
height: textHeight > 450 ? 450 : textHeight
)
.onAppear {
self.textHeight = self.estimatedHeight(
for: liveRoomInfo.notice,
width: 280
)
}
.onChange(of: liveRoomInfo.notice) { newText in
self.textHeight = self.estimatedHeight(
for: newText,
width: 280
)
}
}
.padding(8)
.background(Color.gray33)
.padding(.horizontal, 13.3)
.padding(.bottom, 120)
}
}
if viewModel.isShowMenuPan {
VStack(alignment: .leading, spacing: 0) {
Image("ic_notice_triangle")
.padding(.leading, 60)
VStack(alignment: .leading, spacing: 8) {
Text("[메뉴판]")
.font(.custom(Font.bold.rawValue, size: 11.3))
.foregroundColor(.white)
Text(liveRoomInfo.menuPan)
.font(.custom(Font.light.rawValue, size: 11.3))
.foregroundColor(.white)
.lineSpacing(4)
}
.padding(8)
.background(Color.gray33)
.padding(.horizontal, 60)
.padding(.bottom, 120)
}
}
}
}
}
.popup(isPresented: $viewModel.isShowErrorPopup, type: .toast, position: .top, autohideIn: 1.3) {
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
.onDisappear {
if viewModel.liveRoomInfo == nil {
viewModel.quitRoom()
}
}
}
}
.cornerRadius(16.7, corners: [.topLeft, .topRight])
.offset(y: -(keyboardHandler.keyboardHeight > 0 ? keyboardHandler.keyboardHeight : 0))
.onAppear {
UIApplication.shared.isIdleTimerDisabled = true
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
viewModel.getMemberCan()
viewModel.initAgoraEngine()
viewModel.getRoomInfo()
NotificationCenter.default.addObserver(
forName: UIApplication.willTerminateNotification,
object: nil,
queue: .main) { _ in
viewModel.quitRoom()
sleep(3)
}
}
.onDisappear {
UIApplication.shared.isIdleTimerDisabled = false
NotificationCenter.default.removeObserver(self)
}
ZStack {
if viewModel.isShowProfilePopup, let liveRoomInfo = viewModel.liveRoomInfo, let selectedProfile = viewModel.selectedProfile {
LiveRoomProfileDialog(
isShowing: $viewModel.isShowProfilePopup,
profileInfo: selectedProfile,
creatorId: liveRoomInfo.creatorId,
isSpeaker: viewModel.role == .SPEAKER,
onClickInviteSpeaker: { inviteSpeaker(peerId: $0) },
onClickChangeListener: {
if $0 == UserDefaults.int(forKey: .userId) {
viewModel.setListener()
return
}
viewModel.changeListener(peerId: $0)
}
)
}
if viewModel.isShowDonationPopup {
LiveRoomDonationDialogView(isShowing: $viewModel.isShowDonationPopup, isAudioContentDonation: false) { can, message in
viewModel.donation(can: can, message: message)
}
}
if viewModel.isShowQuitPopup {
SodaDialog(
title: "라이브 나가기",
desc: "라이브에서 나가시겠습니까?",
confirmButtonTitle: "",
confirmButtonAction: {
viewModel.isShowQuitPopup = false
viewModel.quitRoom()
},
cancelButtonTitle: "아니오",
cancelButtonAction: {
viewModel.isShowQuitPopup = false
}
)
}
if viewModel.isShowLiveEndPopup {
SodaDialog(
title: "라이브 종료",
desc: "라이브를 종료하시겠습니까?\n" +
"라이브를 종료하면 대화내용은\n" +
"저장되지 않고 사라집니다.\n" +
"참여자들 또한 라이브가 종료되어\n" +
"강제퇴장 됩니다.",
confirmButtonTitle: "",
confirmButtonAction: {
viewModel.isShowLiveEndPopup = false
viewModel.quitRoom()
},
cancelButtonTitle: "아니오",
cancelButtonAction: {
viewModel.isShowLiveEndPopup = false
}
)
}
}
ZStack {
if viewModel.isShowProfileList, let liveRoomInfo = viewModel.liveRoomInfo {
LiveRoomProfilesDialogView(
isShowing: $viewModel.isShowProfileList,
viewModel: viewModel,
roomInfo: liveRoomInfo,
registerNotification: { viewModel.creatorFollow() },
unRegisterNotification: { viewModel.creatorUnFollow() },
onClickProfile: {
if $0 != UserDefaults.int(forKey: .userId) {
viewModel.getUserProfile(userId: $0)
}
},
onClickNoChatting: { userId, nickname, profileUrl in
viewModel.noChattingUserId = userId
viewModel.noChattingUserNickname = nickname
viewModel.noChattingUserProfileUrl = profileUrl
viewModel.isShowNoChattingConfirm = true
}
)
}
if viewModel.isShowUserProfilePopup, let userProfile = viewModel.userProfile {
Color.black.opacity(0.7)
.edgesIgnoringSafeArea(.all)
LiveRoomUserProfileDialogView(
isShowing: $viewModel.isShowUserProfilePopup,
viewModel: viewModel,
userProfile: userProfile,
onClickSetManager: {
viewModel.setManagerMessageToPeer(userId: $0)
viewModel.setManager(userId: $0)
},
onClickReleaseManager: { viewModel.changeListener(peerId: $0, isFromManager: true) },
onClickFollow: { viewModel.creatorFollow(creatorId: $0, isGetUserProfile: true) },
onClickUnFollow: { viewModel.creatorUnFollow(creatorId: $0, isGetUserProfile: true) },
onClickInviteSpeaker: { inviteSpeaker(peerId: $0) },
onClickChangeListener: {
viewModel.changeListener(peerId: $0)
},
onClickMenu: { userId, userNickname, isBlocked in
viewModel.reportUserId = userId
viewModel.reportUserNickname = userNickname
viewModel.reportUserIsBlocked = isBlocked
viewModel.isShowReportMenu = true
},
onClickNoChatting: { userId, nickname, profileUrl in
viewModel.noChattingUserId = userId
viewModel.noChattingUserNickname = nickname
viewModel.noChattingUserProfileUrl = profileUrl
viewModel.isShowNoChattingConfirm = true
}
)
.padding(20)
.popup(isPresented: $viewModel.isShowReportPopup, type: .toast, position: .top, autohideIn: 1.3) {
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.reportMessage)
.padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
}
if viewModel.isShowReportMenu {
VStack(spacing: 0) {
ProfileReportMenuView(
isShowing: $viewModel.isShowReportMenu,
isBlockedUser: viewModel.reportUserIsBlocked,
userBlockAction: { viewModel.isShowUesrBlockConfirm = true },
userUnBlockAction: { viewModel.userUnBlock() },
userReportAction: { viewModel.isShowUesrReportView = true },
profileReportAction: { viewModel.isShowProfileReportConfirm = true }
)
Rectangle()
.foregroundColor(Color(hex: "222222"))
.frame(width: screenSize().width, height: 15.3)
}
.ignoresSafeArea()
}
if viewModel.isShowUesrBlockConfirm {
UserBlockConfirmDialogView(
isShowing: $viewModel.isShowUesrBlockConfirm,
nickname: viewModel.reportUserNickname,
confirmAction: {
viewModel.userBlock { userId in
viewModel.kickOutId = userId
viewModel.kickOut()
}
}
)
}
if viewModel.isShowUesrReportView {
UserReportDialogView(
isShowing: $viewModel.isShowUesrReportView,
confirmAction: { reason in
viewModel.report(type: .USER, reason: reason)
}
)
}
if viewModel.isShowProfileReportConfirm {
ProfileReportDialogView(
isShowing: $viewModel.isShowProfileReportConfirm,
confirmAction: {
viewModel.report(type: .PROFILE)
}
)
}
if viewModel.isShowNoChattingConfirm && viewModel.noChattingUserId > 0 {
LiveRoomNoChattingDialogView(
nickname: viewModel.noChattingUserNickname,
profileUrl: viewModel.noChattingUserProfileUrl,
confirmAction: {
viewModel.isShowNoChattingConfirm = false
viewModel.setNoChatting()
},
cancelAction: {
viewModel.noChattingUserId = 0
viewModel.noChattingUserNickname = ""
viewModel.noChattingUserProfileUrl = ""
viewModel.isShowNoChattingConfirm = false
}
)
}
if viewModel.isShowPopup {
LiveRoomDialogView(
content: viewModel.popupContent,
cancelTitle: viewModel.popupCancelTitle,
cancelAction: viewModel.popupCancelAction,
confirmTitle: viewModel.popupConfirmTitle,
confirmAction: viewModel.popupConfirmAction
).onAppear {
if viewModel.popupConfirmTitle == nil && viewModel.popupConfirmAction == nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
viewModel.isShowPopup = false
viewModel.popupCancelTitle = nil
viewModel.popupCancelAction = nil
viewModel.popupConfirmTitle = nil
viewModel.popupConfirmAction = nil
}
}
}
}
}
if viewModel.isShowRouletteSettings {
RouletteSettingsView(isShowing: $viewModel.isShowRouletteSettings) { isActiveRoulette, message in
self.viewModel.setActiveRoulette(
isActiveRoulette: isActiveRoulette,
message: message
)
}
}
if let preview = viewModel.roulettePreview, viewModel.isShowRoulettePreview {
RoulettePreviewDialog(
isShowing: $viewModel.isShowRoulettePreview,
title: nil,
onClickSpin: { viewModel.spinRoulette() },
preview: preview
)
}
if viewModel.isShowRoulette {
RouletteViewDialog(isShowing: $viewModel.isShowRoulette, options: viewModel.rouletteItems, selectedOption: viewModel.rouletteSelectedItem) {
viewModel.sendRouletteDonation()
}
}
if viewModel.isLoading && viewModel.liveRoomInfo == nil {
LoadingView()
}
}
.ignoresSafeArea(.keyboard)
.edgesIgnoringSafeArea(keyboardHandler.keyboardHeight > 0 ? .bottom : .init())
.sheet(
isPresented: $viewModel.isShowShareView,
onDismiss: { viewModel.shareMessage = "" },
content: {
ActivityViewController(activityItems: [viewModel.shareMessage])
}
)
.sheet(isPresented: $viewModel.isShowPhotoPicker) {
ImagePicker(
isShowing: $viewModel.isShowPhotoPicker,
selectedImage: $viewModel.coverImage,
sourceType: .photoLibrary
)
}
.sheet(isPresented: $viewModel.isShowEditRoomInfoDialog) {
if let liveRoomInfo = viewModel.liveRoomInfo {
LiveRoomInfoEditDialog(
isShowing: $viewModel.isShowEditRoomInfoDialog,
isShowPhotoPicker: $viewModel.isShowPhotoPicker,
viewModel: viewModel,
isLoading: viewModel.isLoading,
currentTitle: liveRoomInfo.title,
currentNotice: liveRoomInfo.notice,
coverImageUrl: liveRoomInfo.coverImageUrl,
coverImage: viewModel.coverImage
) { newTitle, newNotice in
self.viewModel.editLiveRoomInfo(
title: newTitle,
notice: newNotice
)
}
} else {
EmptyView()
.onAppear {
viewModel.isShowEditRoomInfoDialog = false
}
}
}
.sheet(isPresented: $viewModel.isShowDonationRankingPopup) {
LiveRoomDonationRankingDialog(isShowing: $viewModel.isShowDonationRankingPopup)
}
.sheet(isPresented: $viewModel.isShowDonationMessagePopup) {
LiveRoomDonationMessageDialog(isShowing: $viewModel.isShowDonationMessagePopup)
}
}
private func estimatedHeight(for text: String, width: CGFloat) -> CGFloat {
let textView = UITextView(frame: CGRect(x: 0, y: 0, width: width, height: .greatestFiniteMagnitude))
textView.font = UIFont.systemFont(ofSize: 11.3)
textView.text = text
return textView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)).height
}
private func inviteSpeaker(peerId: Int) {
if viewModel.liveRoomInfo!.speakerList.count <= 4 {
viewModel.inviteSpeaker(peerId: peerId)
self.viewModel.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요."
self.viewModel.isShowPopup = true
} else {
viewModel.popupContent = "스피커 정원을 초과했습니다."
viewModel.isShowPopup = true
}
}
private var scrollObservableView: some View {
GeometryReader { proxy in
let offsetY = proxy.frame(in: .global).origin.y
Color.clear
.preference(
key: ScrollOffsetKey.self,
value: offsetY
)
.onAppear {
viewModel.setOriginOffset(offsetY)
}
}
.frame(height: 0)
}
struct ScrollOffsetKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
}
struct LiveRoomViewV2_Previews: PreviewProvider {
static var previews: some View {
LiveRoomViewV2()
}
}