메시지 - 리스트, 쓰기, 상세 페이지 추가
This commit is contained in:
parent
943e1d9f7f
commit
80ff04f825
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "btn_plus_round.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/btn_plus_round.imageset/btn_plus_round.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/btn_plus_round.imageset/btn_plus_round.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_make_message.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_make_message.imageset/ic_make_message.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_make_message.imageset/ic_make_message.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 571 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_make_voice.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_make_voice.imageset/ic_make_voice.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_make_voice.imageset/ic_make_voice.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 742 B |
|
@ -15,4 +15,10 @@ enum AppStep {
|
||||||
case signUp
|
case signUp
|
||||||
|
|
||||||
case findPassword
|
case findPassword
|
||||||
|
|
||||||
|
case textMessageDetail(messageItem: TextMessageItem, messageBox: MessageFilterTab, refresh: () -> Void)
|
||||||
|
|
||||||
|
case writeTextMessage(userId: Int?, nickname: String?)
|
||||||
|
|
||||||
|
case writeVoiceMessage(userId: Int?, nickname: String?, onRefresh: () -> Void)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
//
|
||||||
|
// TextViewWrapper.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TextViewWrapper: UIViewRepresentable {
|
||||||
|
|
||||||
|
@Binding var text: String
|
||||||
|
|
||||||
|
var placeholder: String
|
||||||
|
var textColorHex: String
|
||||||
|
var backgroundColorHex: String
|
||||||
|
|
||||||
|
var notice: String? = nil
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UITextView {
|
||||||
|
let view = UITextView()
|
||||||
|
|
||||||
|
if let notice = notice {
|
||||||
|
view.text = notice.trimmingCharacters(in: .whitespacesAndNewlines) != "" ? notice : placeholder
|
||||||
|
view.textColor = notice.trimmingCharacters(in: .whitespacesAndNewlines) != "" ? UIColor(hex: textColorHex) : .placeholderText
|
||||||
|
} else {
|
||||||
|
view.text = text.trimmingCharacters(in: .whitespacesAndNewlines) != "" ? text : placeholder
|
||||||
|
view.textColor = text.trimmingCharacters(in: .whitespacesAndNewlines) != "" ? UIColor(hex: textColorHex) : .placeholderText
|
||||||
|
}
|
||||||
|
|
||||||
|
view.font = UIFont(name: Font.medium.rawValue, size: 13.3)
|
||||||
|
view.backgroundColor = backgroundColorHex.isEmpty ? .clear : UIColor(hex: backgroundColorHex)
|
||||||
|
view.layer.cornerRadius = 6.7
|
||||||
|
view.textContainerInset = UIEdgeInsets(top: 20, left: 13.3, bottom: 20, right: 13.3)
|
||||||
|
|
||||||
|
view.delegate = context.coordinator
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||||
|
if text.trimmingCharacters(in: .whitespacesAndNewlines) != "" {
|
||||||
|
uiView.text = text
|
||||||
|
uiView.textColor = UIColor(hex: textColorHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator($text, placeholder: placeholder, textColorHex: textColorHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject {
|
||||||
|
var text: Binding<String>
|
||||||
|
var placeholder: String
|
||||||
|
var textColorHex: String
|
||||||
|
|
||||||
|
init(_ text: Binding<String>, placeholder: String, textColorHex: String) {
|
||||||
|
self.text = text
|
||||||
|
self.placeholder = placeholder
|
||||||
|
self.textColorHex = textColorHex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TextViewWrapper.Coordinator: UITextViewDelegate {
|
||||||
|
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||||
|
if textView.textColor == .placeholderText {
|
||||||
|
DispatchQueue.main.async { [unowned self] in
|
||||||
|
self.text.wrappedValue = ""
|
||||||
|
textView.text = ""
|
||||||
|
textView.textColor = UIColor(hex: textColorHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textViewDidEndEditing(_ textView: UITextView) {
|
||||||
|
if textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
DispatchQueue.main.async { [unowned self] in
|
||||||
|
self.text.wrappedValue = ""
|
||||||
|
textView.text = self.placeholder
|
||||||
|
textView.textColor = .placeholderText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
|
self.text.wrappedValue = textView.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TextViewWrapper_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
TextViewWrapper(
|
||||||
|
text: .constant(""),
|
||||||
|
placeholder: "입력하세요",
|
||||||
|
textColorHex: "777777",
|
||||||
|
backgroundColorHex: "222222"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,15 @@ struct ContentView: View {
|
||||||
case .findPassword:
|
case .findPassword:
|
||||||
FindPasswordView()
|
FindPasswordView()
|
||||||
|
|
||||||
|
case .textMessageDetail(let messageItem, let messageBox, let refresh):
|
||||||
|
TextMessageDetailView(messageItem: messageItem, messageBox: messageBox, refresh: refresh)
|
||||||
|
|
||||||
|
case .writeTextMessage(let userId, let nickname):
|
||||||
|
TextMessageWriteView(replySenderId: userId, replySenderNickname: nickname)
|
||||||
|
|
||||||
|
case .writeVoiceMessage(let userId, let nickname, let onRefresh):
|
||||||
|
VoiceMessageWriteView(replySenderId: userId, replySenderNickname: nickname, onRefresh: onRefresh)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
.frame(width: 0, height: 0, alignment: .topLeading)
|
.frame(width: 0, height: 0, alignment: .topLeading)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Moya
|
||||||
|
|
||||||
enum LiveApi {
|
enum LiveApi {
|
||||||
case roomList(request: GetRoomListRequest)
|
case roomList(request: GetRoomListRequest)
|
||||||
|
case recentVisitRoomUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LiveApi: TargetType {
|
extension LiveApi: TargetType {
|
||||||
|
@ -21,12 +22,15 @@ extension LiveApi: TargetType {
|
||||||
switch self {
|
switch self {
|
||||||
case .roomList:
|
case .roomList:
|
||||||
return "/live/room"
|
return "/live/room"
|
||||||
|
|
||||||
|
case .recentVisitRoomUsers:
|
||||||
|
return "/live/room/recent_visit_room/users"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var method: Moya.Method {
|
var method: Moya.Method {
|
||||||
switch self {
|
switch self {
|
||||||
case .roomList:
|
case .roomList, .recentVisitRoomUsers:
|
||||||
return .get
|
return .get
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +52,9 @@ extension LiveApi: TargetType {
|
||||||
return .requestParameters(
|
return .requestParameters(
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
encoding: URLEncoding.queryString)
|
encoding: URLEncoding.queryString)
|
||||||
|
|
||||||
|
case .recentVisitRoomUsers:
|
||||||
|
return .requestPlain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,4 +16,8 @@ final class LiveRepository {
|
||||||
func roomList(request: GetRoomListRequest) -> AnyPublisher<Response, MoyaError> {
|
func roomList(request: GetRoomListRequest) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.roomList(request: request))
|
return api.requestPublisher(.roomList(request: request))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recentVisitRoomUsers() -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.recentVisitRoomUsers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// KeepMessageRequest.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct KeepMessageRequest: Encodable {
|
||||||
|
let container = "ios"
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
//
|
||||||
|
// MessageApi.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Moya
|
||||||
|
|
||||||
|
enum MessageApi {
|
||||||
|
case getSentTextMessage(page: Int, size: Int)
|
||||||
|
case getReceivedTextMessage(page: Int, size: Int)
|
||||||
|
case getKeepTextMessage(page: Int, size: Int)
|
||||||
|
case sendTextMessage(request: SendTextMessageRequest)
|
||||||
|
case keepTextMessage(messageId: Int)
|
||||||
|
|
||||||
|
case getSentVoiceMessage(page: Int, size: Int)
|
||||||
|
case getReceivedVoiceMessage(page: Int, size: Int)
|
||||||
|
case getKeepVoiceMessage(page: Int, size: Int)
|
||||||
|
case sendVoiceMessage(parameters: [MultipartFormData])
|
||||||
|
case keepVoiceMessage(messageId: Int)
|
||||||
|
|
||||||
|
case deleteMessage(messageId: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MessageApi: TargetType {
|
||||||
|
var baseURL: URL {
|
||||||
|
return URL(string: BASE_URL)!
|
||||||
|
}
|
||||||
|
|
||||||
|
var path: String {
|
||||||
|
switch self {
|
||||||
|
case .getSentTextMessage:
|
||||||
|
return "/message/sent/text"
|
||||||
|
|
||||||
|
case .getReceivedTextMessage:
|
||||||
|
return "/message/received/text"
|
||||||
|
|
||||||
|
case .getKeepTextMessage:
|
||||||
|
return "/message/keep/text"
|
||||||
|
|
||||||
|
case .sendTextMessage:
|
||||||
|
return "/message/send/text"
|
||||||
|
|
||||||
|
case .keepTextMessage(let messageId):
|
||||||
|
return "/message/keep/text/\(messageId)"
|
||||||
|
|
||||||
|
case .getSentVoiceMessage:
|
||||||
|
return "/message/sent/voice"
|
||||||
|
|
||||||
|
case .getReceivedVoiceMessage:
|
||||||
|
return "/message/received/voice"
|
||||||
|
|
||||||
|
case .getKeepVoiceMessage:
|
||||||
|
return "/message/keep/voice"
|
||||||
|
|
||||||
|
case .sendVoiceMessage:
|
||||||
|
return "/message/send/voice"
|
||||||
|
|
||||||
|
case .keepVoiceMessage(let messageId):
|
||||||
|
return "/message/keep/voice/\(messageId)"
|
||||||
|
|
||||||
|
case .deleteMessage(let messageId):
|
||||||
|
return "/message/\(messageId)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var method: Moya.Method {
|
||||||
|
switch self {
|
||||||
|
case .getSentTextMessage, .getReceivedTextMessage, .getKeepTextMessage:
|
||||||
|
return .get
|
||||||
|
|
||||||
|
case .sendTextMessage:
|
||||||
|
return .post
|
||||||
|
|
||||||
|
case .keepTextMessage:
|
||||||
|
return .put
|
||||||
|
|
||||||
|
case .getSentVoiceMessage, .getReceivedVoiceMessage, .getKeepVoiceMessage:
|
||||||
|
return .get
|
||||||
|
|
||||||
|
case .sendVoiceMessage:
|
||||||
|
return .post
|
||||||
|
|
||||||
|
case .keepVoiceMessage:
|
||||||
|
return .put
|
||||||
|
|
||||||
|
case .deleteMessage:
|
||||||
|
return .delete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var task: Task {
|
||||||
|
switch self {
|
||||||
|
case .getSentTextMessage(let page, let size),
|
||||||
|
.getReceivedTextMessage(let page, let size),
|
||||||
|
.getKeepTextMessage(let page, let size),
|
||||||
|
.getSentVoiceMessage(let page, let size),
|
||||||
|
.getReceivedVoiceMessage(let page, let size),
|
||||||
|
.getKeepVoiceMessage(let page, let size):
|
||||||
|
|
||||||
|
let parameters = [
|
||||||
|
"page": page - 1,
|
||||||
|
"size": size,
|
||||||
|
"timezone": TimeZone.current.identifier,
|
||||||
|
] as [String : Any]
|
||||||
|
|
||||||
|
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||||
|
|
||||||
|
case .sendTextMessage(let request):
|
||||||
|
return .requestJSONEncodable(request)
|
||||||
|
|
||||||
|
case .sendVoiceMessage(let parameters):
|
||||||
|
return .uploadMultipart(parameters)
|
||||||
|
|
||||||
|
case .deleteMessage:
|
||||||
|
return .requestPlain
|
||||||
|
|
||||||
|
case .keepTextMessage, .keepVoiceMessage:
|
||||||
|
return .requestJSONEncodable(KeepMessageRequest())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers: [String : String]? {
|
||||||
|
return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// MessageFilterTab.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum MessageFilterTab {
|
||||||
|
case receive, sent, keep
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
//
|
||||||
|
// MessageFilterTabView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MessageFilterTabView: View {
|
||||||
|
|
||||||
|
@Binding var currentFilterTab: MessageFilterTab
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 6.7) {
|
||||||
|
Text("받은 메시지")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: currentFilterTab == .receive ? "9970ff" : "777777"))
|
||||||
|
.padding(.horizontal, 25)
|
||||||
|
.padding(.vertical, 10.7)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 16.7)
|
||||||
|
.stroke(
|
||||||
|
Color(hex: currentFilterTab == .receive ? "9970ff" : "777777"),
|
||||||
|
lineWidth: 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
if currentFilterTab != .receive {
|
||||||
|
currentFilterTab = .receive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("보낸 메시지")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: currentFilterTab == .sent ? "9970ff" : "777777"))
|
||||||
|
.padding(.horizontal, 25)
|
||||||
|
.padding(.vertical, 10.7)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 16.7)
|
||||||
|
.stroke(
|
||||||
|
Color(hex: currentFilterTab == .sent ? "9970ff" : "777777"),
|
||||||
|
lineWidth: 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
if currentFilterTab != .sent {
|
||||||
|
currentFilterTab = .sent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("보관함")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: currentFilterTab == .keep ? "9970ff" : "777777"))
|
||||||
|
.padding(.horizontal, 25)
|
||||||
|
.padding(.vertical, 10.7)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 16.7)
|
||||||
|
.stroke(
|
||||||
|
Color(hex: currentFilterTab == .keep ? "9970ff" : "777777"),
|
||||||
|
lineWidth: 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
if currentFilterTab != .keep {
|
||||||
|
currentFilterTab = .keep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MessageFilterTabView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
MessageFilterTabView(currentFilterTab: .constant(.receive))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
//
|
||||||
|
// MessageRepository.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CombineMoya
|
||||||
|
import Combine
|
||||||
|
import Moya
|
||||||
|
|
||||||
|
final class MessageRepository {
|
||||||
|
private let api = MoyaProvider<MessageApi>()
|
||||||
|
|
||||||
|
func getReceivedTextMessage(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getReceivedTextMessage(page: page, size: size))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSentTextMessage(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getSentTextMessage(page: page, size: size))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeepTextMessage(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getKeepTextMessage(page: page, size: size))
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendTextMessage(request: SendTextMessageRequest) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.sendTextMessage(request:request))
|
||||||
|
}
|
||||||
|
|
||||||
|
func keepTextMessage(messageId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.keepTextMessage(messageId: messageId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReceivedVoiceMessage(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getReceivedVoiceMessage(page: page, size: size))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSentVoiceMessage(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getSentVoiceMessage(page: page, size: size))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeepVoiceMessage(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.getKeepVoiceMessage(page: page, size: size))
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendVoiceMessage(parameters: [MultipartFormData]) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.sendVoiceMessage(parameters: parameters))
|
||||||
|
}
|
||||||
|
|
||||||
|
func keepVoiceMessage(messageId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.keepVoiceMessage(messageId: messageId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteMessage(messageId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.deleteMessage(messageId: messageId))
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,81 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct MessageView: View {
|
struct MessageView: View {
|
||||||
|
|
||||||
|
@StateObject var viewModel = MessageViewModel()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text("Message")
|
GeometryReader { geo in
|
||||||
|
VStack {
|
||||||
|
HomeNavigationBar(title: "메시지") {}
|
||||||
|
|
||||||
|
Tab()
|
||||||
|
|
||||||
|
Text("※ 보관하지 않은 받은 메시지는 3일 후, 자동 삭제됩니다.")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.padding(.top, 20)
|
||||||
|
|
||||||
|
switch viewModel.currentTab {
|
||||||
|
case .text:
|
||||||
|
TextMessageView()
|
||||||
|
|
||||||
|
case .voice:
|
||||||
|
VoiceMessageView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: geo.size.width, height: geo.size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func Tab() -> some View {
|
||||||
|
let tabWidth = screenSize().width / 2
|
||||||
|
|
||||||
|
VStack(spacing:0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Button(action: {
|
||||||
|
if viewModel.currentTab != .text {
|
||||||
|
viewModel.currentTab = .text
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Text("문자")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 16.7))
|
||||||
|
.foregroundColor(Color(hex: viewModel.currentTab == .text ? "eeeeee" : "777777"))
|
||||||
|
.frame(width: tabWidth, height: 50)
|
||||||
|
|
||||||
|
if viewModel.currentTab == .text {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.frame(width: tabWidth, height: 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
if viewModel.currentTab != .voice {
|
||||||
|
viewModel.currentTab = .voice
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Text("음성")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 16.7))
|
||||||
|
.foregroundColor(Color(hex: viewModel.currentTab == .voice ? "eeeeee" : "777777"))
|
||||||
|
.frame(width: tabWidth, height: 50)
|
||||||
|
|
||||||
|
if viewModel.currentTab == .voice {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.frame(width: tabWidth, height: 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.frame(width: screenSize().width, height: 1)
|
||||||
|
.foregroundColor(Color(hex: "909090"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
//
|
||||||
|
// MessageViewModel.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class MessageViewModel: ObservableObject {
|
||||||
|
|
||||||
|
enum CurrentTab: String {
|
||||||
|
case text, voice
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var currentTab: CurrentTab = .text
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
//
|
||||||
|
// SelectRecipientView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct SelectRecipientView: View {
|
||||||
|
|
||||||
|
@ObservedObject var viewModel = SelectRecipientViewModel()
|
||||||
|
|
||||||
|
@Binding var isShowing: Bool
|
||||||
|
let selectUser: (GetRoomDetailUser) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
BaseView {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
DetailNavigationBar(title: "받는 사람 검색") {
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField("닉네임을 입력해주세요", text: $viewModel.searchNickname)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.keyboardType(.default)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.frame(width: screenSize().width - 26.7, height: 50)
|
||||||
|
.background(Color(hex: "232323"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
VStack(spacing: 26.7) {
|
||||||
|
ForEach(viewModel.users, id: \.self) { user in
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
KFImage(URL(string: user.profileImageUrl))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 46.7, height: 46.7, alignment: .top)
|
||||||
|
.clipped()
|
||||||
|
.cornerRadius(23.3)
|
||||||
|
|
||||||
|
Text(user.nickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
selectUser(user)
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: screenSize().width - 26.7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModel.searchUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SelectRecipientView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SelectRecipientView(isShowing: .constant(true)) { _ in }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
//
|
||||||
|
// SelectRecipientViewModel.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class SelectRecipientViewModel: ObservableObject {
|
||||||
|
|
||||||
|
private let liveRepository = LiveRepository()
|
||||||
|
private let userRepository = UserRepository()
|
||||||
|
private var subscription = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@Published var errorMessage = ""
|
||||||
|
@Published var isShowPopup = false
|
||||||
|
@Published var isLoading = false
|
||||||
|
|
||||||
|
@Published var searchNickname = "" {
|
||||||
|
didSet {
|
||||||
|
searchUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var users = [GetRoomDetailUser]()
|
||||||
|
|
||||||
|
func searchUser() {
|
||||||
|
if searchNickname.count > 1 {
|
||||||
|
userRepository.searchUser(nickname: searchNickname)
|
||||||
|
.sink { result in
|
||||||
|
switch result {
|
||||||
|
case .finished:
|
||||||
|
DEBUG_LOG("finish")
|
||||||
|
case .failure(let error):
|
||||||
|
ERROR_LOG(error.localizedDescription)
|
||||||
|
}
|
||||||
|
} receiveValue: { response in
|
||||||
|
let responseData = response.data
|
||||||
|
|
||||||
|
do {
|
||||||
|
let jsonDecoder = JSONDecoder()
|
||||||
|
let decoded = try jsonDecoder.decode(ApiResponse<[GetRoomDetailUser]>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
self.users.removeAll()
|
||||||
|
self.users.append(contentsOf: data)
|
||||||
|
} else {
|
||||||
|
if let message = decoded.message {
|
||||||
|
DEBUG_LOG("message: \(message)")
|
||||||
|
self.errorMessage = message
|
||||||
|
} else {
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
} else {
|
||||||
|
liveRepository.recentVisitRoomUsers()
|
||||||
|
.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<[GetRoomDetailUser]>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
self.users.removeAll()
|
||||||
|
self.users.append(contentsOf: data)
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// SendMessageRequest.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct SendTextMessageRequest: Encodable {
|
||||||
|
let recipientId: Int
|
||||||
|
let textMessage: String
|
||||||
|
let container: String = "ios"
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SendVoiceMessageRequest: Encodable {
|
||||||
|
let recipientId: Int
|
||||||
|
let container: String = "ios"
|
||||||
|
}
|
|
@ -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: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// GetVoiceMessageResponse.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GetVoiceMessageResponse: Decodable {
|
||||||
|
let totalCount: Int
|
||||||
|
let items: [VoiceMessageItem]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VoiceMessageItem: Decodable, Hashable {
|
||||||
|
let messageId: Int
|
||||||
|
let senderId: Int
|
||||||
|
let senderNickname: String
|
||||||
|
let senderProfileImageUrl: String
|
||||||
|
let recipientNickname: String
|
||||||
|
let recipientProfileImageUrl: String
|
||||||
|
let voiceMessageUrl: String
|
||||||
|
let date: String
|
||||||
|
let isKept: Bool
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
//
|
||||||
|
// SoundManager.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AVKit
|
||||||
|
|
||||||
|
class SoundManager: NSObject, ObservableObject {
|
||||||
|
@Published var errorMessage = ""
|
||||||
|
@Published var isShowPopup = false
|
||||||
|
@Published var isLoading = false
|
||||||
|
@Published var onClose = false
|
||||||
|
|
||||||
|
@Published var isPlaying = false
|
||||||
|
@Published var isRecording = false
|
||||||
|
@Published var duration: TimeInterval = 0
|
||||||
|
|
||||||
|
var player: AVAudioPlayer!
|
||||||
|
var audioRecorder: AVAudioRecorder!
|
||||||
|
|
||||||
|
var startTimer: (() -> Void)?
|
||||||
|
var stopTimer: (() -> Void)?
|
||||||
|
|
||||||
|
func prepareRecording() {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
let audioSession = AVAudioSession.sharedInstance()
|
||||||
|
do {
|
||||||
|
try audioSession.setCategory(.playAndRecord, mode: .default)
|
||||||
|
try audioSession.setActive(true)
|
||||||
|
audioSession.requestRecordPermission() { [weak self] allowed in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if !allowed {
|
||||||
|
self?.errorMessage = "권한을 허용하지 않으시면 음성메시지 서비스를 이용하실 수 없습니다."
|
||||||
|
self?.isShowPopup = true
|
||||||
|
self?.onClose = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
|
||||||
|
isShowPopup = true
|
||||||
|
onClose = true
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func startRecording() {
|
||||||
|
let settings = [
|
||||||
|
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
|
||||||
|
AVSampleRateKey: 12000,
|
||||||
|
AVNumberOfChannelsKey: 1,
|
||||||
|
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
|
||||||
|
]
|
||||||
|
|
||||||
|
do {
|
||||||
|
audioRecorder = try AVAudioRecorder(url: getAudioFileURL(), settings: settings)
|
||||||
|
audioRecorder.record()
|
||||||
|
|
||||||
|
if let startTimer = startTimer {
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
isRecording = true
|
||||||
|
} catch {
|
||||||
|
errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
|
||||||
|
isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopRecording() {
|
||||||
|
audioRecorder.stop()
|
||||||
|
audioRecorder = nil
|
||||||
|
isRecording = false
|
||||||
|
|
||||||
|
if let stopTimer = stopTimer {
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareForPlay()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRecorderCurrentTime() -> TimeInterval {
|
||||||
|
return audioRecorder.currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareForPlay(_ url: URL? = nil) {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let audioSession = AVAudioSession.sharedInstance()
|
||||||
|
do {
|
||||||
|
try audioSession.setCategory(.playback, mode: .default)
|
||||||
|
|
||||||
|
if let url = url {
|
||||||
|
self.player = try AVAudioPlayer(data: Data(contentsOf: url))
|
||||||
|
} else {
|
||||||
|
self.player = try AVAudioPlayer(contentsOf: self.getAudioFileURL())
|
||||||
|
}
|
||||||
|
|
||||||
|
self.player?.volume = 1
|
||||||
|
self.player?.delegate = self
|
||||||
|
self.player?.prepareToPlay()
|
||||||
|
|
||||||
|
self.duration = self.player.duration
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func playAudio() {
|
||||||
|
player?.play()
|
||||||
|
|
||||||
|
isPlaying = player.isPlaying
|
||||||
|
if let startTimer = startTimer {
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopAudio() {
|
||||||
|
player.stop()
|
||||||
|
player.currentTime = 0
|
||||||
|
isPlaying = player.isPlaying
|
||||||
|
if let stopTimer = stopTimer {
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPlayerCurrentTime() -> TimeInterval {
|
||||||
|
return player.currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteAudioFile() {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: getAudioFileURL())
|
||||||
|
duration = 0
|
||||||
|
} catch {
|
||||||
|
errorMessage = "오류가 발생했습니다. 다시 시도해 주세요."
|
||||||
|
isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAudioFileURL() -> URL {
|
||||||
|
return getDocumentsDirectory().appendingPathComponent("recording.m4a")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getDocumentsDirectory() -> URL {
|
||||||
|
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
||||||
|
return paths[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SoundManager: AVAudioPlayerDelegate {
|
||||||
|
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
|
||||||
|
stopAudio()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
//
|
||||||
|
// VoiceMessageItemView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct VoiceMessageItemView: View {
|
||||||
|
|
||||||
|
let index: Int
|
||||||
|
let item: VoiceMessageItem
|
||||||
|
let currentFilter: MessageFilterTab
|
||||||
|
let soundManager: SoundManager
|
||||||
|
|
||||||
|
@Binding var openPlayerItemIndex: Int
|
||||||
|
|
||||||
|
let onClickSave: () -> Void
|
||||||
|
let onClickReply: () -> Void
|
||||||
|
let onClickDelete: () -> Void
|
||||||
|
|
||||||
|
@State var progress: TimeInterval = 0
|
||||||
|
@State var timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
KFImage(URL(string: profileUrl))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 46.7, height: 46.7, alignment: .top)
|
||||||
|
.clipped()
|
||||||
|
.cornerRadius(23.4)
|
||||||
|
|
||||||
|
Text(nickname)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
.padding(.leading, 13.3)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(item.date)
|
||||||
|
.font(.custom(Font.light.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "525252"))
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
openPlayerItemIndex = openPlayerItemIndex == index ? -1 : index
|
||||||
|
}
|
||||||
|
|
||||||
|
if openPlayerItemIndex == index {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ProgressView(value: progress, total: soundManager.duration)
|
||||||
|
.progressViewStyle(LinearProgressViewStyle(tint: Color(hex: "9970ff")))
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("00:00")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10.7))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(secondsToMinutesSeconds(seconds: Int(soundManager.duration)))")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10.7))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.padding(.top, 6.7)
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
|
||||||
|
Image("ic_save")
|
||||||
|
.resizable()
|
||||||
|
.frame(
|
||||||
|
width: currentFilter == .receive ? 27 : 22,
|
||||||
|
height: currentFilter == .receive ? 27 : 22
|
||||||
|
)
|
||||||
|
.opacity(currentFilter == .receive ? 1 : 0)
|
||||||
|
.onTapGesture {
|
||||||
|
if currentFilter == .receive {
|
||||||
|
onClickSave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(soundManager.isPlaying ? "btn_bar_stop": "btn_bar_play")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
.onTapGesture {
|
||||||
|
if soundManager.isPlaying {
|
||||||
|
soundManager.stopAudio()
|
||||||
|
} else {
|
||||||
|
soundManager.playAudio()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if currentFilter == .receive {
|
||||||
|
Image("ic_mic_paint")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 27, height: 27)
|
||||||
|
.onTapGesture { onClickReply() }
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentFilter == .sent || currentFilter == .keep {
|
||||||
|
Image(systemName: "trash.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 22, height: 22)
|
||||||
|
.onTapGesture { onClickDelete() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 24.3)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 20)
|
||||||
|
.background(Color(hex: "9970ff").opacity(0.2))
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
.onAppear {
|
||||||
|
soundManager.startTimer = startTimer
|
||||||
|
soundManager.stopTimer = stopTimer
|
||||||
|
soundManager.prepareForPlay(URL(string: item.voiceMessageUrl)!)
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
soundManager.stopAudio()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: screenSize().width - 26.7)
|
||||||
|
.background(Color.black)
|
||||||
|
.onAppear {
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
.onReceive(timer) { _ in
|
||||||
|
self.progress = soundManager.getPlayerCurrentTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func secondsToMinutesSeconds(seconds: Int) -> String {
|
||||||
|
let minute = String(format: "%02d", seconds / 60)
|
||||||
|
let second = String(format: "%02d", seconds % 60)
|
||||||
|
|
||||||
|
return "\(minute):\(second)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startTimer() {
|
||||||
|
timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopTimer() {
|
||||||
|
timer.upstream.connect().cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VoiceMessageItemView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
VoiceMessageItemView(
|
||||||
|
index: 0,
|
||||||
|
item: VoiceMessageItem(
|
||||||
|
messageId: 24,
|
||||||
|
senderId: 13,
|
||||||
|
senderNickname: "user5",
|
||||||
|
senderProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||||
|
recipientNickname: "user8",
|
||||||
|
recipientProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||||
|
voiceMessageUrl: "",
|
||||||
|
date: "2022-07-02 01:42:43",
|
||||||
|
isKept: false
|
||||||
|
),
|
||||||
|
currentFilter: .keep,
|
||||||
|
soundManager: SoundManager(),
|
||||||
|
openPlayerItemIndex: .constant(0),
|
||||||
|
onClickSave: {},
|
||||||
|
onClickReply: {},
|
||||||
|
onClickDelete: {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,215 @@
|
||||||
|
//
|
||||||
|
// VoiceMessageView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct VoiceMessageView: View {
|
||||||
|
|
||||||
|
@StateObject var viewModel = VoiceMessageViewModel()
|
||||||
|
@StateObject var soundManager = SoundManager()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.black.ignoresSafeArea()
|
||||||
|
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
VStack(spacing: 13.3) {
|
||||||
|
|
||||||
|
MessageFilterTabView(currentFilterTab: $viewModel.currentFilter)
|
||||||
|
.padding(.top, 20)
|
||||||
|
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
if viewModel.items.count > 0 {
|
||||||
|
VStack(spacing: 26.7) {
|
||||||
|
ForEach(0..<viewModel.items.count, id: \.self) { index in
|
||||||
|
let item = viewModel.items[index]
|
||||||
|
VoiceMessageItemView(
|
||||||
|
index: index,
|
||||||
|
item: item,
|
||||||
|
currentFilter: viewModel.currentFilter,
|
||||||
|
soundManager: soundManager,
|
||||||
|
openPlayerItemIndex: $viewModel.openPlayerItemIndex,
|
||||||
|
onClickSave: {
|
||||||
|
viewModel.selectedMessageId = item.messageId
|
||||||
|
soundManager.stopAudio()
|
||||||
|
if item.isKept {
|
||||||
|
viewModel.errorMessage = "이미 보관된 메시지 입니다"
|
||||||
|
viewModel.isShowPopup = true
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClickReply: {
|
||||||
|
viewModel.selectedMessageId = item.messageId
|
||||||
|
soundManager.stopAudio()
|
||||||
|
},
|
||||||
|
onClickDelete: {
|
||||||
|
viewModel.selectedMessageId = item.messageId
|
||||||
|
soundManager.stopAudio()
|
||||||
|
viewModel.deleteMessage()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.onAppear {
|
||||||
|
if index == viewModel.items.count - 1 {
|
||||||
|
viewModel.loadMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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_voice")
|
||||||
|
.resizable()
|
||||||
|
.padding(13.3)
|
||||||
|
.frame(width: 53.3, height: 53.3)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(26.7)
|
||||||
|
.padding(.bottom, 33.3)
|
||||||
|
.onTapGesture {
|
||||||
|
AppState.shared.setAppStep(
|
||||||
|
step: .writeVoiceMessage(
|
||||||
|
userId: nil,
|
||||||
|
nickname: nil,
|
||||||
|
onRefresh: {
|
||||||
|
viewModel.refresh()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isShowSavePopup {
|
||||||
|
ZStack {
|
||||||
|
Color.black.opacity(0.7)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Text("메시지 보관")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
.padding(.top, 40)
|
||||||
|
|
||||||
|
Text("메시지를 보관하는데\n\(viewModel.saveMessagePrice)캔이 필요합니다.\n메시지를 보관하시겠습니까?")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 15))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
Text("※ 메시지 보관시, 본인이 삭제하기 전까지 영구보관됩니다.")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
Text("취소")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
.frame(width: (screenSize().width - 66.7) / 3)
|
||||||
|
.background(Color(hex: "9970ff").opacity(0.2))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.stroke(
|
||||||
|
Color(hex: "9970ff"),
|
||||||
|
lineWidth: 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.isShowSavePopup = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("확인")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
.frame(width: (screenSize().width - 66.7) * 2 / 3)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.isShowSavePopup = false
|
||||||
|
viewModel.keepTextMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 20)
|
||||||
|
.padding(.horizontal, 16.7)
|
||||||
|
}
|
||||||
|
.frame(width: screenSize().width - 26.7)
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isLoading || soundManager.isLoading {
|
||||||
|
LoadingView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
||||||
|
GeometryReader { geo in
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text(soundManager.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)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModel.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VoiceMessageView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
VoiceMessageView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,380 @@
|
||||||
|
//
|
||||||
|
// VoiceMessageViewModel.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import Moya
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class VoiceMessageViewModel: ObservableObject {
|
||||||
|
private let repository = MessageRepository()
|
||||||
|
private var subscription = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@Published var items = [VoiceMessageItem]()
|
||||||
|
|
||||||
|
@Published var errorMessage = ""
|
||||||
|
@Published var isShowPopup = false
|
||||||
|
@Published var isLoading = false
|
||||||
|
|
||||||
|
@Published var saveMessagePrice = 0
|
||||||
|
@Published var isShowSavePopup = false
|
||||||
|
|
||||||
|
@Published var currentFilter: MessageFilterTab = .receive {
|
||||||
|
didSet {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published var recipientNickname: String = ""
|
||||||
|
@Published var recipientId = 0
|
||||||
|
|
||||||
|
@Published var sendText = "메시지 보내기"
|
||||||
|
|
||||||
|
@Published var selectedMessageId = -1
|
||||||
|
@Published var openPlayerItemIndex = -1
|
||||||
|
|
||||||
|
@Published var recordMode = RecordMode.RECORD
|
||||||
|
|
||||||
|
enum RecordMode {
|
||||||
|
case RECORD, PLAY
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isLast = false
|
||||||
|
private var page = 1
|
||||||
|
private let size = 10
|
||||||
|
|
||||||
|
func refresh() {
|
||||||
|
page = 1
|
||||||
|
isLast = false
|
||||||
|
selectedMessageId = -1
|
||||||
|
openPlayerItemIndex = -1
|
||||||
|
|
||||||
|
loadMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMessage() {
|
||||||
|
switch currentFilter {
|
||||||
|
case .receive:
|
||||||
|
getReceivedVoiceMessage()
|
||||||
|
|
||||||
|
case .sent:
|
||||||
|
getSentVoiceMessage()
|
||||||
|
|
||||||
|
case .keep:
|
||||||
|
getKeepVoiceMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteMessage() {
|
||||||
|
if selectedMessageId <= 0 {
|
||||||
|
errorMessage = "메시지를 삭제하지 못했습니다\n잠시 후 다시 시도해 주세요."
|
||||||
|
isShowPopup = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
repository.deleteMessage(messageId: selectedMessageId)
|
||||||
|
.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
|
||||||
|
self.refresh()
|
||||||
|
} 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 selectedMessageId <= 0 {
|
||||||
|
errorMessage = "메시지를 저장하지 못했습니다\n잠시 후 다시 시도해 주세요."
|
||||||
|
isShowPopup = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
repository.keepTextMessage(messageId: selectedMessageId)
|
||||||
|
.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
|
||||||
|
self.refresh()
|
||||||
|
} 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 write(soundData: Data, onSuccess: @escaping () -> Void) {
|
||||||
|
if recipientId <= 0 {
|
||||||
|
errorMessage = "받는 사람을 선택해 주세요."
|
||||||
|
isShowPopup = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isLoading {
|
||||||
|
isLoading = true
|
||||||
|
let request = SendVoiceMessageRequest(recipientId: recipientId)
|
||||||
|
|
||||||
|
var multipartData = [MultipartFormData]()
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.outputFormatting = .withoutEscapingSlashes
|
||||||
|
let jsonData = try? encoder.encode(request)
|
||||||
|
if let jsonData = jsonData {
|
||||||
|
multipartData.append(
|
||||||
|
MultipartFormData(
|
||||||
|
provider: .data(soundData),
|
||||||
|
name: "voiceMessageFile",
|
||||||
|
fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).m4a",
|
||||||
|
mimeType: "audio/m4a")
|
||||||
|
)
|
||||||
|
multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request"))
|
||||||
|
|
||||||
|
repository.sendVoiceMessage(parameters: multipartData)
|
||||||
|
.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 {
|
||||||
|
onSuccess()
|
||||||
|
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다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = "음성메시지를 전송하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
} else {
|
||||||
|
self.errorMessage = "음성메시지를 전송하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
self.isShowPopup = true
|
||||||
|
self.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getReceivedVoiceMessage() {
|
||||||
|
if page == 1 {
|
||||||
|
items.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
repository
|
||||||
|
.getReceivedVoiceMessage(page: page, size: size)
|
||||||
|
.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<GetVoiceMessageResponse>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
if data.items.count <= 0 {
|
||||||
|
self.isLast = true
|
||||||
|
} else {
|
||||||
|
self.items.append(contentsOf: data.items)
|
||||||
|
self.page += 1
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getSentVoiceMessage() {
|
||||||
|
if page == 1 {
|
||||||
|
items.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
repository
|
||||||
|
.getSentVoiceMessage(page: page, size: size)
|
||||||
|
.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<GetVoiceMessageResponse>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
if data.items.count <= 0 {
|
||||||
|
self.isLast = true
|
||||||
|
} else {
|
||||||
|
self.items.append(contentsOf: data.items)
|
||||||
|
self.page += 1
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getKeepVoiceMessage() {
|
||||||
|
if page == 1 {
|
||||||
|
items.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
repository
|
||||||
|
.getKeepVoiceMessage(page: page, size: size)
|
||||||
|
.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<GetVoiceMessageResponse>.self, from: responseData)
|
||||||
|
|
||||||
|
if let data = decoded.data, decoded.success {
|
||||||
|
if data.items.count <= 0 {
|
||||||
|
self.isLast = true
|
||||||
|
} else {
|
||||||
|
self.items.append(contentsOf: data.items)
|
||||||
|
self.page += 1
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,337 @@
|
||||||
|
//
|
||||||
|
// VoiceMessageWriteView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 2023/08/10.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct VoiceMessageWriteView: View {
|
||||||
|
|
||||||
|
@StateObject var viewModel = VoiceMessageViewModel()
|
||||||
|
@StateObject var soundManager = SoundManager()
|
||||||
|
@StateObject var appState = AppState.shared
|
||||||
|
|
||||||
|
var replySenderId: Int? = nil
|
||||||
|
var replySenderNickname: String? = nil
|
||||||
|
let onRefresh: () -> Void
|
||||||
|
|
||||||
|
@State var isShowSearchUser = false
|
||||||
|
@State var timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
|
@State var progress: TimeInterval = 0
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Color.black.opacity(0.7)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onTapGesture {
|
||||||
|
hideView()
|
||||||
|
}
|
||||||
|
|
||||||
|
GeometryReader { proxy in
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("음성메시지")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image("ic_close_white")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
.onTapGesture {
|
||||||
|
hideView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 26.7)
|
||||||
|
.padding(.top, 26.7)
|
||||||
|
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
Image("img_thumb_default")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 46.7, height: 46.7)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text("TO.")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color(hex: "eeeeee"))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
viewModel.recipientNickname.count > 0 ?
|
||||||
|
viewModel.recipientNickname :
|
||||||
|
"받는 사람"
|
||||||
|
)
|
||||||
|
.font(
|
||||||
|
.custom(
|
||||||
|
viewModel.recipientNickname.count > 0 ?
|
||||||
|
Font.bold.rawValue :
|
||||||
|
Font.light.rawValue,
|
||||||
|
size: 16.7
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.foregroundColor(
|
||||||
|
Color(
|
||||||
|
hex:
|
||||||
|
viewModel.recipientNickname.count > 0 ?
|
||||||
|
"eeeeee" :
|
||||||
|
"bbbbbb"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if replySenderId == nil && replySenderNickname == nil {
|
||||||
|
Image("btn_plus_round")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 27, height: 27)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
.padding(13.3)
|
||||||
|
.background(Color(hex: "9970ff").opacity(0.2))
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.padding(.top, 26.7)
|
||||||
|
.onTapGesture {
|
||||||
|
if replySenderId == nil && replySenderNickname == nil {
|
||||||
|
isShowSearchUser = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(secondsToHoursMinutesSeconds(seconds:Int(progress)))
|
||||||
|
.font(.custom(Font.light.rawValue, size: 33.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.top, 81)
|
||||||
|
|
||||||
|
switch viewModel.recordMode {
|
||||||
|
case .RECORD:
|
||||||
|
Image(soundManager.isRecording ? "ic_record_stop" : "ic_record")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 70, height: 70)
|
||||||
|
.padding(.vertical, 52.3)
|
||||||
|
.onTapGesture {
|
||||||
|
if viewModel.recipientId <= 0 {
|
||||||
|
viewModel.errorMessage = "받는 사람을 선택해 주세요."
|
||||||
|
viewModel.isShowPopup = true
|
||||||
|
} else {
|
||||||
|
progress = 0
|
||||||
|
if !soundManager.isRecording {
|
||||||
|
soundManager.startRecording()
|
||||||
|
} else {
|
||||||
|
soundManager.stopRecording()
|
||||||
|
viewModel.recordMode = .PLAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .PLAY:
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("삭제")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 15.3))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb").opacity(0))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(
|
||||||
|
!soundManager.isPlaying ?
|
||||||
|
"ic_record_play" :
|
||||||
|
"ic_record_pause"
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
progress = 0
|
||||||
|
if !soundManager.isPlaying {
|
||||||
|
soundManager.playAudio()
|
||||||
|
} else {
|
||||||
|
soundManager.stopAudio()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("삭제")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 15.3))
|
||||||
|
.foregroundColor(Color(hex: "bbbbbb"))
|
||||||
|
.onTapGesture {
|
||||||
|
soundManager.stopAudio()
|
||||||
|
soundManager.deleteAudioFile()
|
||||||
|
viewModel.recordMode = .RECORD
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.top, 90)
|
||||||
|
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
Text("다시 녹음")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color(hex: "9970ff"))
|
||||||
|
.frame(width: (proxy.size.width - 40) / 3, height: 50)
|
||||||
|
.background(Color(hex: "9970ff").opacity(0.2))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.stroke(Color(hex: "9970ff"), lineWidth: 1.3)
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
soundManager.stopAudio()
|
||||||
|
soundManager.deleteAudioFile()
|
||||||
|
viewModel.recordMode = .RECORD
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(viewModel.sendText)
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(width: (proxy.size.width - 40) * 2 / 3, height: 50)
|
||||||
|
.background(Color(hex: "9970ff"))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.onTapGesture {
|
||||||
|
do {
|
||||||
|
let soundData = try Data(contentsOf: soundManager.getAudioFileURL())
|
||||||
|
viewModel.write (soundData: soundData) {
|
||||||
|
soundManager.deleteAudioFile()
|
||||||
|
onRefresh()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
viewModel.errorMessage = "음성메시지를 전송하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
|
||||||
|
viewModel.isShowPopup = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 26.7)
|
||||||
|
.padding(.bottom, 13.3)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if proxy.safeAreaInsets.bottom > 0 {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color(hex: "222222"))
|
||||||
|
.frame(width: proxy.size.width, height: 15.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(16.7, corners: [.topLeft, .topRight])
|
||||||
|
}
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isShowSearchUser {
|
||||||
|
SelectRecipientView(isShowing: $isShowSearchUser) {
|
||||||
|
viewModel.recipientId = $0.id
|
||||||
|
viewModel.recipientNickname = $0.nickname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.isLoading || soundManager.isLoading {
|
||||||
|
LoadingView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .top, autohideIn: 1) {
|
||||||
|
GeometryReader { geo in
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text(soundManager.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()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
if soundManager.onClose {
|
||||||
|
hideView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
stopTimer()
|
||||||
|
soundManager.startTimer = startTimer
|
||||||
|
soundManager.stopTimer = stopTimer
|
||||||
|
soundManager.prepareRecording()
|
||||||
|
|
||||||
|
UITextView.appearance().backgroundColor = .clear
|
||||||
|
|
||||||
|
if let replySenderId = replySenderId, let replySenderNickname = replySenderNickname {
|
||||||
|
viewModel.recipientId = replySenderId
|
||||||
|
viewModel.recipientNickname = replySenderNickname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(timer) { _ in
|
||||||
|
switch viewModel.recordMode {
|
||||||
|
case .RECORD:
|
||||||
|
progress = soundManager.getRecorderCurrentTime()
|
||||||
|
|
||||||
|
case .PLAY:
|
||||||
|
progress = soundManager.getPlayerCurrentTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideView() {
|
||||||
|
if isShowSearchUser {
|
||||||
|
isShowSearchUser = false
|
||||||
|
}
|
||||||
|
|
||||||
|
soundManager.deleteAudioFile()
|
||||||
|
onRefresh()
|
||||||
|
AppState.shared.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func secondsToHoursMinutesSeconds(seconds: Int) -> String {
|
||||||
|
let hour = String(format: "%02d", seconds / 3600)
|
||||||
|
let minute = String(format: "%02d", (seconds % 3600) / 60)
|
||||||
|
let second = String(format: "%02d", (seconds % 3600) % 60)
|
||||||
|
|
||||||
|
return "\(hour):\(minute):\(second)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startTimer() {
|
||||||
|
timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopTimer() {
|
||||||
|
timer.upstream.connect().cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VoiceMessageWriteView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
VoiceMessageWriteView(onRefresh: {})
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ enum UserApi {
|
||||||
case login(request: LoginRequest)
|
case login(request: LoginRequest)
|
||||||
case signUp(parameters: [MultipartFormData])
|
case signUp(parameters: [MultipartFormData])
|
||||||
case findPassword(request: ForgotPasswordRequest)
|
case findPassword(request: ForgotPasswordRequest)
|
||||||
|
case searchUser(nickname: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UserApi: TargetType {
|
extension UserApi: TargetType {
|
||||||
|
@ -29,6 +30,9 @@ extension UserApi: TargetType {
|
||||||
|
|
||||||
case .findPassword:
|
case .findPassword:
|
||||||
return "/forgot-password"
|
return "/forgot-password"
|
||||||
|
|
||||||
|
case .searchUser:
|
||||||
|
return "/member/search"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +40,9 @@ extension UserApi: TargetType {
|
||||||
switch self {
|
switch self {
|
||||||
case .login, .signUp, .findPassword:
|
case .login, .signUp, .findPassword:
|
||||||
return .post
|
return .post
|
||||||
|
|
||||||
|
case .searchUser:
|
||||||
|
return .get
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +56,9 @@ extension UserApi: TargetType {
|
||||||
|
|
||||||
case .findPassword(let request):
|
case .findPassword(let request):
|
||||||
return .requestJSONEncodable(request)
|
return .requestJSONEncodable(request)
|
||||||
|
|
||||||
|
case .searchUser(let nickname):
|
||||||
|
return .requestParameters(parameters: ["nickname" : nickname], encoding: URLEncoding.queryString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,4 +24,8 @@ final class UserRepository {
|
||||||
func findPassword(email: String) -> AnyPublisher<Response, MoyaError> {
|
func findPassword(email: String) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.findPassword(request: ForgotPasswordRequest(email: email)))
|
return api.requestPublisher(.findPassword(request: ForgotPasswordRequest(email: email)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func searchUser(nickname: String) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.searchUser(nickname: nickname))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue