feat(chat-room) 채팅방 API
- 채팅방 입장 API 연동 - 채팅 쿼터가 없을 때 표시할 UI 추가
This commit is contained in:
21
SodaLive/Resources/Assets.xcassets/ic_time.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/ic_time.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "ic_time.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
SodaLive/Resources/Assets.xcassets/ic_time.imageset/ic_time.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_time.imageset/ic_time.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 578 B |
@@ -35,4 +35,9 @@ class ChatRoomRepository {
|
|||||||
func sendMessage(roomId: Int, message: String) -> AnyPublisher<Response, MoyaError> {
|
func sendMessage(roomId: Int, message: String) -> AnyPublisher<Response, MoyaError> {
|
||||||
return talkApi.requestPublisher(.sendMessage(roomId: roomId, request: SendChatMessageRequest(message: message)))
|
return talkApi.requestPublisher(.sendMessage(roomId: roomId, request: SendChatMessageRequest(message: message)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 쿼터 상태 조회 */
|
||||||
|
func getChatQuotaStatus() -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return talkApi.requestPublisher(.getChatQuotaStatus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ struct ChatRoomView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
ChatRoomBgView()
|
ChatRoomBgView(url: viewModel.chatRoomBgImageUrl)
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
@@ -38,6 +38,7 @@ struct ChatRoomView: View {
|
|||||||
}
|
}
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 36, height: 36)
|
.frame(width: 36, height: 36)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(viewModel.characterName)
|
Text(viewModel.characterName)
|
||||||
@@ -116,11 +117,26 @@ struct ChatRoomView: View {
|
|||||||
let message = viewModel.messages[index]
|
let message = viewModel.messages[index]
|
||||||
if message.mine {
|
if message.mine {
|
||||||
UserMessageItemView(message: message)
|
UserMessageItemView(message: message)
|
||||||
|
.id(index)
|
||||||
} else {
|
} else {
|
||||||
AiMessageItemView(
|
AiMessageItemView(
|
||||||
message: message,
|
message: message,
|
||||||
characterName: viewModel.characterName
|
characterName: viewModel.characterName
|
||||||
)
|
)
|
||||||
|
.id(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.showQuotaNoticeView {
|
||||||
|
ChatQuotaNoticeItemView(remainingTime: viewModel.countdownText) {
|
||||||
|
|
||||||
|
}
|
||||||
|
.id(viewModel.messages.count)
|
||||||
|
.padding(.bottom, 12)
|
||||||
|
.onAppear {
|
||||||
|
withAnimation(.easeOut(duration: 0.3)) {
|
||||||
|
proxy.scrollTo(viewModel.messages.count, anchor: .bottom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,65 +155,69 @@ struct ChatRoomView: View {
|
|||||||
.frame(width: screenSize().width)
|
.frame(width: screenSize().width)
|
||||||
.frame(maxHeight: .infinity)
|
.frame(maxHeight: .infinity)
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
if !viewModel.showQuotaNoticeView {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 8) {
|
||||||
ZStack(alignment: .leading) {
|
HStack(spacing: 0) {
|
||||||
if viewModel.messageText.isEmpty {
|
ZStack(alignment: .leading) {
|
||||||
Text("메시지를 입력하세요.")
|
if viewModel.messageText.isEmpty {
|
||||||
.font(.custom(Font.preRegular.rawValue, size: 14))
|
Text("메시지를 입력하세요.")
|
||||||
.foregroundColor(Color(hex: "78909C"))
|
.font(.custom(Font.preRegular.rawValue, size: 14))
|
||||||
}
|
.foregroundColor(Color(hex: "78909C"))
|
||||||
|
|
||||||
TextField("", text: $viewModel.messageText)
|
|
||||||
.font(.custom(Font.preRegular.rawValue, size: 14))
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.onSubmit {
|
|
||||||
viewModel.sendMessage()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.vertical, 13)
|
|
||||||
.background(Color(hex: "263238"))
|
|
||||||
.cornerRadius(999)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 999)
|
|
||||||
.stroke(Color(hex: "263238"), lineWidth: 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
Button(action: {
|
TextField("", text: $viewModel.messageText)
|
||||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
.font(.custom(Font.preRegular.rawValue, size: 14))
|
||||||
viewModel.sendMessage()
|
.foregroundColor(.white)
|
||||||
}) {
|
.onSubmit {
|
||||||
Image("ic_message_send")
|
viewModel.sendMessage()
|
||||||
.resizable()
|
}
|
||||||
.frame(width: 24, height: 24)
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 13)
|
||||||
|
.background(Color(hex: "263238"))
|
||||||
|
.cornerRadius(999)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 999)
|
||||||
|
.stroke(Color(hex: "263238"), lineWidth: 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
viewModel.sendMessage()
|
||||||
|
}) {
|
||||||
|
Image("ic_message_send")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.frame(width: screenSize().width)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.frame(width: screenSize().width)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModel.enterRoom(roomId: roomId)
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
viewModel.stopTimer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChatRoomBgView: View {
|
struct ChatRoomBgView: View {
|
||||||
|
|
||||||
let url: String? = nil
|
let url: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if let url = url {
|
if let url = url {
|
||||||
KFImage(URL(string: url))
|
KFImage(URL(string: url))
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.aspectRatio(4/5, contentMode: .fill)
|
||||||
.ignoresSafeArea()
|
.frame(maxWidth: screenSize().width)
|
||||||
} else {
|
|
||||||
Image("img_sample")
|
|
||||||
.resizable()
|
|
||||||
.scaledToFill()
|
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ final class ChatRoomViewModel: ObservableObject {
|
|||||||
@Published private(set) var characterProfileUrl: String = ""
|
@Published private(set) var characterProfileUrl: String = ""
|
||||||
@Published private(set) var characterName: String = "Character Name"
|
@Published private(set) var characterName: String = "Character Name"
|
||||||
@Published private(set) var characterType: CharacterType = .Character
|
@Published private(set) var characterType: CharacterType = .Character
|
||||||
|
@Published private(set) var chatRoomBgImageUrl: String? = nil
|
||||||
|
@Published private(set) var roomId: Int = 0
|
||||||
|
|
||||||
|
@Published private(set) var countdownText: String = "00:00:00"
|
||||||
|
@Published private(set) var showQuotaNoticeView: Bool = false
|
||||||
|
|
||||||
// MARK: - Message State
|
// MARK: - Message State
|
||||||
@Published var messageText: String = ""
|
@Published var messageText: String = ""
|
||||||
@@ -28,6 +33,11 @@ final class ChatRoomViewModel: ObservableObject {
|
|||||||
private let repository = ChatRoomRepository()
|
private let repository = ChatRoomRepository()
|
||||||
private var subscription = Set<AnyCancellable>()
|
private var subscription = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
private var hasMoreMessages: Bool = true
|
||||||
|
private var nextCursor: Int64? = nil
|
||||||
|
|
||||||
|
private var timer: Timer?
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
@MainActor
|
@MainActor
|
||||||
func sendMessage() {
|
func sendMessage() {
|
||||||
@@ -41,4 +51,188 @@ final class ChatRoomViewModel: ObservableObject {
|
|||||||
// TODO: 실제 메시지 전송 로직 구현
|
// TODO: 실제 메시지 전송 로직 구현
|
||||||
DEBUG_LOG("메시지 전송: \(message)")
|
DEBUG_LOG("메시지 전송: \(message)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func enterRoom(roomId: Int) {
|
||||||
|
isLoading = true
|
||||||
|
self.roomId = roomId
|
||||||
|
|
||||||
|
repository.enterChatRoom(
|
||||||
|
roomId: roomId,
|
||||||
|
characterImageId: getSavedBackgroundImageId()
|
||||||
|
)
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
let responseData = response.data
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponse<ChatRoomEnterResponse>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
self?.characterName = data.character.name
|
||||||
|
self?.characterType = data.character.characterType
|
||||||
|
self?.characterProfileUrl = data.character.profileImageUrl
|
||||||
|
|
||||||
|
self?.chatRoomBgImageUrl = data.bgImageUrl ?? data.character.profileImageUrl
|
||||||
|
self?.messages.insert(contentsOf: data.messages.sorted(), at: 0)
|
||||||
|
|
||||||
|
self?.hasMoreMessages = data.hasMoreMessages
|
||||||
|
self?.nextCursor = data.messages.last?.messageId
|
||||||
|
|
||||||
|
self?.updateQuota(totalRemaining: data.totalRemaining, nextRechargeAtEpoch: data.nextRechargeAtEpoch)
|
||||||
|
} else {
|
||||||
|
if let message = decoded.message {
|
||||||
|
self?.errorMessage = message
|
||||||
|
} else {
|
||||||
|
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
}
|
||||||
|
|
||||||
|
self?.isShowPopup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self?.isLoading = false
|
||||||
|
} catch {
|
||||||
|
self?.isLoading = false
|
||||||
|
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self?.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
func purchaseChatQuota() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkQuotaStatus() {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
repository.getChatQuotaStatus()
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
let responseData = response.data
|
||||||
|
|
||||||
|
DEBUG_LOG(String(data: responseData, encoding: .utf8) ?? "")
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponse<ChatQuotaStatusResponse>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
self?.updateQuota(totalRemaining: data.totalRemaining, nextRechargeAtEpoch: data.nextRechargeAtEpoch)
|
||||||
|
} else {
|
||||||
|
if let message = decoded.message {
|
||||||
|
self?.errorMessage = message
|
||||||
|
} else {
|
||||||
|
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
}
|
||||||
|
|
||||||
|
self?.isShowPopup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self?.isLoading = false
|
||||||
|
} catch {
|
||||||
|
self?.isLoading = false
|
||||||
|
self?.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self?.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateQuota(totalRemaining: Int, nextRechargeAtEpoch: Int64?) {
|
||||||
|
isLoading = true
|
||||||
|
stopTimer()
|
||||||
|
|
||||||
|
// epoch 없음 → 카운트다운 비표시
|
||||||
|
guard let nextRechargeAtEpoch else {
|
||||||
|
countdownText = "00:00:00"
|
||||||
|
showQuotaNoticeView = false
|
||||||
|
isLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 즉시 1회 갱신
|
||||||
|
let remainMs = remainingMs(to: nextRechargeAtEpoch)
|
||||||
|
updateCountdownText(remainMs)
|
||||||
|
|
||||||
|
// 이미 0이면 종료 처리
|
||||||
|
guard remainMs > 0 else {
|
||||||
|
checkQuotaStatus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
showQuotaNoticeView = true
|
||||||
|
|
||||||
|
// 타이머 시작 (1초마다 갱신)
|
||||||
|
startTimer(targetEpoch: nextRechargeAtEpoch)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCountdownText(_ remainMs: Int64) {
|
||||||
|
countdownText = remainMs > 0 ? formatMillisToHms(remainMs) : "00:00:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startTimer(targetEpoch: Int64) {
|
||||||
|
stopTimer()
|
||||||
|
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
let remain = self.remainingMs(to: targetEpoch)
|
||||||
|
self.updateCountdownText(remain)
|
||||||
|
if remain == 0 {
|
||||||
|
self.stopTimer()
|
||||||
|
self.checkQuotaStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let t = timer { RunLoop.main.add(t, forMode: .common) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopTimer() {
|
||||||
|
timer?.invalidate()
|
||||||
|
timer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func remainingMs(to epoch: Int64) -> Int64 {
|
||||||
|
let ms = normalizeToMs(epoch)
|
||||||
|
let nowMs = Int64(Date().timeIntervalSince1970 * 1000)
|
||||||
|
let fudgeMs: Int64 = 5000
|
||||||
|
|
||||||
|
// Kotlin 로직과 동일하게 표시 보정 적용
|
||||||
|
return max(ms - nowMs + fudgeMs, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 초 단위/밀리초 단위 혼용 대비
|
||||||
|
private func normalizeToMs(_ epoch: Int64) -> Int64 {
|
||||||
|
epoch < 1_000_000_000_000 ? epoch * 1000 : epoch
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatMillisToHms(_ ms: Int64) -> String {
|
||||||
|
let total = ms / 1000
|
||||||
|
let h = total / 3600
|
||||||
|
let m = (total % 3600) / 60
|
||||||
|
let s = total % 60
|
||||||
|
return String(format: "%02d:%02d:%02d", h, m, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getSavedBackgroundImageId() -> Int? {
|
||||||
|
let imageId = UserDefaults.standard.integer(forKey: bgImageIdKey())
|
||||||
|
return imageId > 0 ? imageId : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bgImageIdKey() -> String {
|
||||||
|
return "chat_bg_image_id_room_\(roomId)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// Created by klaus on 9/2/25.
|
// Created by klaus on 9/2/25.
|
||||||
//
|
//
|
||||||
|
|
||||||
struct ServerChatMessage: Decodable {
|
struct ServerChatMessage: Decodable, Comparable {
|
||||||
let messageId: Int64
|
let messageId: Int64
|
||||||
let message: String
|
let message: String
|
||||||
let profileImageUrl: String
|
let profileImageUrl: String
|
||||||
@@ -15,4 +15,12 @@ struct ServerChatMessage: Decodable {
|
|||||||
let imageUrl: String?
|
let imageUrl: String?
|
||||||
let price: Int?
|
let price: Int?
|
||||||
let hasAccess: Bool
|
let hasAccess: Bool
|
||||||
|
|
||||||
|
static func < (lhs: ServerChatMessage, rhs: ServerChatMessage) -> Bool {
|
||||||
|
if lhs.createdAt == rhs.createdAt {
|
||||||
|
return lhs.messageId < rhs.messageId
|
||||||
|
} else {
|
||||||
|
return lhs.createdAt < rhs.createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,58 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ChatQuotaNoticeItemView: View {
|
struct ChatQuotaNoticeItemView: View {
|
||||||
|
|
||||||
|
let remainingTime: String
|
||||||
|
let purchase: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
VStack(spacing: 10) {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
Image("ic_time")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 30, height: 30)
|
||||||
|
|
||||||
|
Text(remainingTime)
|
||||||
|
.font(.custom(Font.preBold.rawValue, size: 18))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
Text("기다리면 무료 이용이 가능합니다.")
|
||||||
|
.font(.custom(Font.preBold.rawValue, size: 18))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 15)
|
||||||
|
.background(Color(hex: "EC8280"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image("ic_can")
|
||||||
|
|
||||||
|
Text("30")
|
||||||
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
||||||
|
.foregroundColor(Color(hex: "263238"))
|
||||||
|
|
||||||
|
Text("결제하고 바로 대화 시작")
|
||||||
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
||||||
|
.foregroundColor(Color(hex: "263238"))
|
||||||
|
.padding(.leading, 4)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 12)
|
||||||
|
.background(Color(hex: "B5E7FA"))
|
||||||
|
.cornerRadius(30)
|
||||||
|
.overlay {
|
||||||
|
RoundedRectangle(cornerRadius: 30)
|
||||||
|
.stroke(lineWidth: 1)
|
||||||
|
.foregroundColor(Color.button)
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
purchase()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ChatQuotaNoticeItemView()
|
ChatQuotaNoticeItemView(remainingTime: "05:59:55") {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
|
|
||||||
struct ChatQuotaStatusResponse: Decodable {
|
struct ChatQuotaStatusResponse: Decodable {
|
||||||
let totalRemaining: Int
|
let totalRemaining: Int
|
||||||
let nextRechargeAtEpoch: Int64
|
let nextRechargeAtEpoch: Int64?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ enum TalkApi {
|
|||||||
case enterChatRoom(roomId: Int, characterImageId: Int?)
|
case enterChatRoom(roomId: Int, characterImageId: Int?)
|
||||||
case sendMessage(roomId: Int, request: SendChatMessageRequest)
|
case sendMessage(roomId: Int, request: SendChatMessageRequest)
|
||||||
case getChatRoomMessages(roomId: Int, cursor: Int?, limit: Int)
|
case getChatRoomMessages(roomId: Int, cursor: Int?, limit: Int)
|
||||||
|
|
||||||
|
case getChatQuotaStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TalkApi: TargetType {
|
extension TalkApi: TargetType {
|
||||||
@@ -35,6 +37,9 @@ extension TalkApi: TargetType {
|
|||||||
|
|
||||||
case .getChatRoomMessages(let roomId, _, _):
|
case .getChatRoomMessages(let roomId, _, _):
|
||||||
return "/api/chat/room/\(roomId)/messages"
|
return "/api/chat/room/\(roomId)/messages"
|
||||||
|
|
||||||
|
case .getChatQuotaStatus:
|
||||||
|
return "/api/chat/quota/me"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +59,9 @@ extension TalkApi: TargetType {
|
|||||||
|
|
||||||
case .getChatRoomMessages:
|
case .getChatRoomMessages:
|
||||||
return .get
|
return .get
|
||||||
|
|
||||||
|
case .getChatQuotaStatus:
|
||||||
|
return .get
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +99,9 @@ extension TalkApi: TargetType {
|
|||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
encoding: URLEncoding.queryString
|
encoding: URLEncoding.queryString
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case .getChatQuotaStatus:
|
||||||
|
return .requestPlain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user