메시지 - 리스트, 쓰기, 상세 페이지 추가

This commit is contained in:
Yu Sung
2023-08-10 15:21:08 +09:00
parent 943e1d9f7f
commit 80ff04f825
36 changed files with 2969 additions and 2 deletions

View File

@@ -0,0 +1,195 @@
//
// TextMessageDetailView.swift
// SodaLive
//
// Created by klaus on 2023/08/10.
//
import SwiftUI
import Kingfisher
struct TextMessageDetailView: View {
@StateObject var viewModel = TextMessageDetailViewModel()
@StateObject var appState = AppState.shared
let messageItem: TextMessageItem
let messageBox: MessageFilterTab
let refresh: () -> Void
func back() {
refresh()
AppState.shared.back()
}
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
switch messageBox {
case .receive:
DetailNavigationBar(title: "받은 메시지 상세") { back() }
case .sent:
DetailNavigationBar(title: "보낸 메시지 상세") { back() }
case .keep:
DetailNavigationBar(title: "저장한 메시지 상세") { back() }
}
HStack(spacing: 13.3) {
KFImage(
URL(
string: messageBox == .sent ?
messageItem.recipientProfileImageUrl :
messageItem.senderProfileImageUrl
)
)
.resizable()
.scaledToFill()
.frame(width: 26.7, height: 26.7, alignment: .top)
.cornerRadius(13.3)
Text(
messageBox == .sent ?
messageItem.recipientNickname :
messageItem.senderNickname
)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
}
.padding(.vertical, 12.7)
.frame(width: screenSize().width - 26.7)
.background(Color(hex: "1b1b1b"))
.cornerRadius(10)
Text(messageItem.date.convertDateFormat(
from: "yyyy-MM-dd hh:mm:ss",
to: "yyyy년 MM월 dd일 E요일 HH:mm"
))
.font(.custom(Font.medium.rawValue, size: 15))
.foregroundColor(Color(hex: "bbbbbb"))
.padding(.top, 16.7)
ScrollView(.vertical, showsIndicators: false) {
Text(messageItem.textMessage)
.font(.custom(Font.medium.rawValue, size: 15))
.foregroundColor(Color(hex: "eeeeee"))
.multilineTextAlignment(.leading)
.padding(26.7)
.frame(width: screenSize().width - 26.7, alignment: .leading)
}
.frame(width: screenSize().width - 26.7)
.background(Color(hex: "222222"))
.cornerRadius(10)
.padding(.top, 10)
Spacer()
if messageBox == .receive {
HStack(spacing: 6.7) {
Text("답장")
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee"))
.frame(
width: (screenSize().width - 40) / 3,
height: 48.7
)
.background(Color(hex: "9970ff"))
.cornerRadius(6.7)
.onTapGesture {
AppState.shared.setAppStep(step: .writeTextMessage(userId: messageItem.senderId, nickname: messageItem.senderNickname))
}
Text("보관")
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color(hex: "9970ff"))
.frame(
width: (screenSize().width - 40) / 3,
height: 48.7
)
.background(Color(hex: "1f1734"))
.cornerRadius(6.7)
.onTapGesture {
if messageItem.isKept {
viewModel.errorMessage = "이미 보관된 메시지 입니다"
viewModel.isShowPopup = true
return
} else {
viewModel.keepTextMessage()
}
}
Text("삭제")
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color(hex: "9970ff"))
.frame(
width: (screenSize().width - 40) / 3,
height: 48.7
)
.background(Color(hex: "1f1734"))
.cornerRadius(6.7)
.onTapGesture {
viewModel.deleteMessage { back() }
}
}
.frame(width: screenSize().width - 26.7)
.padding(.vertical, 26.7)
} else {
Text("삭제")
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color(hex: "9970ff"))
.frame(
width: screenSize().width - 26.7,
height: 48.7
)
.background(Color(hex: "1f1734"))
.cornerRadius(6.7)
.onTapGesture {
viewModel.deleteMessage { back() }
}
.padding(.vertical, 26.7)
}
}
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.padding(.horizontal, 6.7)
.frame(width: geo.size.width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
.onAppear {
viewModel.messageId = messageItem.messageId
}
}
}
struct TextMessageDetailView_Previews: PreviewProvider {
static var previews: some View {
TextMessageDetailView(
messageItem: TextMessageItem(
messageId: 10,
senderId: 1,
senderNickname: "누군가",
senderProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
recipientNickname: "테스터",
recipientProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
textMessage: "testtesttesttest",
date: "2022-07-08 10:20:30",
isKept: false),
messageBox: .receive,
refresh: {}
)
}
}

View File

@@ -0,0 +1,118 @@
//
// TextMessageDetailViewModel.swift
// SodaLive
//
// Created by klaus on 2023/08/10.
//
import Foundation
import Combine
final class TextMessageDetailViewModel: ObservableObject {
private let repository = MessageRepository()
private var subscription = Set<AnyCancellable>()
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var isLoading = false
@Published var saveMessagePrice = 0
@Published var isShowSavePopup = false
var messageId = 0
func deleteMessage(onSuccess: @escaping () -> Void) {
if messageId <= 0 {
errorMessage = "메시지를 삭제하지 못했습니다\n잠시 후 다시 시도해 주세요."
isShowPopup = true
return
}
isLoading = true
repository.deleteMessage(messageId: messageId)
.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(ApiResponseWithoutData.self, from: responseData)
if decoded.success {
self.errorMessage = "삭제되었습니다."
self.isShowPopup = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
onSuccess()
}
} else {
if let message = decoded.message {
self.errorMessage = message
self.isShowPopup = true
} else {
self.errorMessage = "메시지를 보관하지 못했습니다.\n잠시 후 다시 시도해 주세요."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "메시지를 보관하지 못했습니다.\n잠시 후 다시 시도해 주세요."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
func keepTextMessage() {
if messageId <= 0 {
errorMessage = "메시지를 저장하지 못했습니다\n잠시 후 다시 시도해 주세요."
isShowPopup = true
return
}
isLoading = true
repository.keepTextMessage(messageId: messageId)
.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(ApiResponseWithoutData.self, from: responseData)
if decoded.success {
self.errorMessage = "보관되었습니다."
self.isShowPopup = true
} else {
if let message = decoded.message {
self.errorMessage = message
self.isShowPopup = true
} else {
self.errorMessage = "메시지를 보관하지 못했습니다.\n잠시 후 다시 시도해 주세요."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "메시지를 보관하지 못했습니다.\n잠시 후 다시 시도해 주세요."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
}

View File

@@ -0,0 +1,25 @@
//
// GetTextMessageResponse.swift
// SodaLive
//
// Created by klaus on 2023/08/10.
//
import Foundation
struct GetTextMessageResponse: Decodable {
let totalCount: Int
let items: [TextMessageItem]
}
struct TextMessageItem: Decodable, Hashable {
let messageId: Int
let senderId: Int
let senderNickname: String
let senderProfileImageUrl: String
let recipientNickname: String
let recipientProfileImageUrl: String
let textMessage: String
let date: String
let isKept: Bool
}

View File

@@ -0,0 +1,66 @@
//
// TextMessageItemView.swift
// SodaLive
//
// Created by klaus on 2023/08/10.
//
import SwiftUI
import Kingfisher
struct TextMessageItemView: View {
let item: TextMessageItem
var body: some View {
let nickname = item.recipientNickname == UserDefaults.string(forKey: .nickname) ? item.senderNickname : item.recipientNickname
let profileUrl = item.recipientNickname == UserDefaults.string(forKey: .nickname) ? item.senderProfileImageUrl : item.recipientProfileImageUrl
HStack(spacing: 13.3) {
KFImage(URL(string: profileUrl))
.resizable()
.scaledToFill()
.frame(width: 46.7, height: 46.7, alignment: .top)
.cornerRadius(23.4)
.clipped()
VStack(alignment: .leading, spacing: 4.7) {
Text(nickname)
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
Text(item.textMessage)
.font(.custom(Font.light.rawValue, size: 12))
.foregroundColor(Color(hex: "777777"))
.multilineTextAlignment(.leading)
.lineLimit(2)
}
Spacer()
Text(item.date)
.font(.custom(Font.light.rawValue, size: 12))
.foregroundColor(Color(hex: "525252"))
}
}
}
struct TextMessageItemView_Previews: PreviewProvider {
static var previews: some View {
TextMessageItemView(
item: TextMessageItem(
messageId: 18,
senderId: 19,
senderNickname: "user8",
senderProfileImageUrl: "https://test-cf.yozm.day/profile/default_profile.png",
recipientNickname: "uset7",
recipientProfileImageUrl: "https://test-cf.yozm.day/profile/default_profile.png",
textMessage: "ㅅㅅㅅㅅㅅㅅㅅㅅ러러러라라가가각개가사러",
date: "2022-05-23 16:15:22",
isKept: false
)
)
}
}

View File

@@ -0,0 +1,92 @@
//
// TextMessageView.swift
// SodaLive
//
// Created by klaus on 2023/08/10.
//
import SwiftUI
struct TextMessageView: View {
@StateObject var viewModel = TextMessageViewModel()
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
ZStack(alignment: .bottomTrailing) {
VStack(spacing: 13.3) {
MessageFilterTabView(currentFilterTab: $viewModel.currentFilter)
.padding(.top, 20)
ScrollView(.vertical, showsIndicators: false) {
if viewModel.items.count > 0 {
LazyVStack(spacing: 26.7) {
ForEach(viewModel.items, id: \.self) { item in
TextMessageItemView(item: item)
.frame(width: screenSize().width - 26.7)
.contentShape(Rectangle())
.onTapGesture {
AppState.shared.setAppStep(
step: .textMessageDetail(
messageItem: item,
messageBox: viewModel.currentFilter,
refresh: {
viewModel.page = 1
switch viewModel.currentFilter {
case .receive:
viewModel.getReceivedTextMessage()
case .sent:
viewModel.getSentTextMessage()
case .keep:
viewModel.getKeepTextMessage()
}
}
)
)
}
}
}
.padding(.top, 26.7)
} else {
VStack(spacing: 6.7) {
Image("ic_no_item")
.resizable()
.frame(width: 60, height: 60)
Text("메시지가 없습니다.\n친구들과 소통해보세요!")
.multilineTextAlignment(.center)
.font(.custom(Font.medium.rawValue, size: 10.7))
.foregroundColor(Color(hex: "bbbbbb"))
}
.frame(width: screenSize().width - 26.7, height: screenSize().height / 2)
.background(Color(hex: "2b2635"))
.cornerRadius(4.7)
}
}
}
Image("ic_make_message")
.resizable()
.padding(13.3)
.frame(width: 53.3, height: 53.3)
.background(Color(hex: "9970ff"))
.cornerRadius(26.7)
.padding(.bottom, 33.3)
.padding(.trailing, 6.7)
.onTapGesture {
AppState.shared.setAppStep(step: .writeTextMessage(userId: nil, nickname: nil))
}
}
.onAppear {
viewModel.getReceivedTextMessage()
}
}
}
}
struct TextMessageView_Previews: PreviewProvider {
static var previews: some View {
TextMessageView()
}
}

View File

@@ -0,0 +1,234 @@
//
// TextMessageViewModel.swift
// SodaLive
//
// Created by klaus on 2023/08/10.
//
import Foundation
import Combine
final class TextMessageViewModel: ObservableObject {
private let repository = MessageRepository()
private var subscription = Set<AnyCancellable>()
@Published var items = [TextMessageItem]()
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var isLoading = false
@Published var currentFilter: MessageFilterTab = .receive {
willSet(newVal) {
page = 1
switch newVal {
case .receive:
getReceivedTextMessage()
case .sent:
getSentTextMessage()
case .keep:
getKeepTextMessage()
}
}
}
@Published var message: String = ""
@Published var recipientNickname: String = ""
@Published var recipientId = 0
@Published var sendText = "메시지 보내기"
let placeholder = "내용을 입력해 주세요."
var page = 1
private let size = 10
func write() {
let textMessage = message.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? message : ""
if recipientId <= 0 {
errorMessage = "받는 사람을 선택해 주세요."
isShowPopup = true
return
}
if textMessage.count < 10 {
errorMessage = "10글자 이상 입력해 주세요."
isShowPopup = true
return
}
if !isLoading {
isLoading = true
let request = SendTextMessageRequest(recipientId: recipientId, textMessage: textMessage)
repository.sendTextMessage(request: request)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
if decoded.success {
self.errorMessage = "메시지 전송이 완료되었습니다."
self.isShowPopup = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
AppState.shared.back()
}
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
}
func getReceivedTextMessage() {
if page == 1 {
items.removeAll()
}
isLoading = true
repository
.getReceivedTextMessage(page: page, size: size)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<GetTextMessageResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.items.append(contentsOf: data.items)
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
func getSentTextMessage() {
if page == 1 {
items.removeAll()
}
isLoading = true
repository
.getSentTextMessage(page: page, size: size)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<GetTextMessageResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.items.append(contentsOf: data.items)
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
func getKeepTextMessage() {
if page == 1 {
items.removeAll()
}
isLoading = true
repository
.getKeepTextMessage(page: page, size: size)
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { response in
self.isLoading = false
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<GetTextMessageResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.items.append(contentsOf: data.items)
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
}
.store(in: &subscription)
}
}

View File

@@ -0,0 +1,153 @@
//
// TextMessageWriteView.swift
// SodaLive
//
// Created by klaus on 2023/08/10.
//
import SwiftUI
struct TextMessageWriteView: View {
@StateObject var viewModel = TextMessageViewModel()
@StateObject var appState = AppState.shared
var replySenderId: Int? = nil
var replySenderNickname: String? = nil
@State var isShowSearchUser = false
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
HStack(spacing: 0) {
Text("취소")
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(Color(hex: "9970ff").opacity(0))
Spacer()
Text("새로운 메시지")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
Text("취소")
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(Color(hex: "9970ff"))
.onTapGesture {
AppState.shared.back()
}
}
.padding(.horizontal, 13.3)
.frame(height: 50)
VStack(spacing: 0) {
Rectangle()
.frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0))
Spacer()
HStack(spacing: 13.3) {
Text("받는 사람")
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(Color(hex: "777777"))
Text(viewModel.recipientNickname)
.font(.custom(Font.medium.rawValue, size: 16.7))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
if replySenderId == nil && replySenderNickname == nil {
Image("btn_plus_round")
.resizable()
.frame(width: 27, height: 27)
.onTapGesture {
isShowSearchUser = true
}
}
}
.padding(.horizontal, 13.3)
Spacer()
Rectangle()
.frame(height: 1)
.foregroundColor(Color(hex: "909090").opacity(0.5))
}
.frame(height: 50)
TextViewWrapper(
text: $viewModel.message,
placeholder: viewModel.placeholder,
textColorHex: "eeeeee",
backgroundColorHex: "333333"
)
.frame(width: screenSize().width - 26.7, height: 150)
.cornerRadius(6.7)
.overlay(
RoundedRectangle(cornerRadius: 6.7)
.stroke(Color(hex: "9970ff"), lineWidth: 1.3)
)
.padding(.top, 13.3)
Spacer()
Text(viewModel.sendText)
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color(hex: "eeeeee"))
.frame(width: screenSize().width - 26.7, height: 48.7)
.background(Color(hex: "9970ff"))
.cornerRadius(6.7)
.padding(.bottom, 13.3)
.onTapGesture {
hideKeyboard()
viewModel.write()
}
}
if isShowSearchUser {
SelectRecipientView(isShowing: $isShowSearchUser) {
viewModel.recipientId = $0.id
viewModel.recipientNickname = $0.nickname
}
}
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.padding(.horizontal, 6.7)
.frame(width: geo.size.width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "9970ff"))
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
.onAppear {
UITextView.appearance().backgroundColor = .clear
if let replySenderId = replySenderId, let replySenderNickname = replySenderNickname {
viewModel.recipientId = replySenderId
viewModel.recipientNickname = replySenderNickname
}
}
}
}
struct TextMessageWriteView_Previews: PreviewProvider {
static var previews: some View {
TextMessageWriteView()
}
}