라이브 방

- 하트 후원 API 연결
- 하트 후원 성공시 하트 애니메이션 호출
- 하트 후원 성공시 채팅으로 알림
This commit is contained in:
Yu Sung 2024-10-24 18:13:56 +09:00
parent 9fa1bf9f64
commit 17b4516b87
12 changed files with 221 additions and 11 deletions

View File

@ -37,6 +37,8 @@ enum LiveApi {
case deleteDonationMessage(roomId: Int, messageUUID: String) case deleteDonationMessage(roomId: Int, messageUUID: String)
case getUserProfile(roomId: Int, userId: Int) case getUserProfile(roomId: Int, userId: Int)
case getAllMenuPreset(creatorId: Int) case getAllMenuPreset(creatorId: Int)
case likeHeart(request: LiveRoomLikeHeartRequest)
case getTotalHeartCount(roomId: Int)
} }
extension LiveApi: TargetType { extension LiveApi: TargetType {
@ -129,15 +131,21 @@ extension LiveApi: TargetType {
case .getAllMenuPreset: case .getAllMenuPreset:
return "/live/room/menu/all" return "/live/room/menu/all"
case .likeHeart:
return "/live/room/like-heart"
case .getTotalHeartCount(let roomId):
return "/live/room/\(roomId)/heart-total"
} }
} }
var method: Moya.Method { var method: Moya.Method {
switch self { switch self {
case .roomList, .recentVisitRoomUsers, .getReservations, .getReservation, .getRoomDetail, .getTags, .getRecentRoomInfo, .getRoomInfo, .donationStatus, .donationTotal, .getDonationMessageList, .getUserProfile, .getAllMenuPreset: case .roomList, .recentVisitRoomUsers, .getReservations, .getReservation, .getRoomDetail, .getTags, .getRecentRoomInfo, .getRoomInfo, .donationStatus, .donationTotal, .getDonationMessageList, .getUserProfile, .getAllMenuPreset, .getTotalHeartCount:
return .get return .get
case .makeReservation, .enterRoom, .createRoom, .quitRoom, .donation, .refundDonation, .kickOut: case .makeReservation, .enterRoom, .createRoom, .quitRoom, .donation, .refundDonation, .kickOut, .likeHeart:
return .post return .post
case .setListener, .setSpeaker, .setManager, .cancelReservation, .startLive, .cancelRoom, .editLiveRoomInfo: case .setListener, .setSpeaker, .setManager, .cancelReservation, .startLive, .cancelRoom, .editLiveRoomInfo:
@ -166,7 +174,7 @@ extension LiveApi: TargetType {
parameters: parameters, parameters: parameters,
encoding: URLEncoding.queryString) encoding: URLEncoding.queryString)
case .recentVisitRoomUsers, .getTags, .getRecentRoomInfo, .getRoomInfo, .refundDonation, .donationStatus, .donationTotal, .getUserProfile: case .recentVisitRoomUsers, .getTags, .getRecentRoomInfo, .getRoomInfo, .refundDonation, .donationStatus, .donationTotal, .getUserProfile, .getTotalHeartCount:
return .requestPlain return .requestPlain
case .getReservations(let isActive): case .getReservations(let isActive):
@ -233,6 +241,9 @@ extension LiveApi: TargetType {
case .getAllMenuPreset(let creatorId): case .getAllMenuPreset(let creatorId):
return .requestParameters(parameters: ["creatorId" : creatorId], encoding: URLEncoding.queryString) return .requestParameters(parameters: ["creatorId" : creatorId], encoding: URLEncoding.queryString)
case .likeHeart(let request):
return .requestJSONEncodable(request)
} }
} }

View File

@ -120,4 +120,12 @@ final class LiveRepository {
func getAllMenuPreset(creatorId: Int) -> AnyPublisher<Response, MoyaError> { func getAllMenuPreset(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getAllMenuPreset(creatorId: creatorId)) return api.requestPublisher(.getAllMenuPreset(creatorId: creatorId))
} }
func likeHeart(roomId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.likeHeart(request: LiveRoomLikeHeartRequest(roomId: roomId)))
}
func getTotalHeartCount(roomId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getTotalHeartCount(roomId: roomId))
}
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
enum LiveRoomChatType: String { enum LiveRoomChatType: String {
case CHAT, DONATION, JOIN, ROULETTE_DONATION case CHAT, DONATION, JOIN, ROULETTE_DONATION, HEART
} }
protocol LiveRoomChat { protocol LiveRoomChat {
@ -48,3 +48,9 @@ struct LiveRoomJoinChat: LiveRoomChat {
var type: LiveRoomChatType = .JOIN var type: LiveRoomChatType = .JOIN
} }
struct LiveRoomHeartDonationChat: LiveRoomChat {
let nickname: String
var type: LiveRoomChatType = .HEART
}

View File

@ -9,7 +9,7 @@ import Foundation
struct LiveRoomChatRawMessage: Codable { struct LiveRoomChatRawMessage: Codable {
enum LiveRoomChatRawMessageType: String, Codable { enum LiveRoomChatRawMessageType: String, Codable {
case DONATION, SECRET_DONATION, EDIT_ROOM_INFO, SET_MANAGER, TOGGLE_ROULETTE, ROULETTE_DONATION case DONATION, SECRET_DONATION, EDIT_ROOM_INFO, SET_MANAGER, TOGGLE_ROULETTE, ROULETTE_DONATION, HEART_DONATION
} }
let type: LiveRoomChatRawMessageType let type: LiveRoomChatRawMessageType

View File

@ -0,0 +1,38 @@
//
// LiveRoomHeartDonationChatItemView.swift
// SodaLive
//
// Created by klaus on 10/24/24.
//
import SwiftUI
struct LiveRoomHeartDonationChatItemView: View {
let chatMessage: LiveRoomHeartDonationChat
var body: some View {
HStack(spacing: 0) {
Text("'")
.font(.system(size: 12))
.foregroundColor(Color.gray11)
Text(chatMessage.nickname)
.font(.system(size: 12, weight: .bold))
.foregroundColor(Color(hex: "ec3aa6"))
Text("'님이 마음을 전했습니다 : 💕")
.font(.system(size: 12))
.foregroundColor(Color.gray11)
}
.padding(.vertical, 6.7)
.frame(width: screenSize().width - 86)
.background(Color.white.opacity(0.7))
.cornerRadius(4.7)
.padding(.leading, 20)
}
}
#Preview {
LiveRoomHeartDonationChatItemView(chatMessage: LiveRoomHeartDonationChat(nickname: "닉네임"))
}

View File

@ -118,6 +118,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
} }
@Published var totalDonationCan = 0 @Published var totalDonationCan = 0
@Published var totalHeartCount = 0
@Published var donationMessageList = [LiveRoomDonationMessage]() @Published var donationMessageList = [LiveRoomDonationMessage]()
@Published var donationMessageCount = 0 @Published var donationMessageCount = 0
@ -361,6 +362,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
) )
getTotalDonationCan() getTotalDonationCan()
getTotalHeartCount()
if data.isAdult && !UserDefaults.bool(forKey: .auth) { if data.isAdult && !UserDefaults.bool(forKey: .auth) {
changeIsAdult = true changeIsAdult = true
@ -1133,6 +1135,32 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
} }
} }
private func getTotalHeartCount() {
repository.getTotalHeartCount(roomId: AppState.shared.roomId)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<GetLiveRoomHeartTotalResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.totalHeartCount = data.totalHeartCount
}
} catch {
}
}
.store(in: &subscription)
}
func getTotalDonationCan() { func getTotalDonationCan() {
repository.getTotalDoantionCan(roomId: AppState.shared.roomId) repository.getTotalDoantionCan(roomId: AppState.shared.roomId)
.sink { result in .sink { result in
@ -1821,11 +1849,61 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
if isAvailableLikeHeart { if isAvailableLikeHeart {
if !isLoadingLikeHeart { if !isLoadingLikeHeart {
isLoadingLikeHeart = true isLoadingLikeHeart = true
addHeart()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [unowned self] in repository.likeHeart(roomId: AppState.shared.roomId)
self.isLoadingLikeHeart = false .sink { result in
} switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
self.isLoadingLikeHeart = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
if decoded.success {
UserDefaults.set(UserDefaults.int(forKey: .can) - 1, forKey: .can)
let donationRawMessage = LiveRoomChatRawMessage(
type: .HEART_DONATION,
message: "",
can: 1,
donationMessage: nil
)
agora.sendRawMessageToGroup(
rawMessage: donationRawMessage,
completion: { [unowned self] errorCode in
if errorCode == .errorOk {
let (nickname, _) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
self.messages.append(LiveRoomHeartDonationChat(nickname: nickname))
totalHeartCount += 1
addHeart()
self.messageChangeFlag.toggle()
if self.messages.count > 100 {
self.messages.remove(at: 0)
}
} else {
refundDonation()
}
},
fail: { [unowned self] in
refundDonation()
}
)
}
} catch {
refundDonation()
}
}
.store(in: &subscription)
} }
} else { } else {
isShowNoticeLikeHeart = true isShowNoticeLikeHeart = true
@ -2114,6 +2192,10 @@ extension LiveRoomViewModel: AgoraRtmChannelDelegate {
self.isActiveRoulette = decoded.isActiveRoulette! self.isActiveRoulette = decoded.isActiveRoulette!
} else if decoded.type == .EDIT_ROOM_INFO || decoded.type == .SET_MANAGER { } else if decoded.type == .EDIT_ROOM_INFO || decoded.type == .SET_MANAGER {
self.getRoomInfo() self.getRoomInfo()
} else if decoded.type == .HEART_DONATION {
self.messages.append(LiveRoomHeartDonationChat(nickname: nickname))
self.totalHeartCount += decoded.can
self.addHeart()
} }
} catch { } catch {
} }

View File

@ -28,6 +28,10 @@ struct LiveRoomChatView: View {
let chatMessage = messages[index] as! LiveRoomJoinChat let chatMessage = messages[index] as! LiveRoomJoinChat
LiveRoomJoinChatItemView(chatMessage: chatMessage) LiveRoomJoinChatItemView(chatMessage: chatMessage)
case LiveRoomChatType.HEART:
let chatMessage = messages[index] as! LiveRoomHeartDonationChat
LiveRoomHeartDonationChatItemView(chatMessage: chatMessage)
default: default:
let chatMessage = messages[index] as! LiveRoomNormalChat let chatMessage = messages[index] as! LiveRoomNormalChat
LiveRoomChatItemView( LiveRoomChatItemView(

View File

@ -10,6 +10,7 @@ import SwiftUI
struct LiveRoomInfoGuestView: View { struct LiveRoomInfoGuestView: View {
let title: String let title: String
let totalHeart: Int
let totalDonationCan: Int let totalDonationCan: Int
let isOnBg: Bool let isOnBg: Bool
@ -163,7 +164,23 @@ struct LiveRoomInfoGuestView: View {
Spacer() Spacer()
HStack(spacing: 2.7) { HStack(spacing: 6.7) {
Image("ic_heart_pink")
.resizable()
.frame(width: 12, height: 12)
Text("\(totalHeart)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(.graybb)
}
.padding(.horizontal, 11)
.padding(.vertical, 5.3)
.overlay(
RoundedRectangle(cornerRadius: 5.3)
.stroke(Color.graybb, lineWidth: 1)
)
HStack(spacing: 6.7) {
Image("ic_can") Image("ic_can")
.resizable() .resizable()
.frame(width: 12, height: 12) .frame(width: 12, height: 12)
@ -202,6 +219,7 @@ struct LiveRoomInfoGuestView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
LiveRoomInfoGuestView( LiveRoomInfoGuestView(
title: "qwer", title: "qwer",
totalHeart: 1234,
totalDonationCan: 123456, totalDonationCan: 123456,
isOnBg: true, isOnBg: true,
isOnNotice: false, isOnNotice: false,

View File

@ -11,6 +11,7 @@ import Kingfisher
struct LiveRoomInfoHostView: View { struct LiveRoomInfoHostView: View {
let title: String let title: String
let totalHeart: Int
let totalDonationCan: Int let totalDonationCan: Int
let participantsCount: Int let participantsCount: Int
@ -161,7 +162,23 @@ struct LiveRoomInfoHostView: View {
Spacer() Spacer()
HStack(spacing: 2.7) { HStack(spacing: 6.7) {
Image("ic_heart_pink")
.resizable()
.frame(width: 12, height: 12)
Text("\(totalHeart)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(.graybb)
}
.padding(.horizontal, 11)
.padding(.vertical, 5.3)
.overlay(
RoundedRectangle(cornerRadius: 5.3)
.stroke(Color.graybb, lineWidth: 1)
)
HStack(spacing: 6.7) {
Image("ic_can") Image("ic_can")
.resizable() .resizable()
.frame(width: 12, height: 12) .frame(width: 12, height: 12)
@ -217,6 +234,7 @@ struct LiveRoomInfoHostView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
LiveRoomInfoHostView( LiveRoomInfoHostView(
title: "오늘의 라이브방송은 OOO입니다.", title: "오늘의 라이브방송은 OOO입니다.",
totalHeart: 1234,
totalDonationCan: 123456, totalDonationCan: 123456,
participantsCount: 18, participantsCount: 18,
isOnBg: true, isOnBg: true,

View File

@ -0,0 +1,12 @@
//
// GetLiveRoomHeartTotalResponse.swift
// SodaLive
//
// Created by klaus on 10/24/24.
//
import Foundation
struct GetLiveRoomHeartTotalResponse: Decodable {
let totalHeartCount: Int
}

View File

@ -0,0 +1,11 @@
//
// LiveRoomLikeHeartRequest.swift
// SodaLive
//
// Created by klaus on 10/24/24.
//
struct LiveRoomLikeHeartRequest: Encodable {
let roomId: Int
let container: String = "ios"
}

View File

@ -25,6 +25,7 @@ struct LiveRoomViewV2: View {
if liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) { if liveRoomInfo.creatorId == UserDefaults.int(forKey: .userId) {
LiveRoomInfoHostView( LiveRoomInfoHostView(
title: liveRoomInfo.title, title: liveRoomInfo.title,
totalHeart: viewModel.totalHeartCount,
totalDonationCan: viewModel.totalDonationCan, totalDonationCan: viewModel.totalDonationCan,
participantsCount: liveRoomInfo.participantsCount, participantsCount: liveRoomInfo.participantsCount,
isOnBg: viewModel.isBgOn, isOnBg: viewModel.isBgOn,
@ -75,6 +76,7 @@ struct LiveRoomViewV2: View {
} else { } else {
LiveRoomInfoGuestView( LiveRoomInfoGuestView(
title: liveRoomInfo.title, title: liveRoomInfo.title,
totalHeart: viewModel.totalHeartCount,
totalDonationCan: viewModel.totalDonationCan, totalDonationCan: viewModel.totalDonationCan,
isOnBg: viewModel.isBgOn, isOnBg: viewModel.isBgOn,
isOnNotice: viewModel.isShowNotice, isOnNotice: viewModel.isShowNotice,