feat(explorer): 채널 후원 목록/등록 기능을 추가한다
This commit is contained in:
@@ -75,7 +75,9 @@ enum AppStep {
|
||||
case followerList(userId: Int)
|
||||
|
||||
case userProfileDonationAll(userId: Int)
|
||||
|
||||
|
||||
case channelDonationAll(creatorId: Int)
|
||||
|
||||
case userProfileFanTalkAll(userId: Int)
|
||||
|
||||
case createLive(
|
||||
|
||||
@@ -21,6 +21,7 @@ enum DateParser {
|
||||
{ ISO8601.fractional.date(from: $0) },
|
||||
{ ISO8601.basic.date(from: $0) },
|
||||
{ DF.rfc3339.date(from: $0) },
|
||||
{ DF.isoLocalDateTime.date(from: $0) },
|
||||
{ DF.basic.date(from: $0) }
|
||||
]
|
||||
|
||||
@@ -56,5 +57,13 @@ enum DateParser {
|
||||
f.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return f
|
||||
}()
|
||||
|
||||
static let isoLocalDateTime: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.locale = Locale(identifier: "en_US_POSIX")
|
||||
f.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
|
||||
return f
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,22 @@ struct LiveRoomDonationDialogView: View {
|
||||
|
||||
@Binding var isShowing: Bool
|
||||
let isAudioContentDonation: Bool
|
||||
let messageLimit: Int
|
||||
let onClickDonation: (Int, String, Bool) -> Void
|
||||
|
||||
@StateObject var keyboardHandler = KeyboardHandler()
|
||||
|
||||
init(
|
||||
isShowing: Binding<Bool>,
|
||||
isAudioContentDonation: Bool,
|
||||
messageLimit: Int = 1000,
|
||||
onClickDonation: @escaping (Int, String, Bool) -> Void
|
||||
) {
|
||||
self._isShowing = isShowing
|
||||
self.isAudioContentDonation = isAudioContentDonation
|
||||
self.messageLimit = messageLimit
|
||||
self.onClickDonation = onClickDonation
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -204,7 +217,7 @@ struct LiveRoomDonationDialogView: View {
|
||||
.stroke(Color.graybb, lineWidth: 1)
|
||||
)
|
||||
|
||||
TextField("함께 보낼 \(isSecret ? "비밀 " : "")메시지 입력(최대 1000자)", text: $donationMessage)
|
||||
TextField("함께 보낼 \(isSecret ? "비밀 " : "")메시지 입력(최대 \(messageLimit)자)", text: $donationMessage)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(13.3)
|
||||
@@ -286,8 +299,8 @@ struct LiveRoomDonationDialogView: View {
|
||||
}
|
||||
|
||||
func limitText() {
|
||||
if donationMessage.count > 1000 {
|
||||
donationMessage = String(donationMessage.prefix(1000))
|
||||
if donationMessage.count > messageLimit {
|
||||
donationMessage = String(donationMessage.prefix(messageLimit))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,10 @@ struct ContentView: View {
|
||||
|
||||
case .userProfileDonationAll(let userId):
|
||||
UserProfileDonationAllView(userId: userId)
|
||||
|
||||
|
||||
case .channelDonationAll(let creatorId):
|
||||
ChannelDonationAllView(creatorId: creatorId)
|
||||
|
||||
case .userProfileFanTalkAll(let userId):
|
||||
UserProfileFanTalkAllView(userId: userId)
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ enum ExplorerApi {
|
||||
case modifyCheers(request: PutModifyCheersRequest)
|
||||
case writeCreatorNotice(request: PostCreatorNoticeRequest)
|
||||
case getCreatorProfileDonationRanking(userId: Int, page: Int, size: Int, period: DonationRankingPeriod?)
|
||||
case getChannelDonationList(creatorId: Int)
|
||||
case postChannelDonation(request: PostChannelDonationRequest)
|
||||
}
|
||||
|
||||
extension ExplorerApi: TargetType {
|
||||
@@ -61,15 +63,18 @@ extension ExplorerApi: TargetType {
|
||||
|
||||
case .writeCreatorNotice:
|
||||
return "/explorer/profile/notice"
|
||||
|
||||
case .getChannelDonationList, .postChannelDonation:
|
||||
return "/explorer/profile/channel-donation"
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .getExplorer, .searchChannel, .getCreatorProfile, .getCreatorDetail, .getFollowerList, .getCreatorProfileCheers, .getCreatorProfileDonationRanking, .getCreatorRank:
|
||||
case .getExplorer, .searchChannel, .getCreatorProfile, .getCreatorDetail, .getFollowerList, .getCreatorProfileCheers, .getCreatorProfileDonationRanking, .getCreatorRank, .getChannelDonationList:
|
||||
return .get
|
||||
|
||||
case .writeCheers, .writeCreatorNotice:
|
||||
|
||||
case .writeCheers, .writeCreatorNotice, .postChannelDonation:
|
||||
return .post
|
||||
|
||||
case .modifyCheers:
|
||||
@@ -115,7 +120,13 @@ extension ExplorerApi: TargetType {
|
||||
|
||||
case .writeCreatorNotice(let request):
|
||||
return .requestJSONEncodable(request)
|
||||
|
||||
|
||||
case .getChannelDonationList(let creatorId):
|
||||
return .requestParameters(parameters: ["creatorId": creatorId], encoding: URLEncoding.queryString)
|
||||
|
||||
case .postChannelDonation(let request):
|
||||
return .requestJSONEncodable(request)
|
||||
|
||||
case .getCreatorProfileDonationRanking(_, let page, let size, let period):
|
||||
var parameters = [
|
||||
"page": page - 1,
|
||||
|
||||
@@ -70,4 +70,12 @@ final class ExplorerRepository {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getChannelDonationList(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getChannelDonationList(creatorId: creatorId))
|
||||
}
|
||||
|
||||
func postChannelDonation(request: PostChannelDonationRequest) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.postChannelDonation(request: request))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ChannelDonationAllView: View {
|
||||
let creatorId: Int
|
||||
|
||||
@StateObject private var viewModel = ChannelDonationViewModel()
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 0) {
|
||||
DetailNavigationBar(title: I18n.MemberChannel.channelDonationAllTitle)
|
||||
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text(I18n.MemberChannel.totalLabel)
|
||||
.appFont(size: 14.7, weight: .medium)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
|
||||
Text("\(viewModel.totalCount)")
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(Color(hex: "80d8ff"))
|
||||
.padding(.leading, 6.7)
|
||||
|
||||
Text(I18n.MemberChannel.countUnit)
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(Color(hex: "777777"))
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 13.3)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
Rectangle()
|
||||
.frame(width: screenSize().width - 26.7, height: 1)
|
||||
.foregroundColor(Color(hex: "595959"))
|
||||
.padding(.top, 6.7)
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
if viewModel.donationItems.isEmpty {
|
||||
Text(I18n.MemberChannel.channelDonationEmpty)
|
||||
.appFont(size: 16, weight: .regular)
|
||||
.foregroundColor(Color(hex: "CFD8DC"))
|
||||
.padding(.top, 40)
|
||||
} else {
|
||||
LazyVStack(spacing: 12) {
|
||||
ForEach(0..<viewModel.donationItems.count, id: \.self) { index in
|
||||
let item = viewModel.donationItems[index]
|
||||
ChannelDonationItemView(
|
||||
item: item,
|
||||
previewLimit: 30,
|
||||
isShowFullMessageOnTap: true
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 16.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.setCreatorId(creatorId)
|
||||
}
|
||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(viewModel.errorMessage)
|
||||
.padding(.vertical, 13.3)
|
||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.background(Color.button)
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.cornerRadius(20)
|
||||
.padding(.bottom, 66.7)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ChannelDonationItemView: View {
|
||||
let item: GetChannelDonationListItem
|
||||
let previewLimit: Int?
|
||||
let isShowFullMessageOnTap: Bool
|
||||
|
||||
@State private var isExpanded = false
|
||||
|
||||
init(
|
||||
item: GetChannelDonationListItem,
|
||||
previewLimit: Int? = nil,
|
||||
isShowFullMessageOnTap: Bool = false
|
||||
) {
|
||||
self.item = item
|
||||
self.previewLimit = previewLimit
|
||||
self.isShowFullMessageOnTap = isShowFullMessageOnTap
|
||||
}
|
||||
|
||||
private var donationBackgroundColor: Color {
|
||||
if item.isSecret {
|
||||
return Color(hex: "59548f").opacity(0.8)
|
||||
}
|
||||
|
||||
if item.can >= 10000 {
|
||||
return Color(hex: "c25264").opacity(0.8)
|
||||
}
|
||||
|
||||
if item.can >= 5000 {
|
||||
return Color(hex: "d85e37").opacity(0.8)
|
||||
}
|
||||
|
||||
if item.can >= 1000 {
|
||||
return Color(hex: "d38c38").opacity(0.8)
|
||||
}
|
||||
|
||||
if item.can >= 500 {
|
||||
return Color(hex: "c25264").opacity(0.8)
|
||||
}
|
||||
|
||||
if item.can >= 100 {
|
||||
return Color(hex: "4d6aa4").opacity(0.8)
|
||||
}
|
||||
|
||||
if item.can >= 50 {
|
||||
return Color(hex: "2d7390").opacity(0.8)
|
||||
}
|
||||
|
||||
return Color(hex: "548f7d").opacity(0.8)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack(spacing: 11) {
|
||||
KFImage(URL(string: item.profileUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(size: CGSize(width: 40, height: 40))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 40, height: 40)
|
||||
.clipShape(Circle())
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(item.nickname)
|
||||
.appFont(size: 18, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(item.relativeTimeText())
|
||||
.appFont(size: 14, weight: .regular)
|
||||
.foregroundColor(Color(hex: "78909C"))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
highlightedMessageText(displayMessage)
|
||||
.appFont(size: 16, weight: .regular)
|
||||
.lineLimit(isExpanded ? nil : 2)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(donationBackgroundColor)
|
||||
.cornerRadius(16)
|
||||
.onTapGesture {
|
||||
guard isShowFullMessageOnTap else { return }
|
||||
guard isTruncated else { return }
|
||||
isExpanded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ChannelDonationItemView {
|
||||
var normalizedMessage: String {
|
||||
item.message.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
var displayMessage: String {
|
||||
guard !isExpanded else { return normalizedMessage }
|
||||
guard let previewLimit else { return normalizedMessage }
|
||||
|
||||
if normalizedMessage.count > previewLimit {
|
||||
return String(normalizedMessage.prefix(previewLimit)) + "..."
|
||||
}
|
||||
|
||||
return normalizedMessage
|
||||
}
|
||||
|
||||
var isTruncated: Bool {
|
||||
guard let previewLimit else { return false }
|
||||
return normalizedMessage.count > previewLimit
|
||||
}
|
||||
|
||||
func highlightedMessageText(_ message: String) -> Text {
|
||||
let plainCanToken = "\(item.can)캔"
|
||||
let commaCanToken = "\(item.can.comma())캔"
|
||||
|
||||
let range = message.range(of: commaCanToken)
|
||||
?? message.range(of: plainCanToken)
|
||||
|
||||
guard let range else {
|
||||
return Text(message).foregroundColor(Color(hex: "CFD8DC"))
|
||||
}
|
||||
|
||||
let prefixText = String(message[..<range.lowerBound])
|
||||
let canText = String(message[range])
|
||||
let suffixText = String(message[range.upperBound...])
|
||||
|
||||
return Text(prefixText).foregroundColor(Color(hex: "CFD8DC"))
|
||||
+ Text(canText).foregroundColor(Color(hex: "FDCA2F"))
|
||||
+ Text(suffixText).foregroundColor(Color(hex: "CFD8DC"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ChannelDonationViewModel: ObservableObject {
|
||||
private var repository = ExplorerRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var totalCount = 0
|
||||
@Published var donationItems: [GetChannelDonationListItem] = []
|
||||
|
||||
private var creatorId = 0
|
||||
|
||||
func setCreatorId(_ creatorId: Int, shouldFetch: Bool = true) {
|
||||
guard creatorId > 0 else { return }
|
||||
|
||||
if self.creatorId != creatorId {
|
||||
self.creatorId = creatorId
|
||||
}
|
||||
|
||||
if shouldFetch {
|
||||
getChannelDonationList()
|
||||
}
|
||||
}
|
||||
|
||||
func getChannelDonationList() {
|
||||
guard creatorId > 0, !isLoading else { return }
|
||||
|
||||
isLoading = true
|
||||
|
||||
repository.getChannelDonationList(creatorId: creatorId)
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
self.isLoading = false
|
||||
self.errorMessage = I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.isLoading = false
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<GetChannelDonationListResponse>.self, from: response.data)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.totalCount = data.totalCount
|
||||
self.donationItems = data.items
|
||||
} else {
|
||||
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.errorMessage = I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
}
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
|
||||
func postChannelDonation(
|
||||
can: Int,
|
||||
message: String,
|
||||
isSecret: Bool,
|
||||
reloadAfterSuccess: Bool = true,
|
||||
onSuccess: (() -> Void)? = nil
|
||||
) {
|
||||
guard creatorId > 0, !isLoading else { return }
|
||||
|
||||
isLoading = true
|
||||
|
||||
let request = PostChannelDonationRequest(
|
||||
creatorId: creatorId,
|
||||
can: can,
|
||||
isSecret: isSecret,
|
||||
message: message
|
||||
)
|
||||
|
||||
repository.postChannelDonation(request: request)
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
self.isLoading = false
|
||||
self.errorMessage = I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: response.data)
|
||||
|
||||
self.isLoading = false
|
||||
|
||||
if decoded.success {
|
||||
if reloadAfterSuccess {
|
||||
self.getChannelDonationList()
|
||||
} else {
|
||||
onSuccess?()
|
||||
}
|
||||
} else {
|
||||
self.errorMessage = decoded.message ?? I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
self.isLoading = false
|
||||
self.errorMessage = I18n.Common.commonError
|
||||
self.isShowPopup = true
|
||||
}
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// GetChannelDonationListResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/25/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GetChannelDonationListResponse: Decodable {
|
||||
let totalCount: Int
|
||||
let items: [GetChannelDonationListItem]
|
||||
}
|
||||
|
||||
struct GetChannelDonationListItem: Decodable {
|
||||
let id: Int
|
||||
let memberId: Int
|
||||
let nickname: String
|
||||
let profileUrl: String
|
||||
let can: Int
|
||||
let isSecret: Bool
|
||||
let message: String
|
||||
let createdAt: String
|
||||
}
|
||||
|
||||
extension GetChannelDonationListItem {
|
||||
func relativeTimeText(now: Date = Date()) -> String {
|
||||
guard let createdDate = DateParser.parse(createdAt) else {
|
||||
return createdAt
|
||||
}
|
||||
|
||||
let interval = max(0, now.timeIntervalSince(createdDate))
|
||||
|
||||
let calendar = Calendar.current
|
||||
let ym = calendar.dateComponents([.year, .month],
|
||||
from: createdDate,
|
||||
to: now)
|
||||
|
||||
if let years = ym.year, years >= 1 {
|
||||
return I18n.Time.yearsAgo(years)
|
||||
}
|
||||
|
||||
if let months = ym.month, months >= 1 {
|
||||
return I18n.Time.monthsAgo(months)
|
||||
}
|
||||
|
||||
if interval < 60 {
|
||||
return I18n.Time.justNow
|
||||
}
|
||||
|
||||
if interval < 3600 {
|
||||
let minutes = max(1, Int(interval / 60))
|
||||
return I18n.Time.minutesAgo(minutes)
|
||||
}
|
||||
|
||||
if interval < 86_400 {
|
||||
let hours = max(1, Int(interval / 3600))
|
||||
return I18n.Time.hoursAgo(hours)
|
||||
}
|
||||
|
||||
let days = max(1, Int(interval / 86_400))
|
||||
return I18n.Time.daysAgo(days)
|
||||
}
|
||||
|
||||
var messageBodyText: String {
|
||||
let trimmed = message.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
return I18n.MemberChannel.channelDonationDefaultMessage
|
||||
}
|
||||
|
||||
return " \(trimmed)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// PostChannelDonationRequest.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/25/26.
|
||||
//
|
||||
|
||||
struct PostChannelDonationRequest: Encodable {
|
||||
let creatorId: Int
|
||||
let can: Int
|
||||
var isSecret: Bool = false
|
||||
var message: String = ""
|
||||
var container: String = "ios"
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import SwiftUI
|
||||
|
||||
struct UserProfileChannelDonationView: View {
|
||||
let creatorId: Int
|
||||
let donationItems: [GetChannelDonationListItem]
|
||||
let onTapDonationButton: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack(spacing: 0) {
|
||||
Text(I18n.MemberChannel.channelDonationHeader)
|
||||
.appFont(size: 26, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
if !donationItems.isEmpty {
|
||||
Text(I18n.Common.viewAll)
|
||||
.appFont(size: 14, weight: .light)
|
||||
.foregroundColor(Color(hex: "78909C"))
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .channelDonationAll(creatorId: creatorId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
if donationItems.isEmpty {
|
||||
Text(I18n.MemberChannel.channelDonationEmpty)
|
||||
.appFont(size: 16, weight: .regular)
|
||||
.foregroundColor(Color(hex: "CFD8DC"))
|
||||
.padding(.horizontal, 24)
|
||||
} else {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: 14) {
|
||||
ForEach(0..<donationItems.count, id: \.self) { index in
|
||||
let item = donationItems[index]
|
||||
|
||||
ChannelDonationItemView(
|
||||
item: item,
|
||||
previewLimit: 30,
|
||||
isShowFullMessageOnTap: false
|
||||
)
|
||||
.frame(width: screenSize().width - 104)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 6.7) {
|
||||
Image("ic_donation_white")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
Text(I18n.MemberChannel.channelDonationButton)
|
||||
.appFont(size: 16, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(Color(hex: "525252"))
|
||||
.cornerRadius(16)
|
||||
.padding(.horizontal, 24)
|
||||
.onTapGesture {
|
||||
onTapDonationButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ struct GetCreatorProfileResponse: Decodable {
|
||||
let notice: String
|
||||
let communityPostList: [GetCommunityPostListResponse]
|
||||
let cheers: GetCheersResponse
|
||||
let activitySummary: GetCreatorActivitySummary
|
||||
let seriesList: [SeriesListItem]
|
||||
let channelDonationList: [GetChannelDonationListItem]
|
||||
let isBlock: Bool
|
||||
let isCreatorRole: Bool
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ struct UserProfileView: View {
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
|
||||
@StateObject var viewModel = UserProfileViewModel()
|
||||
@StateObject private var channelDonationViewModel = ChannelDonationViewModel()
|
||||
@StateObject private var keyboardHandler = KeyboardHandler()
|
||||
|
||||
@State private var memberId: Int = 0
|
||||
@@ -22,6 +23,7 @@ struct UserProfileView: View {
|
||||
@State private var isShowRouletteSettings: Bool = false
|
||||
@State private var isShowMenuSettings: Bool = false
|
||||
@State private var isShowCreatorDetailDialog: Bool = false
|
||||
@State private var isShowChannelDonationDialog: Bool = false
|
||||
|
||||
@State private var maxCommunityPostHeight: CGFloat? = nil
|
||||
|
||||
@@ -191,6 +193,15 @@ struct UserProfileView: View {
|
||||
.setAppStep(step: .contentDetail(contentId: item.contentId))
|
||||
}
|
||||
}
|
||||
|
||||
UserProfileChannelDonationView(
|
||||
creatorId: creatorProfile.creator.creatorId,
|
||||
donationItems: creatorProfile.channelDonationList,
|
||||
onTapDonationButton: {
|
||||
channelDonationViewModel.setCreatorId(creatorProfile.creator.creatorId, shouldFetch: false)
|
||||
isShowChannelDonationDialog = true
|
||||
}
|
||||
)
|
||||
|
||||
if creatorProfile.creator.creatorId == UserDefaults.int(forKey: .userId) || creatorProfile.liveRoomList.count > 0 {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
@@ -425,6 +436,25 @@ struct UserProfileView: View {
|
||||
}
|
||||
|
||||
ZStack {
|
||||
if isShowChannelDonationDialog {
|
||||
LiveRoomDonationDialogView(
|
||||
isShowing: $isShowChannelDonationDialog,
|
||||
isAudioContentDonation: false,
|
||||
messageLimit: 100,
|
||||
onClickDonation: { can, message, isSecret in
|
||||
channelDonationViewModel.postChannelDonation(
|
||||
can: can,
|
||||
message: message,
|
||||
isSecret: isSecret,
|
||||
reloadAfterSuccess: false,
|
||||
onSuccess: {
|
||||
viewModel.getCreatorProfile(userId: userId)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if viewModel.isShowPaymentDialog {
|
||||
LivePaymentDialog(
|
||||
title: viewModel.paymentDialogTitle,
|
||||
@@ -581,6 +611,21 @@ struct UserProfileView: View {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.popup(isPresented: $channelDonationViewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(channelDonationViewModel.errorMessage)
|
||||
.padding(.vertical, 13.3)
|
||||
.frame(width: screenSize().width - 66.7, alignment: .center)
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.background(Color.button)
|
||||
.foregroundColor(Color.white)
|
||||
.multilineTextAlignment(.leading)
|
||||
.cornerRadius(20)
|
||||
.padding(.bottom, 66.7)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.container, edges: .all)
|
||||
.sheet(
|
||||
isPresented: $viewModel.isShowShareView,
|
||||
|
||||
@@ -201,6 +201,30 @@ enum I18n {
|
||||
)
|
||||
}
|
||||
|
||||
static func minutesAgoCompact(_ minutes: Int) -> String {
|
||||
pick(
|
||||
ko: "\(minutes)분전",
|
||||
en: "\(minutes)m ago",
|
||||
ja: "\(minutes)分前"
|
||||
)
|
||||
}
|
||||
|
||||
static func hoursAgoCompact(_ hours: Int) -> String {
|
||||
pick(
|
||||
ko: "\(hours)시간전",
|
||||
en: "\(hours)h ago",
|
||||
ja: "\(hours)時間前"
|
||||
)
|
||||
}
|
||||
|
||||
static func daysAgoCompact(_ days: Int) -> String {
|
||||
pick(
|
||||
ko: "\(days)일전",
|
||||
en: "\(days)d ago",
|
||||
ja: "\(days)日前"
|
||||
)
|
||||
}
|
||||
|
||||
static func monthsAgo(_ months: Int) -> String {
|
||||
pick(
|
||||
ko: "\(months)개월 전",
|
||||
@@ -876,6 +900,14 @@ enum I18n {
|
||||
|
||||
static var cheersDeleteTitle: String { pick(ko: "응원글 삭제", en: "Delete Cheer", ja: "応援削除") }
|
||||
|
||||
static var channelDonationHeader: String { pick(ko: "채널 후원", en: "Channel Donation", ja: "チャンネル支援") }
|
||||
static var channelDonationButton: String { pick(ko: "채널 후원하기", en: "Donate to Channel", ja: "チャンネルを支援する") }
|
||||
static var channelDonationEmpty: String { pick(ko: "채널 후원이 없습니다.", en: "No channel donations.", ja: "チャンネル支援はありません。") }
|
||||
static var channelDonationAllTitle: String { pick(ko: "채널 후원 전체보기", en: "All Channel Donations", ja: "チャンネル支援一覧") }
|
||||
static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "全体") }
|
||||
static var countUnit: String { pick(ko: "개", en: "items", ja: "件") }
|
||||
static var channelDonationDefaultMessage: String { pick(ko: "을 후원했습니다.", en: " donated.", ja: "を支援しました。") }
|
||||
|
||||
static var liveHeader: String { pick(ko: "라이브", en: "Live", ja: "ライブ") }
|
||||
static var rouletteSettings: String { pick(ko: "룰렛 설정", en: "Roulette settings", ja: "ルーレット設定") }
|
||||
static var menuSettings: String { pick(ko: "메뉴 설정", en: "Menu settings", ja: "メニュー設定") }
|
||||
|
||||
Reference in New Issue
Block a user