mv-experiment #1
|
@ -48,6 +48,7 @@
|
|||
// MARK: Conversation
|
||||
"Conversation.title" = "Conversation";
|
||||
"Conversation.startError" = "Error occurs in conversation starting";
|
||||
"Chat.textfieldPrompt" = "Type a message";
|
||||
|
||||
|
||||
|
||||
|
@ -57,8 +58,6 @@
|
|||
|
||||
|
||||
|
||||
//"Chat.textfieldPrompt" = "Type a message";
|
||||
|
||||
//"Chats.Create.Main.createGroup" = "Create public group";
|
||||
//"Chats.Create.Main.createPrivateGroup" = "Create private group";
|
||||
//"Chats.Create.Main.findGroup" = "Find public group";
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
import AVKit
|
||||
import MapKit
|
||||
import QuickLook
|
||||
import SwiftUI
|
||||
|
||||
struct ConversationMessageContainer: View {
|
||||
let message: Message
|
||||
let isOutgoing: Bool
|
||||
|
||||
var body: some View {
|
||||
if let msgText = message.body, msgText.isLocation {
|
||||
EmbededMapView(location: msgText.getLatLon)
|
||||
} else if let msgText = message.body, msgText.isContact {
|
||||
ContactView(message: message)
|
||||
// } else if message.attachmentType != nil {
|
||||
// AttachmentView(message: message)
|
||||
} else {
|
||||
Text(message.body ?? "...")
|
||||
.font(.body2)
|
||||
.foregroundColor(.Material.Text.main)
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageAttr: View {
|
||||
let message: Message
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(message.date, style: .time)
|
||||
.font(.sub2)
|
||||
.foregroundColor(.Material.Shape.separator)
|
||||
Spacer()
|
||||
if message.status == .error {
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.font(.body3)
|
||||
.foregroundColor(.Rainbow.red500)
|
||||
} else if message.status == .pending {
|
||||
Image(systemName: "clock")
|
||||
.font(.body3)
|
||||
.foregroundColor(.Material.Shape.separator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct EmbededMapView: View {
|
||||
let location: CLLocationCoordinate2D
|
||||
|
||||
var body: some View {
|
||||
Map(
|
||||
coordinateRegion: .constant(MKCoordinateRegion(center: location, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))),
|
||||
interactionModes: [],
|
||||
showsUserLocation: false,
|
||||
userTrackingMode: .none,
|
||||
annotationItems: [location],
|
||||
annotationContent: { _ in
|
||||
MapMarker(coordinate: location, tint: .blue)
|
||||
}
|
||||
)
|
||||
.frame(width: Const.mapPreviewSize, height: Const.mapPreviewSize)
|
||||
.onTapGesture {
|
||||
let mapItem = MKMapItem(placemark: MKPlacemark(coordinate: location))
|
||||
mapItem.name = "Location"
|
||||
mapItem.openInMaps(launchOptions: [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ContactView: View {
|
||||
let message: Message
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
Circle()
|
||||
.frame(width: 44, height: 44)
|
||||
.foregroundColor(contactName.firstLetterColor)
|
||||
Text(contactName.firstLetter)
|
||||
.foregroundColor(.white)
|
||||
.font(.body1)
|
||||
}
|
||||
Text(message.body?.getContactJid ?? "...")
|
||||
.font(.body2)
|
||||
.foregroundColor(.Material.Text.main)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
.padding()
|
||||
.onTapGesture {
|
||||
// TODO: Jump to add roster from here
|
||||
}
|
||||
}
|
||||
|
||||
private var contactName: String {
|
||||
message.body?.getContactJid ?? "?"
|
||||
}
|
||||
}
|
||||
|
||||
// private struct AttachmentView: View {
|
||||
// let message: Message
|
||||
//
|
||||
// var body: some View {
|
||||
// if message.attachmentDownloadFailed || (message.attachmentLocalName != nil && message.sentError) {
|
||||
// failed
|
||||
// } else {
|
||||
// switch message.attachmentType {
|
||||
// case .image:
|
||||
// if let thumbnail = thumbnail() {
|
||||
// thumbnail
|
||||
// .resizable()
|
||||
// .aspectRatio(contentMode: .fit)
|
||||
// .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
||||
// } else {
|
||||
// placeholder
|
||||
// }
|
||||
//
|
||||
// case .movie:
|
||||
// if let file = message.attachmentLocalPath {
|
||||
// VideoPlayerView(url: file)
|
||||
// .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
||||
// } else {
|
||||
// placeholder
|
||||
// }
|
||||
//
|
||||
// case .file:
|
||||
// if let file = message.attachmentLocalPath {
|
||||
// DocumentPreview(url: file)
|
||||
// .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
||||
// } else {
|
||||
// placeholder
|
||||
// }
|
||||
//
|
||||
// default:
|
||||
// placeholder
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @ViewBuilder private var placeholder: some View {
|
||||
// Rectangle()
|
||||
// .foregroundColor(.Material.Background.dark)
|
||||
// .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
||||
// .overlay {
|
||||
// ZStack {
|
||||
// ProgressView()
|
||||
// .scaleEffect(1.5)
|
||||
// .progressViewStyle(CircularProgressViewStyle(tint: .Material.Elements.active))
|
||||
// let imageName = progressImageName(message.attachmentType ?? .file)
|
||||
// Image(systemName: imageName)
|
||||
// .font(.body1)
|
||||
// .foregroundColor(.Material.Elements.active)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @ViewBuilder private var failed: some View {
|
||||
// Rectangle()
|
||||
// .foregroundColor(.Material.Background.dark)
|
||||
// .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
||||
// .overlay {
|
||||
// ZStack {
|
||||
// VStack {
|
||||
// Text(L10n.Attachment.Downloading.retry)
|
||||
// .font(.body3)
|
||||
// .foregroundColor(.Rainbow.red500)
|
||||
// Image(systemName: "exclamationmark.arrow.triangle.2.circlepath")
|
||||
// .font(.body1)
|
||||
// .foregroundColor(.Rainbow.red500)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .onTapGesture {
|
||||
// if let url = message.attachmentRemotePath {
|
||||
// store.dispatch(.fileAction(.downloadAttachmentFile(messageId: message.id, attachmentRemotePath: url)))
|
||||
// } else if message.attachmentLocalName != nil && message.sentError {
|
||||
// store.dispatch(.sharingAction(.retrySharing(messageId: message.id)))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func progressImageName(_ type: MessageAttachmentType) -> String {
|
||||
// switch type {
|
||||
// case .image:
|
||||
// return "photo"
|
||||
// case .audio:
|
||||
// return "music.note"
|
||||
// case .movie:
|
||||
// return "film"
|
||||
// case .file:
|
||||
// return "doc"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func thumbnail() -> Image? {
|
||||
// guard let thumbnailPath = message.attachmentThumbnailPath else { return nil }
|
||||
// guard let uiImage = UIImage(contentsOfFile: thumbnailPath.path()) else { return nil }
|
||||
// return Image(uiImage: uiImage)
|
||||
// }
|
||||
// }
|
||||
|
||||
// TODO: Make video player better!
|
||||
private struct VideoPlayerView: UIViewControllerRepresentable {
|
||||
let url: URL
|
||||
|
||||
func makeUIViewController(context _: Context) -> AVPlayerViewController {
|
||||
let controller = AVPlayerViewController()
|
||||
controller.player = AVPlayer(url: url)
|
||||
controller.allowsPictureInPicturePlayback = true
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_: AVPlayerViewController, context _: Context) {
|
||||
// Update the controller if needed.
|
||||
}
|
||||
}
|
||||
|
||||
struct DocumentPreview: UIViewControllerRepresentable {
|
||||
var url: URL
|
||||
|
||||
func makeUIViewController(context: Context) -> QLPreviewController {
|
||||
let controller = QLPreviewController()
|
||||
controller.dataSource = context.coordinator
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_: QLPreviewController, context _: Context) {
|
||||
// Update the controller if needed.
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, QLPreviewControllerDataSource {
|
||||
var parent: DocumentPreview
|
||||
|
||||
init(_ parent: DocumentPreview) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func numberOfPreviewItems(in _: QLPreviewController) -> Int {
|
||||
1
|
||||
}
|
||||
|
||||
func previewController(_: QLPreviewController, previewItemAt _: Int) -> QLPreviewItem {
|
||||
parent.url as QLPreviewItem
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct ConversationMessageRow: View {
|
||||
@EnvironmentObject var conversation: ConversationStore
|
||||
let message: Message
|
||||
@State private var offset: CGSize = .zero
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
if isOutgoing() {
|
||||
Spacer()
|
||||
MessageAttr(message: message)
|
||||
.padding(.trailing, 4)
|
||||
}
|
||||
ConversationMessageContainer(message: message, isOutgoing: isOutgoing())
|
||||
.background(isOutgoing() ? Color.Material.Shape.alternate : Color.Material.Shape.white)
|
||||
.clipShape(ConversationMessageBubble(isOutgoing: isOutgoing()))
|
||||
if !isOutgoing() {
|
||||
MessageAttr(message: message)
|
||||
.padding(.leading, 4)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 16)
|
||||
.background(Color.clearTappable)
|
||||
.offset(offset)
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 30, coordinateSpace: .local)
|
||||
.onChanged { value in
|
||||
var width = value.translation.width
|
||||
width = width > 0 ? 0 : width
|
||||
offset = CGSize(width: width, height: 0)
|
||||
}
|
||||
.onEnded { value in
|
||||
let targetWidth: CGFloat = -90
|
||||
withAnimation(.easeOut(duration: 0.1)) {
|
||||
if value.translation.width <= targetWidth {
|
||||
Vibration.success.vibrate()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
||||
withAnimation(.easeOut(duration: 0.1)) {
|
||||
offset = .zero
|
||||
}
|
||||
}
|
||||
} else {
|
||||
offset = .zero
|
||||
}
|
||||
}
|
||||
if value.translation.width <= targetWidth {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
|
||||
// store.dispatch(.conversationAction(.setReplyText(message.body ?? "")))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.listRowInsets(.zero)
|
||||
.listRowSeparator(.hidden)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.Material.Background.light)
|
||||
}
|
||||
|
||||
private func isOutgoing() -> Bool {
|
||||
message.from == conversation.roster.bareJid
|
||||
}
|
||||
}
|
||||
|
||||
struct ConversationMessageBubble: Shape {
|
||||
let isOutgoing: Bool
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let path = UIBezierPath(
|
||||
roundedRect: rect,
|
||||
byRoundingCorners: isOutgoing ? [.topLeft, .bottomLeft, .bottomRight] : [.topRight, .bottomLeft, .bottomRight],
|
||||
cornerRadii: CGSize(width: 8, height: 10)
|
||||
)
|
||||
return Path(path.cgPath)
|
||||
}
|
||||
}
|
|
@ -32,39 +32,38 @@ struct ConversationScreen: View {
|
|||
// Msg list
|
||||
let messages = conversation.messages
|
||||
if !messages.isEmpty {
|
||||
ScrollViewReader { _ in
|
||||
ScrollViewReader { proxy in
|
||||
List {
|
||||
Text("Test")
|
||||
// ForEach(messages) { message in
|
||||
// ConversationMessageRow(message: message)
|
||||
// .id(message.id)
|
||||
// .onAppear {
|
||||
// if message.id == messages.first?.id {
|
||||
// firstIsVisible = true
|
||||
// autoScroll = true
|
||||
// }
|
||||
// }
|
||||
// .onDisappear {
|
||||
// if message.id == messages.first?.id {
|
||||
// firstIsVisible = false
|
||||
// autoScroll = false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .rotationEffect(.degrees(180))
|
||||
ForEach(messages) { message in
|
||||
ConversationMessageRow(message: message)
|
||||
.id(message.id)
|
||||
.onAppear {
|
||||
if message.id == messages.first?.id {
|
||||
firstIsVisible = true
|
||||
autoScroll = true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if message.id == messages.first?.id {
|
||||
firstIsVisible = false
|
||||
autoScroll = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.rotationEffect(.degrees(180))
|
||||
}
|
||||
.rotationEffect(.degrees(180))
|
||||
.listStyle(.plain)
|
||||
.background(Color.Material.Background.light)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.scrollIndicators(.hidden)
|
||||
// .onChange(of: autoScroll) { new in
|
||||
// if new, !firstIsVisible {
|
||||
// withAnimation {
|
||||
// proxy.scrollTo(messages.first?.id, anchor: .top)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
.onChange(of: autoScroll) { new in
|
||||
if new, !firstIsVisible {
|
||||
withAnimation {
|
||||
proxy.scrollTo(messages.first?.id, anchor: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Spacer()
|
||||
|
@ -98,9 +97,10 @@ struct ConversationScreen: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
// .safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
// ConversationTextInput(autoScroll: $autoScroll)
|
||||
// }
|
||||
.environmentObject(conversation)
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
ConversationTextInput(autoScroll: $autoScroll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct ConversationTextInput: View {
|
||||
@State private var messageStr = ""
|
||||
@FocusState private var isFocused: Bool
|
||||
@Binding var autoScroll: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Rectangle()
|
||||
.foregroundColor(.Material.Shape.separator)
|
||||
.frame(height: 0.5)
|
||||
.padding(.bottom, 8)
|
||||
if !replyText.isEmpty {
|
||||
VStack(spacing: 0) {
|
||||
HStack(alignment: .top) {
|
||||
Text(replyText)
|
||||
.font(.body3)
|
||||
.foregroundColor(Color.Material.Text.main)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(3)
|
||||
.padding(8)
|
||||
Spacer()
|
||||
Image(systemName: "xmark")
|
||||
.font(.title2)
|
||||
.foregroundColor(.Material.Elements.active)
|
||||
.padding(.leading, 8)
|
||||
.tappablePadding(.symmetric(8)) {
|
||||
// store.dispatch(.conversationAction(.setReplyText("")))
|
||||
}
|
||||
.padding(8)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(RoundedRectangle(cornerRadius: 4)
|
||||
.foregroundColor(.Material.Background.light)
|
||||
.shadow(radius: 0.5)
|
||||
)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
HStack {
|
||||
Image(systemName: "paperclip")
|
||||
.font(.title2)
|
||||
.foregroundColor(.Material.Elements.active)
|
||||
.padding(.leading, 8)
|
||||
.tappablePadding(.symmetric(8)) {
|
||||
// store.dispatch(.sharingAction(.showSharing(true)))
|
||||
}
|
||||
TextField("", text: $messageStr, prompt: Text(L10n.Chat.textfieldPrompt).foregroundColor(.Material.Shape.separator))
|
||||
.font(.body1)
|
||||
.foregroundColor(Color.Material.Text.main)
|
||||
.accentColor(.Material.Shape.black)
|
||||
.focused($isFocused)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.Material.Shape.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.padding(.vertical, 4)
|
||||
let img = messageStr.isEmpty ? "paperplane" : "paperplane.fill"
|
||||
Image(systemName: img)
|
||||
.font(.title2)
|
||||
.foregroundColor(messageStr.isEmpty ? .Material.Elements.inactive : .Material.Elements.active)
|
||||
.padding(.trailing, 8)
|
||||
.tappablePadding(.symmetric(8)) {
|
||||
if !messageStr.isEmpty {
|
||||
// guard let acc = store.state.conversationsState.currentChat?.account else { return }
|
||||
// guard let contact = store.state.conversationsState.currentChat?.participant else { return }
|
||||
// store.dispatch(.conversationAction(.sendMessage(
|
||||
// from: acc,
|
||||
// to: contact,
|
||||
// body: composedMessage
|
||||
// )))
|
||||
// messageStr = ""
|
||||
// // UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
// store.dispatch(.conversationAction(.setReplyText("")))
|
||||
// autoScroll = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
.background(Color.Material.Background.dark)
|
||||
// .onChange(of: store.state.conversationsState.replyText) { new in
|
||||
// if !new.isEmpty {
|
||||
// isFocused = true
|
||||
// }
|
||||
// }
|
||||
// .fullScreenCover(isPresented: Binding<Bool>(
|
||||
// get: { store.state.sharingState.sharingShown },
|
||||
// set: { _ in }
|
||||
// )) {
|
||||
// AttachmentPickerScreen()
|
||||
// }
|
||||
}
|
||||
|
||||
private var replyText: String {
|
||||
""
|
||||
// store.state.conversationsState.replyText
|
||||
}
|
||||
|
||||
private var composedMessage: String {
|
||||
var result = ""
|
||||
if !replyText.isEmpty {
|
||||
result += replyText + "\n\n"
|
||||
}
|
||||
result += messageStr
|
||||
return result
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue