feat(chat): 크리에이터 DM 시작을 추가한다
This commit is contained in:
@@ -139,6 +139,19 @@ class AppState: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func replaceCurrentAppStep(step: AppStep) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let route = self.navigationPath.popLast() {
|
||||||
|
self.routeStepMap.removeValue(forKey: route)
|
||||||
|
}
|
||||||
|
|
||||||
|
let route = AppRoute()
|
||||||
|
self.routeStepMap[route] = step
|
||||||
|
self.navigationPath.append(route)
|
||||||
|
self.appStep = step
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func back() {
|
func back() {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.liveDetailSheet != nil {
|
if self.liveDetailSheet != nil {
|
||||||
@@ -244,6 +257,10 @@ class AppState: ObservableObject {
|
|||||||
pendingMainChatFilter = filter
|
pendingMainChatFilter = filter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requestMainChatRefresh(filter: MainChatFilter) {
|
||||||
|
pendingMainChatFilter = filter
|
||||||
|
}
|
||||||
|
|
||||||
func consumePendingMainChatFilter() -> MainChatFilter? {
|
func consumePendingMainChatFilter() -> MainChatFilter? {
|
||||||
let filter = pendingMainChatFilter
|
let filter = pendingMainChatFilter
|
||||||
pendingMainChatFilter = nil
|
pendingMainChatFilter = nil
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ enum AppStep {
|
|||||||
|
|
||||||
case userCreatorChatCreator(creatorId: Int)
|
case userCreatorChatCreator(creatorId: Int)
|
||||||
|
|
||||||
|
case userCreatorChatRecipientSearch
|
||||||
|
|
||||||
case newCharacterAll
|
case newCharacterAll
|
||||||
|
|
||||||
case originalWorkDetail(originalId: Int)
|
case originalWorkDetail(originalId: Int)
|
||||||
|
|||||||
@@ -354,6 +354,9 @@ struct AppStepLayerView: View {
|
|||||||
case .userCreatorChatCreator(let creatorId):
|
case .userCreatorChatCreator(let creatorId):
|
||||||
UserCreatorChatRoomView(creatorId: creatorId)
|
UserCreatorChatRoomView(creatorId: creatorId)
|
||||||
|
|
||||||
|
case .userCreatorChatRecipientSearch:
|
||||||
|
UserCreatorChatRecipientSearchView()
|
||||||
|
|
||||||
case .newCharacterAll:
|
case .newCharacterAll:
|
||||||
NewCharacterListView()
|
NewCharacterListView()
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,48 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum UserCreatorChatRecipientSearch {
|
||||||
|
static var searchPlaceholder: String {
|
||||||
|
pick(ko: "팬 이름을 입력하세요", en: "Enter a fan name", ja: "ファンの名前を入力してください")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var sectionTitle: String {
|
||||||
|
pick(ko: "팔로워", en: "Followers", ja: "フォロワー")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var searchResultLabel: String {
|
||||||
|
pick(ko: "검색결과", en: "Search results", ja: "検索結果")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var emptyMessage: String {
|
||||||
|
pick(
|
||||||
|
ko: "사용자가 없어요.\n다른 이름으로 다시 검색해 주세요.",
|
||||||
|
en: "No users found.\nTry searching with another name.",
|
||||||
|
ja: "ユーザーがいません。\n別の名前で検索してください。"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var dialogTitle: String {
|
||||||
|
pick(ko: "메시지 보내기", en: "Send message", ja: "メッセージを送る")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func dialogMessage(_ nickname: String) -> String {
|
||||||
|
pick(
|
||||||
|
ko: "\(nickname)에게 메시지를 보낼까요?",
|
||||||
|
en: "Send a message to \(nickname)?",
|
||||||
|
ja: "\(nickname)にメッセージを送りますか?"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var sendButton: String {
|
||||||
|
pick(ko: "보내기", en: "Send", ja: "送信")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var openButtonAccessibilityLabel: String {
|
||||||
|
pick(ko: "메시지 보낼 사용자 선택", en: "Select user to message", ja: "メッセージを送るユーザーを選択")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 채팅방(캐릭터 톡) 관련 문자열
|
// 채팅방(캐릭터 톡) 관련 문자열
|
||||||
enum ChatRoom {
|
enum ChatRoom {
|
||||||
// 잠금된 메시지 다이얼로그
|
// 잠금된 메시지 다이얼로그
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ struct MainChatView: View {
|
|||||||
|
|
||||||
private func applyPendingFilterIfNeeded() -> Bool {
|
private func applyPendingFilterIfNeeded() -> Bool {
|
||||||
guard let filter = appState.consumePendingMainChatFilter() else { return false }
|
guard let filter = appState.consumePendingMainChatFilter() else { return false }
|
||||||
viewModel.applyFilter(filter)
|
viewModel.fetchFirstPage(filter: filter)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct UserCreatorCreateRoomRequest: Encodable {
|
struct UserCreatorCreateRoomRequest: Encodable {
|
||||||
let creatorId: Int
|
let recipientId: Int?
|
||||||
|
let creatorId: Int?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case recipientId
|
||||||
|
case creatorId
|
||||||
|
}
|
||||||
|
|
||||||
|
init(recipientId: Int? = nil, creatorId: Int? = nil) {
|
||||||
|
self.recipientId = recipientId
|
||||||
|
self.creatorId = creatorId
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encodeIfPresent(recipientId, forKey: .recipientId)
|
||||||
|
try container.encodeIfPresent(creatorId, forKey: .creatorId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UserCreatorCreateRoomResponse: Decodable {
|
struct UserCreatorCreateRoomResponse: Decodable {
|
||||||
|
|||||||
+4
@@ -10,6 +10,10 @@ final class UserCreatorChatRepository {
|
|||||||
return api.requestPublisher(.createRoom(request: UserCreatorCreateRoomRequest(creatorId: creatorId)))
|
return api.requestPublisher(.createRoom(request: UserCreatorCreateRoomRequest(creatorId: creatorId)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createRoom(recipientId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.createRoom(request: UserCreatorCreateRoomRequest(recipientId: recipientId)))
|
||||||
|
}
|
||||||
|
|
||||||
func openRoom(roomId: Int, limit: Int) -> AnyPublisher<Response, MoyaError> {
|
func openRoom(roomId: Int, limit: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.openRoom(roomId: roomId, limit: limit))
|
return api.requestPublisher(.openRoom(roomId: roomId, limit: limit))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
struct UserCreatorChatRoomView: View {
|
struct UserCreatorChatRoomView: View {
|
||||||
@StateObject private var viewModel = UserCreatorChatRoomViewModel()
|
@StateObject private var viewModel = UserCreatorChatRoomViewModel()
|
||||||
@@ -200,3 +201,388 @@ struct UserCreatorChatRoomView_Previews: PreviewProvider {
|
|||||||
UserCreatorChatRoomView(roomId: 1)
|
UserCreatorChatRoomView(roomId: 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct UserCreatorChatRecipientItem: Identifiable, Equatable {
|
||||||
|
let id: Int
|
||||||
|
let nickname: String
|
||||||
|
let profileImageUrl: String
|
||||||
|
|
||||||
|
init(follower item: GetFollowerListResponseItem) {
|
||||||
|
self.id = item.userId
|
||||||
|
self.nickname = item.nickname
|
||||||
|
self.profileImageUrl = item.profileImage
|
||||||
|
}
|
||||||
|
|
||||||
|
init(search item: GetRoomDetailUser) {
|
||||||
|
self.id = item.id
|
||||||
|
self.nickname = item.nickname
|
||||||
|
self.profileImageUrl = item.profileImageUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UserCreatorChatRecipientSearchViewModel: ObservableObject {
|
||||||
|
@Published var isLoading = false
|
||||||
|
@Published var errorMessage = ""
|
||||||
|
@Published var isShowPopup = false
|
||||||
|
@Published var searchText = ""
|
||||||
|
@Published private(set) var items = [UserCreatorChatRecipientItem]()
|
||||||
|
@Published private(set) var isSearchMode = false
|
||||||
|
@Published private(set) var isCreatingRoom = false
|
||||||
|
|
||||||
|
private let followRepository = ExplorerRepository()
|
||||||
|
private let userRepository = UserRepository()
|
||||||
|
private let chatRepository = UserCreatorChatRepository()
|
||||||
|
private var subscription = Set<AnyCancellable>()
|
||||||
|
private var searchSubscription: AnyCancellable?
|
||||||
|
private var followingItems = [UserCreatorChatRecipientItem]()
|
||||||
|
private var page = 1
|
||||||
|
private var isLast = false
|
||||||
|
private let pageSize = 10
|
||||||
|
private var isFetchingFollowing = false
|
||||||
|
private var isSearching = false
|
||||||
|
private var didCompleteSearch = false
|
||||||
|
private var activeSearchQuery: String?
|
||||||
|
|
||||||
|
var shouldShowEmptySearchResult: Bool {
|
||||||
|
isSearchMode && didCompleteSearch && !isSearching && items.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
$searchText
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.removeDuplicates()
|
||||||
|
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
|
||||||
|
.sink { [weak self] query in
|
||||||
|
self?.handleSearchQuery(query)
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchFirstPageIfNeeded() {
|
||||||
|
guard followingItems.isEmpty else { return }
|
||||||
|
fetchFollowing()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchNextPageIfNeeded(currentItem item: UserCreatorChatRecipientItem) {
|
||||||
|
guard !isSearchMode, item.id == followingItems.last?.id else { return }
|
||||||
|
fetchFollowing()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRoom(recipientId: Int) {
|
||||||
|
guard recipientId > 0, !isCreatingRoom else { return }
|
||||||
|
isCreatingRoom = true
|
||||||
|
updateLoading()
|
||||||
|
|
||||||
|
chatRepository.createRoom(recipientId: recipientId)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
guard let self else { return }
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self.applyCreateRoomFailure(message: nil)
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
do {
|
||||||
|
let decoded = try JSONDecoder().decode(ApiResponse<UserCreatorCreateRoomResponse>.self, from: response.data)
|
||||||
|
if let data = decoded.data, decoded.success, data.roomId > 0 {
|
||||||
|
self.isCreatingRoom = false
|
||||||
|
self.updateLoading()
|
||||||
|
AppState.shared.requestMainChatRefresh(filter: .dm)
|
||||||
|
AppState.shared.replaceCurrentAppStep(step: .userCreatorChatRoom(roomId: data.roomId))
|
||||||
|
} else {
|
||||||
|
self.applyCreateRoomFailure(message: decoded.message)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self.applyCreateRoomFailure(message: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleSearchQuery(_ query: String) {
|
||||||
|
guard query.count >= 2 else {
|
||||||
|
searchSubscription?.cancel()
|
||||||
|
activeSearchQuery = nil
|
||||||
|
isSearching = false
|
||||||
|
didCompleteSearch = false
|
||||||
|
isSearchMode = false
|
||||||
|
items = followingItems
|
||||||
|
updateLoading()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
search(query: query)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchFollowing() {
|
||||||
|
guard !isLast, !isFetchingFollowing else { return }
|
||||||
|
isFetchingFollowing = true
|
||||||
|
updateLoading()
|
||||||
|
|
||||||
|
followRepository.getFollowerList(userId: UserDefaults.int(forKey: .userId), page: page, size: pageSize)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
guard let self else { return }
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self.isFetchingFollowing = false
|
||||||
|
self.applyFailure(message: nil)
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
self.isFetchingFollowing = false
|
||||||
|
self.updateLoading()
|
||||||
|
do {
|
||||||
|
let decoded = try JSONDecoder().decode(ApiResponse<GetFollowerListResponse>.self, from: response.data)
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
let mappedItems = data.items.map(UserCreatorChatRecipientItem.init(follower:))
|
||||||
|
if self.page == 1 {
|
||||||
|
self.followingItems.removeAll()
|
||||||
|
}
|
||||||
|
self.followingItems.append(contentsOf: mappedItems)
|
||||||
|
self.items = self.isSearchMode ? self.items : self.followingItems
|
||||||
|
if data.items.isEmpty {
|
||||||
|
self.isLast = true
|
||||||
|
} else {
|
||||||
|
self.page += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.applyFailure(message: decoded.message)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self.applyFailure(message: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func search(query: String) {
|
||||||
|
isSearchMode = true
|
||||||
|
isSearching = true
|
||||||
|
didCompleteSearch = false
|
||||||
|
items.removeAll()
|
||||||
|
updateLoading()
|
||||||
|
searchSubscription?.cancel()
|
||||||
|
activeSearchQuery = query
|
||||||
|
|
||||||
|
searchSubscription = userRepository.searchUser(nickname: query)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
guard let self else { return }
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
guard self.activeSearchQuery == query else { return }
|
||||||
|
self.isSearching = false
|
||||||
|
self.applyFailure(message: nil)
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
guard self.activeSearchQuery == query else { return }
|
||||||
|
self.isSearching = false
|
||||||
|
self.updateLoading()
|
||||||
|
do {
|
||||||
|
let decoded = try JSONDecoder().decode(ApiResponse<[GetRoomDetailUser]>.self, from: response.data)
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
self.items = data.map(UserCreatorChatRecipientItem.init(search:))
|
||||||
|
self.didCompleteSearch = true
|
||||||
|
} else {
|
||||||
|
self.applyFailure(message: decoded.message)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
self.applyFailure(message: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyFailure(message: String?) {
|
||||||
|
updateLoading()
|
||||||
|
errorMessage = message ?? I18n.Common.commonError
|
||||||
|
isShowPopup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyCreateRoomFailure(message: String?) {
|
||||||
|
isCreatingRoom = false
|
||||||
|
applyFailure(message: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLoading() {
|
||||||
|
isLoading = isFetchingFollowing || isSearching || isCreatingRoom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserCreatorChatRecipientSearchView: View {
|
||||||
|
@StateObject private var viewModel = UserCreatorChatRecipientSearchViewModel()
|
||||||
|
@State private var selectedRecipient: UserCreatorChatRecipientItem?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
|
ZStack {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
searchBar
|
||||||
|
.padding(.top, SodaSpacing.s8)
|
||||||
|
|
||||||
|
listContent
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.simultaneousGesture(TapGesture().onEnded { hideKeyboard() })
|
||||||
|
}
|
||||||
|
.background(Color.black.ignoresSafeArea())
|
||||||
|
.onAppear { viewModel.fetchFirstPageIfNeeded() }
|
||||||
|
.sodaToast(
|
||||||
|
isPresented: $viewModel.isShowPopup,
|
||||||
|
message: viewModel.errorMessage,
|
||||||
|
autohideIn: 2
|
||||||
|
)
|
||||||
|
|
||||||
|
if let selectedRecipient {
|
||||||
|
confirmationDialog(for: selectedRecipient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var searchBar: some View {
|
||||||
|
HStack(spacing: SodaSpacing.s8) {
|
||||||
|
Image("ic_back")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
.onTapGesture { AppState.shared.back() }
|
||||||
|
|
||||||
|
HStack(spacing: SodaSpacing.s8) {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
|
||||||
|
TextField(I18n.UserCreatorChatRecipientSearch.searchPlaceholder, text: $viewModel.searchText)
|
||||||
|
.appFont(size: 14, weight: .regular)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.accentColor(Color.soda400)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, SodaSpacing.s12)
|
||||||
|
.frame(height: 42)
|
||||||
|
.background(Color.gray900)
|
||||||
|
.cornerRadius(21)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
.frame(width: screenSize().width, height: 54)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var listContent: some View {
|
||||||
|
if viewModel.shouldShowEmptySearchResult {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
sectionHeader
|
||||||
|
emptySearchContent
|
||||||
|
}
|
||||||
|
.padding(.top, SodaSpacing.s8)
|
||||||
|
} else {
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
LazyVStack(alignment: .leading, spacing: 0) {
|
||||||
|
sectionHeader
|
||||||
|
|
||||||
|
ForEach(viewModel.items) { item in
|
||||||
|
UserCreatorChatRecipientRow(item: item) {
|
||||||
|
hideKeyboard()
|
||||||
|
selectedRecipient = item
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModel.fetchNextPageIfNeeded(currentItem: item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, SodaSpacing.s8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var sectionHeader: some View {
|
||||||
|
if viewModel.isSearchMode {
|
||||||
|
HStack(spacing: SodaSpacing.s4) {
|
||||||
|
Text(I18n.UserCreatorChatRecipientSearch.searchResultLabel)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Text("\(viewModel.items.count)")
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
}
|
||||||
|
.appFont(size: 14, weight: .bold)
|
||||||
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 40, alignment: .leading)
|
||||||
|
} else {
|
||||||
|
Text(I18n.UserCreatorChatRecipientSearch.sectionTitle)
|
||||||
|
.appFont(size: 14, weight: .bold)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
.frame(height: 40, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emptySearchContent: some View {
|
||||||
|
Text(I18n.UserCreatorChatRecipientSearch.emptyMessage)
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func confirmationDialog(for item: UserCreatorChatRecipientItem) -> some View {
|
||||||
|
SodaV2ActionModal(
|
||||||
|
title: I18n.UserCreatorChatRecipientSearch.dialogTitle,
|
||||||
|
message: I18n.UserCreatorChatRecipientSearch.dialogMessage(item.nickname),
|
||||||
|
button1: SodaV2ActionModalButton(
|
||||||
|
label: I18n.UserCreatorChatRecipientSearch.sendButton,
|
||||||
|
action: {
|
||||||
|
selectedRecipient = nil
|
||||||
|
viewModel.createRoom(recipientId: item.id)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
button2: SodaV2ActionModalButton(
|
||||||
|
label: I18n.Common.cancel,
|
||||||
|
action: { selectedRecipient = nil }
|
||||||
|
),
|
||||||
|
onDimmedTap: { selectedRecipient = nil }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct UserCreatorChatRecipientRow: View {
|
||||||
|
let item: UserCreatorChatRecipientItem
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
HStack(spacing: SodaSpacing.s12) {
|
||||||
|
DownsampledKFImage(
|
||||||
|
url: URL(string: item.profileImageUrl),
|
||||||
|
size: CGSize(width: 42, height: 42)
|
||||||
|
)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
Text(item.nickname)
|
||||||
|
.appFont(size: 14, weight: .bold)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
.frame(height: 66)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,6 +124,26 @@ struct MainView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldShowChatRecipientSearchButton {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
CreatorChannelFloatingIconButton(
|
||||||
|
imageName: "ic_plus_no_bg",
|
||||||
|
backgroundColor: Color.soda400,
|
||||||
|
accessibilityLabel: I18n.UserCreatorChatRecipientSearch.openButtonAccessibilityLabel,
|
||||||
|
action: showUserCreatorChatRecipientSearch
|
||||||
|
)
|
||||||
|
.padding(.trailing, SodaSpacing.s14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(.container, edges: .bottom)
|
||||||
|
.padding(.bottom, chatRecipientSearchButtonBottomPadding)
|
||||||
|
}
|
||||||
|
|
||||||
if isShowPlayer {
|
if isShowPlayer {
|
||||||
ContentPlayerView(isShowing: $isShowPlayer, playlist: [])
|
ContentPlayerView(isShowing: $isShowPlayer, playlist: [])
|
||||||
}
|
}
|
||||||
@@ -161,6 +181,10 @@ struct MainView: View {
|
|||||||
UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue && viewModel.currentTab == .home
|
UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue && viewModel.currentTab == .home
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var shouldShowChatRecipientSearchButton: Bool {
|
||||||
|
UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue && viewModel.currentTab == .chat
|
||||||
|
}
|
||||||
|
|
||||||
private var creatorActionMenuBottomPadding: CGFloat {
|
private var creatorActionMenuBottomPadding: CGFloat {
|
||||||
var bottomPadding = mainTabBarHeight + SodaSpacing.s14
|
var bottomPadding = mainTabBarHeight + SodaSpacing.s14
|
||||||
if contentPlayerPlayManager.isShowingMiniPlayer {
|
if contentPlayerPlayManager.isShowingMiniPlayer {
|
||||||
@@ -172,6 +196,10 @@ struct MainView: View {
|
|||||||
return bottomPadding
|
return bottomPadding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var chatRecipientSearchButtonBottomPadding: CGFloat {
|
||||||
|
creatorActionMenuBottomPadding
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var contentView: some View {
|
private var contentView: some View {
|
||||||
switch viewModel.currentTab {
|
switch viewModel.currentTab {
|
||||||
@@ -686,6 +714,10 @@ struct MainView: View {
|
|||||||
appState.setAppStep(step: .createLive(timeSettingMode: .NOW, onSuccess: handleMainCreateLiveSuccess))
|
appState.setAppStep(step: .createLive(timeSettingMode: .NOW, onSuccess: handleMainCreateLiveSuccess))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func showUserCreatorChatRecipientSearch() {
|
||||||
|
appState.setAppStep(step: .userCreatorChatRecipientSearch)
|
||||||
|
}
|
||||||
|
|
||||||
private func handleMainCreateLiveSuccess(response: CreateLiveRoomResponse) {
|
private func handleMainCreateLiveSuccess(response: CreateLiveRoomResponse) {
|
||||||
liveViewModel.getLiveMain()
|
liveViewModel.getLiveMain()
|
||||||
if let _ = response.channelName {
|
if let _ = response.channelName {
|
||||||
|
|||||||
Reference in New Issue
Block a user