라이브 방 추가
This commit is contained in:
932
SodaLive/Sources/Live/Room/LiveRoomView.swift
Normal file
932
SodaLive/Sources/Live/Room/LiveRoomView.swift
Normal file
@@ -0,0 +1,932 @@
|
||||
//
|
||||
// LiveRoomView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2023/08/14.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import PopupView
|
||||
|
||||
struct LiveRoomView: View {
|
||||
@State private var isShowingNewChat = false
|
||||
@State private var isShowPhotoPicker = false
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible())
|
||||
]
|
||||
|
||||
let chatColumns = [GridItem(.flexible())]
|
||||
|
||||
@StateObject var keyboardHandler = KeyboardHandler()
|
||||
@StateObject var viewModel = LiveRoomViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black.edgesIgnoringSafeArea(.all)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 6.7) {
|
||||
Text(
|
||||
UserDefaults.int(forKey: .userId) == viewModel.liveRoomInfo?.creatorId ?
|
||||
"라이브 종료":
|
||||
"나가기"
|
||||
)
|
||||
.font(.custom(Font.medium.rawValue, size: 10))
|
||||
.foregroundColor(Color(hex: "ff5c49"))
|
||||
.padding(.horizontal, 14.3)
|
||||
.padding(.vertical, 8.3)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 13.3)
|
||||
.stroke(Color(hex: "ff5c49"), lineWidth: 1)
|
||||
)
|
||||
.onTapGesture {
|
||||
if let liveRoomInfo = viewModel.liveRoomInfo, liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) {
|
||||
viewModel.isShowLiveEndPopup = true
|
||||
} else {
|
||||
viewModel.isShowQuitPopup = true
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(viewModel.isBgOn ? "배경 ON" : "배경 OFF")
|
||||
.font(.custom(Font.medium.rawValue, size: 10))
|
||||
.foregroundColor(Color(hex: viewModel.isBgOn ? "9970ff" : "eeeeee"))
|
||||
.padding(.horizontal, 14.3)
|
||||
.padding(.vertical, 8.3)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 13.3)
|
||||
.stroke(Color(hex: viewModel.isBgOn ? "9970ff" : "bbbbbb"), lineWidth: 1)
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.isBgOn.toggle()
|
||||
}
|
||||
|
||||
HStack(spacing: 4.7) {
|
||||
Image("ic_share")
|
||||
.resizable()
|
||||
.frame(width: 16, height: 16)
|
||||
}
|
||||
.padding(.horizontal, 14.3)
|
||||
.padding(.vertical, 6)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 13.3)
|
||||
.stroke(Color(hex: "bbbbbb"), lineWidth: 1)
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.shareRoom()
|
||||
}
|
||||
|
||||
if let liveRoomInfo = viewModel.liveRoomInfo,
|
||||
liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) {
|
||||
HStack(spacing: 4.7) {
|
||||
Image("ic_edit")
|
||||
.resizable()
|
||||
.frame(width: 16, height: 16)
|
||||
}
|
||||
.padding(.horizontal, 14.3)
|
||||
.padding(.vertical, 6)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 13.3)
|
||||
.stroke(Color(hex: "bbbbbb"), lineWidth: 1)
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.isShowEditRoomInfoDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
if let liveRoomInfo = viewModel.liveRoomInfo {
|
||||
ZStack {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(liveRoomInfo.title)
|
||||
.font(.custom(Font.bold.rawValue, size: 15.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.lineLimit(1)
|
||||
.padding(.top, 16.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
LiveRoomTopCreatorView(
|
||||
nickname: liveRoomInfo.creatorNickname,
|
||||
profileImageUrl: liveRoomInfo.creatorProfileUrl,
|
||||
isFollowing: liveRoomInfo.isFollowing,
|
||||
onClickProfile: {
|
||||
if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) {
|
||||
viewModel.getUserProfile(userId: liveRoomInfo.creatorId)
|
||||
}
|
||||
},
|
||||
onClickFollow: {
|
||||
if $0 {
|
||||
viewModel.unRegisterNotification()
|
||||
} else {
|
||||
viewModel.registerNotification()
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(.top, 16.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color(hex: "909090").opacity(0.3))
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.top, 8)
|
||||
|
||||
NotificationView(liveRoomInfo: liveRoomInfo)
|
||||
}
|
||||
|
||||
if viewModel.isMute {
|
||||
Image("img_noti_mute")
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isShowNotice {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Text("[공지]")
|
||||
.font(.custom(Font.bold.rawValue, size: 11.3))
|
||||
.foregroundColor(.white)
|
||||
|
||||
AttributedTextView(
|
||||
attributedString: makeAttributedString(liveRoomInfo.notice),
|
||||
lineLimit: viewModel.isExpandNotice ? Int.max : 1
|
||||
) {
|
||||
UIApplication.shared.open($0)
|
||||
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineSpacing(6)
|
||||
}
|
||||
.padding(.horizontal, 26.7)
|
||||
.padding(.vertical, 13.3)
|
||||
.frame(width: screenSize().width, alignment: .leading)
|
||||
.background(Color(hex: "3d2a6c"))
|
||||
.padding(.top, 10)
|
||||
.onTapGesture {
|
||||
viewModel.isExpandNotice.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
if !viewModel.isSpeakerFold {
|
||||
HStack(spacing: 0) {
|
||||
Text("스피커")
|
||||
.font(.custom(Font.bold.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 20)
|
||||
.padding(.horizontal, 23.3)
|
||||
|
||||
LazyVGrid(columns: columns) {
|
||||
ForEach(liveRoomInfo.speakerList, id: \.self) { speaker in
|
||||
VStack(spacing: 6.7) {
|
||||
ZStack {
|
||||
KFImage(URL(string: speaker.profileImage))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 46.7, height: 46.7, alignment: .top)
|
||||
.clipShape(Circle())
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(
|
||||
Color(hex: "9970ff"),
|
||||
lineWidth: viewModel.activeSpeakers.contains(UInt(speaker.id)) ? 3 : 0
|
||||
)
|
||||
)
|
||||
|
||||
if viewModel.muteSpeakers.contains(UInt(speaker.id)) {
|
||||
Image("ic_mute")
|
||||
.resizable()
|
||||
.frame(width: 46.7, height: 46.7)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
if liveRoomInfo.creatorId == speaker.id {
|
||||
Image("ic_crown")
|
||||
.resizable()
|
||||
.frame(width: 16.7, height: 16.7)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.frame(width: 46.7, height: 46.7)
|
||||
|
||||
Text(speaker.nickname)
|
||||
.font(.custom(Font.light.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
.lineLimit(1)
|
||||
}
|
||||
.onTapGesture {
|
||||
viewModel.selectedProfile = speaker
|
||||
viewModel.isShowProfilePopup = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 16.7)
|
||||
.padding(.horizontal, 23.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 16.7)
|
||||
.frame(width: screenSize().width)
|
||||
.background(Color(hex: "222222"))
|
||||
.cornerRadius(16.7, corners: [.topLeft, .topRight])
|
||||
|
||||
ZStack(alignment: .top) {
|
||||
ScrollViewReader { proxy in
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
if let liveRoomInfo = viewModel.liveRoomInfo, viewModel.isBgOn {
|
||||
GeometryReader { proxy in
|
||||
KFImage(URL(string: liveRoomInfo.coverImageUrl))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center)
|
||||
.clipped()
|
||||
|
||||
Color.black.opacity(0.4)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
scrollObservableView
|
||||
ChatView()
|
||||
.frame(width: screenSize().width)
|
||||
}
|
||||
.rotationEffect(Angle(degrees: 180))
|
||||
.onTapGesture { hideKeyboard() }
|
||||
.onPreferenceChange(ScrollOffsetKey.self) {
|
||||
viewModel.setOffset($0)
|
||||
}
|
||||
|
||||
InputChatView {
|
||||
isShowingNewChat = false
|
||||
proxy.scrollTo(viewModel.messages.count - 1, anchor: .center)
|
||||
}.padding(.bottom, keyboardHandler.keyboardHeight > 0 ? 0 : 15)
|
||||
}
|
||||
|
||||
VStack(spacing: 13.3) {
|
||||
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)
|
||||
.padding(.bottom, 13.3)
|
||||
.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)
|
||||
.padding(.bottom, 13.3)
|
||||
.onTapGesture {
|
||||
viewModel.toggleSpeakerMute()
|
||||
}
|
||||
|
||||
if let liveRoomInfo = viewModel.liveRoomInfo, 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, 16.7)
|
||||
.padding(.bottom, 85)
|
||||
|
||||
if isShowingNewChat {
|
||||
NewChatView{
|
||||
isShowingNewChat = false
|
||||
proxy.scrollTo(viewModel.messages.count - 1, anchor: .center)
|
||||
}.padding(.bottom, 70)
|
||||
}
|
||||
}
|
||||
.frame(width: screenSize().width)
|
||||
.animation(nil)
|
||||
}
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 6.7) {
|
||||
Image(viewModel.isSpeakerFold ? "ic_live_detail_bottom" : "ic_live_detail_top")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Text(viewModel.isSpeakerFold ? "펼치기" : "접기")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
}
|
||||
.padding(.vertical, 6.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
.background(Color(hex: "222222"))
|
||||
.cornerRadius(10, corners: [.bottomLeft, .bottomRight])
|
||||
.onTapGesture {
|
||||
viewModel.isSpeakerFold.toggle()
|
||||
}
|
||||
}
|
||||
.background(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [Color(hex: "222222").opacity(0.95), Color.black.opacity(0.005)]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
).ignoresSafeArea()
|
||||
)
|
||||
}
|
||||
}
|
||||
.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(hex: "9970ff"))
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.center)
|
||||
.cornerRadius(20)
|
||||
.padding(.top, 66.7)
|
||||
Spacer()
|
||||
}
|
||||
.onDisappear {
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ZStack {
|
||||
if viewModel.isShowProfileList, let liveRoomInfo = viewModel.liveRoomInfo {
|
||||
LiveRoomProfilesDialogView(
|
||||
isShowing: $viewModel.isShowProfileList,
|
||||
viewModel: viewModel,
|
||||
roomInfo: liveRoomInfo,
|
||||
isShowRequestSpeaker: viewModel.role != .SPEAKER,
|
||||
onClickRequestSpeaker: {
|
||||
viewModel.requestSpeaker()
|
||||
},
|
||||
registerNotification: { viewModel.registerNotification() },
|
||||
unRegisterNotification: { viewModel.unRegisterNotification() },
|
||||
onClickProfile: {
|
||||
if $0 != UserDefaults.int(forKey: .userId) {
|
||||
viewModel.getUserProfile(userId: $0)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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.registerNotification(creatorId: $0, isGetUserProfile: true) },
|
||||
onClickUnFollow: { viewModel.unRegisterNotification(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
|
||||
}
|
||||
)
|
||||
.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(hex: "9970ff"))
|
||||
.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() }
|
||||
)
|
||||
}
|
||||
|
||||
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.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: $isShowPhotoPicker) {
|
||||
ImagePicker(
|
||||
isShowing: $isShowPhotoPicker,
|
||||
selectedImage: $viewModel.coverImage,
|
||||
sourceType: .photoLibrary
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.isShowEditRoomInfoDialog) {
|
||||
if let liveRoomInfo = viewModel.liveRoomInfo {
|
||||
LiveRoomInfoEditDialog(
|
||||
isShowing: $viewModel.isShowEditRoomInfoDialog,
|
||||
isShowPhotoPicker: $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)
|
||||
}
|
||||
}
|
||||
|
||||
func makeAttributedString(_ text: String) -> NSAttributedString {
|
||||
let attributedString = NSMutableAttributedString(string: text)
|
||||
|
||||
let urlRegex = try! NSRegularExpression(pattern: "\\b(https?://\\S+\\b|www\\.\\S+\\b)")
|
||||
let matches = urlRegex.matches(in: text, options: [], range: NSRange(text.startIndex..., in: text))
|
||||
|
||||
for match in matches {
|
||||
let url = (text as NSString).substring(with: match.range)
|
||||
if let detectedURL = URL(string: url) {
|
||||
attributedString.addAttribute(.link, value: detectedURL, range: match.range)
|
||||
}
|
||||
}
|
||||
|
||||
return attributedString
|
||||
}
|
||||
|
||||
|
||||
private func inviteSpeaker(peerId: Int) {
|
||||
if viewModel.liveRoomInfo!.speakerList.count <= 9 {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func NotificationView(liveRoomInfo: GetRoomInfoResponse) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(
|
||||
viewModel.isShowNotice ?
|
||||
"ic_notice_selected" :
|
||||
"ic_notice_normal"
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.isShowNotice.toggle()
|
||||
}
|
||||
.padding(.trailing, 10)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 4.7) {
|
||||
Image("ic_donation_status")
|
||||
.resizable()
|
||||
.frame(width: 16, height: 16)
|
||||
|
||||
Text("\(viewModel.totalDonationCan)")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
}
|
||||
.padding(.horizontal, 11.5)
|
||||
.padding(.vertical, 5.3)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12.8)
|
||||
.strokeBorder(lineWidth: 1)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.isShowDonationRankingPopup = true
|
||||
}
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("참여자")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "bbbbbb"))
|
||||
|
||||
Text("\(liveRoomInfo.participantsCount)")
|
||||
.font(.custom(Font.medium.rawValue, size: 12))
|
||||
.foregroundColor(Color(hex: "9970ff"))
|
||||
.padding(.leading, 6.7)
|
||||
}
|
||||
.padding(.horizontal, 11.5)
|
||||
.padding(.vertical, 7.3)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12.8)
|
||||
.strokeBorder(lineWidth: 1)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
)
|
||||
.onTapGesture {
|
||||
viewModel.isShowProfileList = true
|
||||
}
|
||||
}
|
||||
.padding(.top, 13.3)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func NewChatView(scrollToBottom: @escaping () -> Void) -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 6.7) {
|
||||
Image("ic_bottom_white")
|
||||
Text("새로운 채팅")
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 13.3)
|
||||
.background(Color(hex: "555555").opacity(0.8))
|
||||
.cornerRadius(16.7)
|
||||
.padding(.bottom, 13.3)
|
||||
.onTapGesture { scrollToBottom() }
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func InputChatView(scrollToBottom: @escaping () -> Void) -> some View {
|
||||
HStack(spacing: 0) {
|
||||
TextField("채팅을 입력하세요", text: $viewModel.chatMessage)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.accentColor(Color(hex: "9970ff"))
|
||||
.keyboardType(.default)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("btn_message_send")
|
||||
.resizable()
|
||||
.frame(width: 35, height: 35)
|
||||
.padding(6.7)
|
||||
.onTapGesture {
|
||||
viewModel.sendMessage()
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
.background(Color(hex: "232323"))
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.strokeBorder(lineWidth: 1)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
)
|
||||
.padding(13.3)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func ChatView() -> some View {
|
||||
LazyVGrid(columns: chatColumns, alignment: .leading, spacing: 20) {
|
||||
ForEach(0..<viewModel.messages.count, id: \.self) { index in
|
||||
switch (viewModel.messages[index].type) {
|
||||
case LiveRoomChatType.DONATION:
|
||||
let chatMessage = viewModel.messages[index] as! LiveRoomDonationChat
|
||||
LiveRoomDonationChatItemView(chatMessage: chatMessage)
|
||||
|
||||
case LiveRoomChatType.JOIN:
|
||||
let chatMessage = viewModel.messages[index] as! LiveRoomJoinChat
|
||||
LiveRoomJoinChatItemView(chatMessage: chatMessage)
|
||||
|
||||
default:
|
||||
let chatMessage = viewModel.messages[index] as! LiveRoomNormalChat
|
||||
LiveRoomChatItemView(
|
||||
chatMessage: chatMessage,
|
||||
onClickProfile: {
|
||||
if chatMessage.userId != UserDefaults.int(forKey: .userId) {
|
||||
viewModel.getUserProfile(userId: chatMessage.userId)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.rotationEffect(Angle(degrees: 180))
|
||||
.valueChanged(value: viewModel.messageChangeFlag) { _ in
|
||||
if viewModel.offset - viewModel.originOffset > (56.7 * 2) {
|
||||
isShowingNewChat = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveRoomView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LiveRoomView()
|
||||
}
|
||||
}
|
||||
|
||||
struct AttributedTextView: UIViewRepresentable {
|
||||
let attributedString: NSAttributedString
|
||||
let lineLimit: Int
|
||||
let onURLTapped: (URL) -> Void
|
||||
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let textView = UITextView()
|
||||
textView.isEditable = false
|
||||
textView.isSelectable = false
|
||||
textView.isScrollEnabled = false
|
||||
textView.backgroundColor = .clear
|
||||
textView.font = UIFont(name: Font.light.rawValue, size: 11.3)
|
||||
textView.textColor = .white
|
||||
textView.textContainer.lineFragmentPadding = 0
|
||||
textView.textContainerInset = .zero
|
||||
textView.textContainer.maximumNumberOfLines = lineLimit
|
||||
textView.textContainer.lineBreakMode = lineLimit == 1 ? .byTruncatingTail : .byWordWrapping
|
||||
textView.delegate = context.coordinator
|
||||
|
||||
textView.isUserInteractionEnabled = true
|
||||
|
||||
// Add tap gesture recognizer to handle URL tap events
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTapGesture(_:)))
|
||||
textView.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||
uiView.attributedText = attributedString
|
||||
uiView.textColor = UIColor.white
|
||||
uiView.textContainer.maximumNumberOfLines = lineLimit
|
||||
uiView.textContainer.lineBreakMode = lineLimit == 1 ? .byTruncatingTail : .byWordWrapping
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(onURLTapped: onURLTapped)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UITextViewDelegate {
|
||||
let onURLTapped: (URL) -> Void
|
||||
let linkAttributeName = NSAttributedString.Key.link.rawValue
|
||||
|
||||
init(onURLTapped: @escaping (URL) -> Void) {
|
||||
self.onURLTapped = onURLTapped
|
||||
}
|
||||
|
||||
@objc func handleTapGesture(_ gesture: UITapGestureRecognizer) {
|
||||
let textView = gesture.view as? UITextView
|
||||
let location = gesture.location(in: textView)
|
||||
|
||||
let layoutManager = textView?.layoutManager
|
||||
let characterIndex = layoutManager?.characterIndex(for: location, in: textView!.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
|
||||
|
||||
if characterIndex != NSNotFound {
|
||||
let attributedString = textView?.attributedText
|
||||
|
||||
attributedString?.enumerateAttribute(NSAttributedString.Key(rawValue: linkAttributeName), in: NSRange(location: 0, length: attributedString!.length), options: []) { value, range, _ in
|
||||
if let url = value as? URL, NSLocationInRange(characterIndex!, range) {
|
||||
onURLTapped(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user