mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-01-17 11:08:06 +00:00
Test Table view for messages list
This commit is contained in:
parent
d8d867d798
commit
73d6f805d3
11 changed files with 1188 additions and 243 deletions
|
|
@ -46,6 +46,7 @@
|
|||
D726E43D2B19E4FE0083C415 /* StartCallFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */; };
|
||||
D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */; };
|
||||
D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */; };
|
||||
D72A9A052B9750A1000DC093 /* UIList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9A042B9750A1000DC093 /* UIList.swift */; };
|
||||
D732A9092AFD235500DB42BA /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9082AFD235500DB42BA /* ShareSheetController.swift */; };
|
||||
D732A90C2B0376F500DB42BA /* linphonerc-default in Resources */ = {isa = PBXBuildFile; fileRef = D732A90A2B0376F500DB42BA /* linphonerc-default */; };
|
||||
D732A90D2B0376F500DB42BA /* linphonerc-factory in Resources */ = {isa = PBXBuildFile; fileRef = D732A90B2B0376F500DB42BA /* linphonerc-factory */; };
|
||||
|
|
@ -99,6 +100,8 @@
|
|||
D7D24D182AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D122AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf */; };
|
||||
D7DA67622ACCB2FA00E95002 /* LoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */; };
|
||||
D7DA67642ACCB31700E95002 /* ProfileModeFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */; };
|
||||
D7E6ADF32B9875C20009A2BC /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6ADF22B9875C20009A2BC /* Message.swift */; };
|
||||
D7E6ADF52B9876ED0009A2BC /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6ADF42B9876ED0009A2BC /* Attachment.swift */; };
|
||||
D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */; };
|
||||
D7E6D04B2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D04A2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift */; };
|
||||
D7E6D04D2AEBD77600A57AAF /* CustomBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D04C2AEBD77600A57AAF /* CustomBottomSheet.swift */; };
|
||||
|
|
@ -151,6 +154,7 @@
|
|||
D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallFragment.swift; sourceTree = "<group>"; };
|
||||
D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallViewModel.swift; sourceTree = "<group>"; };
|
||||
D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryContactFragment.swift; sourceTree = "<group>"; };
|
||||
D72A9A042B9750A1000DC093 /* UIList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIList.swift; sourceTree = "<group>"; };
|
||||
D732A9082AFD235500DB42BA /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = "<group>"; };
|
||||
D732A90A2B0376F500DB42BA /* linphonerc-default */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-default"; sourceTree = "<group>"; };
|
||||
D732A90B2B0376F500DB42BA /* linphonerc-factory */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-factory"; sourceTree = "<group>"; };
|
||||
|
|
@ -205,6 +209,8 @@
|
|||
D7D24D122AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-ExtraBold.ttf"; sourceTree = "<group>"; };
|
||||
D7DA67612ACCB2FA00E95002 /* LoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFragment.swift; sourceTree = "<group>"; };
|
||||
D7DA67632ACCB31700E95002 /* ProfileModeFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileModeFragment.swift; sourceTree = "<group>"; };
|
||||
D7E6ADF22B9875C20009A2BC /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
|
||||
D7E6ADF42B9876ED0009A2BC /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
||||
D7E6D0482AE933AD00A57AAF /* FavoriteContactsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteContactsListFragment.swift; sourceTree = "<group>"; };
|
||||
D7E6D04A2AE9347D00A57AAF /* FavoriteContactsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteContactsListViewModel.swift; sourceTree = "<group>"; };
|
||||
D7E6D04C2AEBD77600A57AAF /* CustomBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBottomSheet.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -258,6 +264,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */,
|
||||
D7E6ADF22B9875C20009A2BC /* Message.swift */,
|
||||
D7E6ADF42B9876ED0009A2BC /* Attachment.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -576,6 +584,7 @@
|
|||
D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */,
|
||||
D70A26F12B7F5D95006CC8FC /* ConversationFragment.swift */,
|
||||
D71968912B86369D00DF4459 /* ChatBubbleView.swift */,
|
||||
D72A9A042B9750A1000DC093 /* UIList.swift */,
|
||||
);
|
||||
path = Fragments;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -786,6 +795,7 @@
|
|||
D75759322B56D40900E7AC10 /* ZRTPPopup.swift in Sources */,
|
||||
D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */,
|
||||
D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */,
|
||||
D7E6ADF52B9876ED0009A2BC /* Attachment.swift in Sources */,
|
||||
D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */,
|
||||
D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */,
|
||||
D7173EBE2B7A5C0A00BCC481 /* LinphoneUtils.swift in Sources */,
|
||||
|
|
@ -797,11 +807,13 @@
|
|||
D732A9092AFD235500DB42BA /* ShareSheetController.swift in Sources */,
|
||||
D72343362AD037AF009AA24E /* ToastView.swift in Sources */,
|
||||
D7FB55112AD447FD00A5AB15 /* RegisterFragment.swift in Sources */,
|
||||
D7E6ADF32B9875C20009A2BC /* Message.swift in Sources */,
|
||||
D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */,
|
||||
D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */,
|
||||
662B69D92B25DE18007118BF /* TelecomManager.swift in Sources */,
|
||||
D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */,
|
||||
66C491FB2B24D32600CEA16D /* CoreExtension.swift in Sources */,
|
||||
D72A9A052B9750A1000DC093 /* UIList.swift in Sources */,
|
||||
D726E43D2B19E4FE0083C415 /* StartCallFragment.swift in Sources */,
|
||||
D7B99E9B2B29F7C300BE7BF2 /* ActivityIndicator.swift in Sources */,
|
||||
D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -23,13 +23,17 @@ struct ChatBubbleView: View {
|
|||
|
||||
@ObservedObject var conversationViewModel: ConversationViewModel
|
||||
|
||||
let index: Int
|
||||
//let index: IndexPath
|
||||
|
||||
let message: Message
|
||||
|
||||
var body: some View {
|
||||
if index < conversationViewModel.conversationMessagesList.count
|
||||
/*
|
||||
if index < conversationViewModel.conversationMessagesList.count
|
||||
&& conversationViewModel.conversationMessagesList[index].eventLog.chatMessage != nil {
|
||||
VStack {
|
||||
if index == 0 && conversationViewModel.displayedConversationHistorySize > conversationViewModel.conversationMessagesList.count {
|
||||
//if index % 30 == 29 && conversationViewModel.displayedConversationHistorySize > conversationViewModel.conversationMessagesList.count {
|
||||
ProgressView()
|
||||
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
||||
.id(UUID())
|
||||
|
|
@ -57,9 +61,61 @@ struct ChatBubbleView: View {
|
|||
.padding(.trailing, !conversationViewModel.conversationMessagesList[index].eventLog.chatMessage!.isOutgoing ? 40 : 0)
|
||||
}
|
||||
}
|
||||
if conversationViewModel.conversationMessagesSection.count > index.section && conversationViewModel.conversationMessagesSection[index.section].rows.count > index.row {
|
||||
VStack {
|
||||
HStack {
|
||||
if message.isOutgoing {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
VStack {
|
||||
Text(message.text
|
||||
)
|
||||
.foregroundStyle(Color.grayMain2c700)
|
||||
.default_text_style(styleSize: 16)
|
||||
}
|
||||
.padding(.all, 15)
|
||||
.background(message.isOutgoing ? Color.orangeMain100 : Color.grayMain2c100)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
|
||||
if !message.isOutgoing {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(.leading, message.isOutgoing ? 40 : 0)
|
||||
.padding(.trailing, !message.isOutgoing ? 40 : 0)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
if message.isOutgoing {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
VStack {
|
||||
Text(message.text
|
||||
)
|
||||
.foregroundStyle(Color.grayMain2c700)
|
||||
.default_text_style(styleSize: 16)
|
||||
}
|
||||
.padding(.all, 15)
|
||||
.background(message.isOutgoing ? Color.orangeMain100 : Color.grayMain2c100)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
|
||||
if !message.isOutgoing {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(.leading, message.isOutgoing ? 40 : 0)
|
||||
.padding(.trailing, !message.isOutgoing ? 40 : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#Preview {
|
||||
ChatBubbleView(conversationViewModel: ConversationViewModel(), index: 0)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@ struct ConversationFragment: View {
|
|||
|
||||
@State var offset: CGPoint = .zero
|
||||
|
||||
private let ids: [String] = []
|
||||
|
||||
@State private var isScrolledToBottom: Bool = true
|
||||
var showMessageMenuOnLongPress: Bool = true
|
||||
|
||||
@StateObject private var viewModel = ChatViewModel()
|
||||
@StateObject private var paginationState = PaginationState()
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
GeometryReader { geometry in
|
||||
|
|
@ -142,223 +150,106 @@ struct ConversationFragment: View {
|
|||
.padding(.bottom, 4)
|
||||
.background(.white)
|
||||
|
||||
/*
|
||||
List {
|
||||
ForEach(0..<conversationViewModel.conversationMessagesList.count, id: \.self) { index in
|
||||
ChatBubbleView(conversationViewModel: conversationViewModel, index: index)
|
||||
.id(conversationViewModel.conversationMessagesList[index])
|
||||
.scaleEffect(x: 1, y: -1, anchor: .center)
|
||||
.listRowInsets(EdgeInsets(top: 2, leading: 10, bottom: 2, trailing: 10))
|
||||
.listRowSeparator(.hidden)
|
||||
.transition(.move(edge: .top))
|
||||
}
|
||||
}
|
||||
.scaleEffect(x: 1, y: -1, anchor: .center)
|
||||
.listStyle(.plain)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(.white)
|
||||
.onTapGesture {
|
||||
UIApplication.shared.endEditing()
|
||||
}
|
||||
.onDisappear {
|
||||
conversationViewModel.resetMessage()
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
ScrollViewReader { proxy in
|
||||
List {
|
||||
ForEach(0..<conversationViewModel.conversationMessagesList.count, id: \.self) { index in
|
||||
ChatBubbleView(conversationViewModel: conversationViewModel, index: index)
|
||||
.id(conversationViewModel.conversationMessagesList[index])
|
||||
.listRowInsets(EdgeInsets(top: 2, leading: 10, bottom: 2, trailing: 10))
|
||||
.listRowSeparator(.hidden)
|
||||
.onAppear {
|
||||
if index == 0 && conversationViewModel.displayedConversationHistorySize > conversationViewModel.conversationMessagesList.count {
|
||||
//DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
conversationViewModel.getOldMessages()
|
||||
//}
|
||||
if #available(iOS 16.0, *) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
list
|
||||
|
||||
if !isScrolledToBottom {
|
||||
Button {
|
||||
NotificationCenter.default.post(name: .onScrollToBottom, object: nil)
|
||||
} label: {
|
||||
ZStack {
|
||||
|
||||
Image("caret-down")
|
||||
.renderingMode(.template)
|
||||
.foregroundStyle(.white)
|
||||
.padding()
|
||||
.background(Color.orangeMain500)
|
||||
.clipShape(Circle())
|
||||
.shadow(color: .black.opacity(0.2), radius: 4)
|
||||
|
||||
if conversationViewModel.displayedConversationUnreadMessagesCount > 0 {
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Text(
|
||||
conversationViewModel.displayedConversationUnreadMessagesCount < 99
|
||||
? String(conversationViewModel.displayedConversationUnreadMessagesCount)
|
||||
: "99+"
|
||||
)
|
||||
.foregroundStyle(.white)
|
||||
.default_text_style(styleSize: 10)
|
||||
.lineLimit(1)
|
||||
|
||||
}
|
||||
.frame(width: 18, height: 18)
|
||||
.background(Color.redDanger500)
|
||||
.cornerRadius(50)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.frame(width: 50, height: 50)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.onTapGesture {
|
||||
UIApplication.shared.endEditing()
|
||||
}
|
||||
.onAppear {
|
||||
conversationViewModel.getMessages()
|
||||
}
|
||||
.onChange(of: conversationViewModel.conversationMessagesList) { _ in
|
||||
if conversationViewModel.conversationMessagesList.count <= 30 {
|
||||
proxy.scrollTo(
|
||||
conversationViewModel.conversationMessagesList.last, anchor: .top
|
||||
)
|
||||
} else if conversationViewModel.conversationMessagesList.count >= conversationViewModel.displayedConversationHistorySize {
|
||||
print("ChatBubbleViewChatBubbleView 1 "
|
||||
+ "\(conversationViewModel.conversationMessagesList.count) "
|
||||
+ "\(conversationViewModel.displayedConversationHistorySize - 30) "
|
||||
+ "\(conversationViewModel.conversationMessagesList.first?.eventLog.chatMessage!.utf8Text ?? "") "
|
||||
+ "\(conversationViewModel.conversationMessagesList[29].eventLog.chatMessage!.utf8Text ?? "")"
|
||||
)
|
||||
|
||||
proxy.scrollTo(
|
||||
conversationViewModel.conversationMessagesList[conversationViewModel.displayedConversationHistorySize%30], anchor: .top
|
||||
)
|
||||
} else {
|
||||
print("ChatBubbleViewChatBubbleView 2 "
|
||||
+ "\(conversationViewModel.conversationMessagesList.count) "
|
||||
+ "\(conversationViewModel.displayedConversationHistorySize - 30) "
|
||||
+ "\(conversationViewModel.conversationMessagesList.first?.eventLog.chatMessage!.utf8Text ?? "") "
|
||||
+ "\(conversationViewModel.conversationMessagesList[29].eventLog.chatMessage!.utf8Text ?? "")"
|
||||
)
|
||||
|
||||
proxy.scrollTo(30, anchor: .top)
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
conversationViewModel.resetMessage()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
GeometryReader { reader in
|
||||
} else {
|
||||
ScrollViewReader { proxy in
|
||||
if #available(iOS 17.0, *) {
|
||||
ScrollView(.vertical) {
|
||||
VStack(spacing: 4) {
|
||||
Spacer()
|
||||
ForEach(0..<conversationViewModel.conversationMessagesList.count, id: \.self) { index in
|
||||
ChatBubbleView(conversationViewModel: conversationViewModel, index: index)
|
||||
.id(conversationViewModel.conversationMessagesList[index])
|
||||
}
|
||||
}
|
||||
.frame(minHeight: reader.size.height)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.bottom, 8)
|
||||
.background(GeometryReader { geometry -> Color in
|
||||
DispatchQueue.main.async {
|
||||
//self.offset = -geometry.frame(in: .named("scroll")).origin.y
|
||||
let offsetMax = geometry.size.height - reader.size.height
|
||||
//print("ScrollOffsetPreferenceKey >> \(self.offset) \(offsetMax)")
|
||||
if -geometry.frame(in: .named("scroll")).origin.y <= 0 && self.offset > 0 {
|
||||
List {
|
||||
ForEach(0..<conversationViewModel.conversationMessagesList.count, id: \.self) { index in
|
||||
ChatBubbleView(conversationViewModel: conversationViewModel, message: conversationViewModel.conversationMessagesSection.first!.rows[index])
|
||||
.id(conversationViewModel.conversationMessagesList[index])
|
||||
.listRowInsets(EdgeInsets(top: 2, leading: 10, bottom: 2, trailing: 10))
|
||||
.listRowSeparator(.hidden)
|
||||
.onAppear {
|
||||
if index == 0 && conversationViewModel.displayedConversationHistorySize > conversationViewModel.conversationMessagesList.count {
|
||||
//DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
conversationViewModel.getOldMessages()
|
||||
print("ScrollOffsetPreferenceKey >> \(self.offset) \(-geometry.frame(in: .named("scroll")).origin.y) \(offsetMax)")
|
||||
//proxy.scrollTo(conversationViewModel.conversationMessagesList[19], anchor: .top)
|
||||
//}
|
||||
}
|
||||
self.offset = -geometry.frame(in: .named("scroll")).origin.y
|
||||
}
|
||||
return Color.clear
|
||||
})
|
||||
/*/
|
||||
.background(GeometryReader { geometry in
|
||||
Color.clear
|
||||
.preference(key: ScrollOffsetPreferenceKey.self, value: (geometry.frame(in: .named("scroll")).origin))
|
||||
})
|
||||
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
|
||||
//self.scrollOffset = value
|
||||
print("ScrollOffsetPreferenceKey \(value)")
|
||||
if value.y > 0 {
|
||||
print("ScrollOffsetPreferenceKey \(value) \(conversationViewModel.conversationMessagesList.count)")
|
||||
conversationViewModel.getOldMessages()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
.coordinateSpace(name: "scroll")
|
||||
.onTapGesture {
|
||||
UIApplication.shared.endEditing()
|
||||
}
|
||||
.onAppear {
|
||||
conversationViewModel.getMessages()
|
||||
}
|
||||
.onDisappear {
|
||||
conversationViewModel.resetMessage()
|
||||
}
|
||||
.defaultScrollAnchor(.bottom)
|
||||
} else {
|
||||
ScrollView(.vertical) {
|
||||
VStack {
|
||||
ForEach(0..<conversationViewModel.conversationMessagesList.count, id: \.self) { index in
|
||||
ChatBubbleView(conversationViewModel: conversationViewModel, index: index)
|
||||
.id(conversationViewModel.conversationMessagesList[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
UIApplication.shared.endEditing()
|
||||
}
|
||||
.onAppear {
|
||||
conversationViewModel.getMessages()
|
||||
}
|
||||
.onChange(of: conversationViewModel.conversationMessagesList) { _ in
|
||||
withAnimation {
|
||||
proxy.scrollTo(conversationViewModel.conversationMessagesList.last, anchor: .top)
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
conversationViewModel.resetMessage()
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.onTapGesture {
|
||||
UIApplication.shared.endEditing()
|
||||
}
|
||||
.onAppear {
|
||||
conversationViewModel.getMessages()
|
||||
}
|
||||
.onChange(of: conversationViewModel.conversationMessagesList) { _ in
|
||||
if conversationViewModel.conversationMessagesList.count <= 30 {
|
||||
proxy.scrollTo(
|
||||
conversationViewModel.conversationMessagesList.last, anchor: .top
|
||||
)
|
||||
} else if conversationViewModel.conversationMessagesList.count >= conversationViewModel.displayedConversationHistorySize {
|
||||
proxy.scrollTo(
|
||||
conversationViewModel.conversationMessagesList[conversationViewModel.displayedConversationHistorySize%30], anchor: .top
|
||||
)
|
||||
} else {
|
||||
proxy.scrollTo(30, anchor: .top)
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
conversationViewModel.resetMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
ScrollViewReader { proxy in
|
||||
if #available(iOS 17.0, *) {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(0..<conversationViewModel.conversationMessagesList.count, id: \.self) { index in
|
||||
ChatBubbleView(conversationViewModel: conversationViewModel, index: index)
|
||||
.id(conversationViewModel.conversationMessagesList[index])
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(.white)
|
||||
.onTapGesture {
|
||||
UIApplication.shared.endEditing()
|
||||
}
|
||||
.onAppear {
|
||||
conversationViewModel.getMessage()
|
||||
}
|
||||
.onDisappear {
|
||||
conversationViewModel.resetMessage()
|
||||
}
|
||||
}
|
||||
.defaultScrollAnchor(.bottom)
|
||||
} else {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(0..<conversationViewModel.conversationMessagesList.count, id: \.self) { index in
|
||||
ChatBubbleView(conversationViewModel: conversationViewModel, index: index)
|
||||
.id(conversationViewModel.conversationMessagesList[index])
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(.white)
|
||||
.onTapGesture {
|
||||
UIApplication.shared.endEditing()
|
||||
}
|
||||
.onAppear {
|
||||
conversationViewModel.getMessage()
|
||||
if conversationViewModel.conversationMessagesList.last != nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
proxy.scrollTo(conversationViewModel.conversationMessagesList.last!, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
conversationViewModel.resetMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Button {
|
||||
|
|
@ -384,7 +275,7 @@ struct ConversationFragment: View {
|
|||
.padding(.top, 4)
|
||||
}
|
||||
.padding(.horizontal, isMessageTextFocused ? 0 : 2)
|
||||
|
||||
|
||||
Button {
|
||||
} label: {
|
||||
Image("camera")
|
||||
|
|
@ -398,36 +289,35 @@ struct ConversationFragment: View {
|
|||
.padding(.horizontal, isMessageTextFocused ? 0 : 2)
|
||||
|
||||
HStack {
|
||||
if #available(iOS 16.0, *) {
|
||||
if #available(iOS 16.0, *) {
|
||||
TextField("Say something...", text: $conversationViewModel.messageText, axis: .vertical)
|
||||
.default_text_style(styleSize: 15)
|
||||
.focused($isMessageTextFocused)
|
||||
.padding(.vertical, 5)
|
||||
} else {
|
||||
ZStack(alignment: .leading) {
|
||||
TextEditor(text: $conversationViewModel.messageText)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxHeight: 160)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.default_text_style(styleSize: 15)
|
||||
.focused($isMessageTextFocused)
|
||||
|
||||
if conversationViewModel.messageText.isEmpty {
|
||||
Text("Say something...")
|
||||
.padding(.leading, 4)
|
||||
.opacity(conversationViewModel.messageText.isEmpty ? 1 : 0)
|
||||
.foregroundStyle(Color.gray300)
|
||||
.default_text_style(styleSize: 15)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
isMessageTextFocused = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ZStack(alignment: .leading) {
|
||||
TextEditor(text: $conversationViewModel.messageText)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxHeight: 160)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.default_text_style(styleSize: 15)
|
||||
.focused($isMessageTextFocused)
|
||||
|
||||
if conversationViewModel.messageText.isEmpty {
|
||||
Text("Say something...")
|
||||
.padding(.leading, 4)
|
||||
.opacity(conversationViewModel.messageText.isEmpty ? 1 : 0)
|
||||
.foregroundStyle(Color.gray300)
|
||||
.default_text_style(styleSize: 15)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
isMessageTextFocused = true
|
||||
}
|
||||
}
|
||||
|
||||
if conversationViewModel.messageText.isEmpty {
|
||||
Button {
|
||||
conversationViewModel.getOldMessages()
|
||||
} label: {
|
||||
Image("microphone")
|
||||
.renderingMode(.template)
|
||||
|
|
@ -439,6 +329,7 @@ struct ConversationFragment: View {
|
|||
}
|
||||
} else {
|
||||
Button {
|
||||
NotificationCenter.default.post(name: .onScrollToBottom, object: nil)
|
||||
conversationViewModel.sendMessage()
|
||||
} label: {
|
||||
Image("paper-plane-tilt")
|
||||
|
|
@ -488,6 +379,18 @@ struct ConversationFragment: View {
|
|||
}
|
||||
.navigationViewStyle(.stack)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var list: some View {
|
||||
UIList(viewModel: viewModel,
|
||||
paginationState: paginationState,
|
||||
conversationViewModel: conversationViewModel,
|
||||
isScrolledToBottom: $isScrolledToBottom,
|
||||
showMessageMenuOnLongPress: showMessageMenuOnLongPress,
|
||||
sections: conversationViewModel.conversationMessagesSection,
|
||||
ids: conversationViewModel.conversationMessagesIds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIApplication {
|
||||
|
|
@ -495,7 +398,9 @@ extension UIApplication {
|
|||
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
#Preview {
|
||||
ConversationFragment(conversationViewModel: ConversationViewModel(), conversationsListViewModel: ConversationsListViewModel())
|
||||
ConversationFragment(conversationViewModel: ConversationViewModel(), conversationsListViewModel: ConversationsListViewModel(), sections: [MessagesSection], ids: [""])
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -134,7 +134,14 @@ struct ConversationsListFragment: View {
|
|||
.background(.white)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
conversationViewModel.changeDisplayedChatRoom(conversationModel: conversationsListViewModel.conversationsList[index])
|
||||
if conversationViewModel.displayedConversation != nil {
|
||||
conversationViewModel.displayedConversation = nil
|
||||
conversationViewModel.resetMessage()
|
||||
conversationViewModel.changeDisplayedChatRoom(conversationModel: conversationsListViewModel.conversationsList[index])
|
||||
conversationViewModel.getMessages()
|
||||
} else {
|
||||
conversationViewModel.changeDisplayedChatRoom(conversationModel: conversationsListViewModel.conversationsList[index])
|
||||
}
|
||||
conversationsListViewModel.conversationsList[index].markAsRead()
|
||||
conversationsListViewModel.updateUnreadMessagesCount()
|
||||
}
|
||||
|
|
|
|||
30
Linphone/UI/Main/Conversations/Fragments/MessageMenu.swift
Normal file
30
Linphone/UI/Main/Conversations/Fragments/MessageMenu.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2023 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-iphone
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MessageMenu: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MessageMenu()
|
||||
}
|
||||
487
Linphone/UI/Main/Conversations/Fragments/UIList.swift
Normal file
487
Linphone/UI/Main/Conversations/Fragments/UIList.swift
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2023 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-iphone
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
import SwiftUI
|
||||
|
||||
public extension Notification.Name {
|
||||
static let onScrollToBottom = Notification.Name("onScrollToBottom")
|
||||
}
|
||||
|
||||
struct UIList: UIViewRepresentable {
|
||||
|
||||
@ObservedObject var viewModel: ChatViewModel
|
||||
@ObservedObject var paginationState: PaginationState
|
||||
@ObservedObject var conversationViewModel: ConversationViewModel
|
||||
|
||||
@Binding var isScrolledToBottom: Bool
|
||||
|
||||
let showMessageMenuOnLongPress: Bool
|
||||
let sections: [MessagesSection]
|
||||
let ids: [String]
|
||||
|
||||
@State private var isScrolledToTop = false
|
||||
|
||||
private let updatesQueue = DispatchQueue(label: "updatesQueue", qos: .utility)
|
||||
@State private var updateSemaphore = DispatchSemaphore(value: 1)
|
||||
@State private var tableSemaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
func makeUIView(context: Context) -> UITableView {
|
||||
let tableView = UITableView(frame: .zero, style: .grouped)
|
||||
tableView.contentInset = UIEdgeInsets(top: -10, left: 0, bottom: -20, right: 0)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.separatorStyle = .none
|
||||
tableView.dataSource = context.coordinator
|
||||
tableView.delegate = context.coordinator
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
|
||||
tableView.transform = CGAffineTransformMakeScale(1, -1)
|
||||
|
||||
tableView.showsVerticalScrollIndicator = true
|
||||
tableView.estimatedSectionHeaderHeight = 1
|
||||
tableView.estimatedSectionFooterHeight = UITableView.automaticDimension
|
||||
tableView.backgroundColor = UIColor(.white)
|
||||
tableView.scrollsToTop = true
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .onScrollToBottom, object: nil, queue: nil) { _ in
|
||||
DispatchQueue.main.async {
|
||||
if !context.coordinator.sections.isEmpty {
|
||||
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .bottom, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tableView
|
||||
}
|
||||
|
||||
func updateUIView(_ tableView: UITableView, context: Context) {
|
||||
if context.coordinator.sections == sections {
|
||||
return
|
||||
}
|
||||
updatesQueue.async {
|
||||
updateSemaphore.wait()
|
||||
|
||||
if context.coordinator.sections == sections {
|
||||
updateSemaphore.signal()
|
||||
return
|
||||
}
|
||||
|
||||
let prevSections = context.coordinator.sections
|
||||
let (appliedDeletes, appliedDeletesSwapsAndEdits, deleteOperations, swapOperations, editOperations, insertOperations) = operationsSplit(oldSections: prevSections, newSections: sections)
|
||||
|
||||
// step 1
|
||||
// preapare intermediate sections and operations
|
||||
//print("1 updateUIView sections:", "\n")
|
||||
//print("whole previous:\n", formatSections(prevSections), "\n")
|
||||
//print("whole appliedDeletes:\n", formatSections(appliedDeletes), "\n")
|
||||
//print("whole appliedDeletesSwapsAndEdits:\n", formatSections(appliedDeletesSwapsAndEdits), "\n")
|
||||
//print("whole final sections:\n", formatSections(sections), "\n")
|
||||
|
||||
//print("operations delete:\n", deleteOperations)
|
||||
//print("operations swap:\n", swapOperations)
|
||||
//print("operations edit:\n", editOperations)
|
||||
//print("operations insert:\n", insertOperations)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
tableView.performBatchUpdates {
|
||||
// step 2
|
||||
// delete sections and rows if necessary
|
||||
//print("2 apply delete")
|
||||
context.coordinator.sections = appliedDeletes
|
||||
for operation in deleteOperations {
|
||||
applyOperation(operation, tableView: tableView)
|
||||
}
|
||||
} completion: { _ in
|
||||
tableSemaphore.signal()
|
||||
//print("2 finished delete")
|
||||
}
|
||||
}
|
||||
tableSemaphore.wait()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
tableView.performBatchUpdates {
|
||||
// step 3
|
||||
// swap places for rows that moved inside the table
|
||||
// (example of how this happens. send two messages: first m1, then m2. if m2 is delivered to server faster, then it should jump above m1 even though it was sent later)
|
||||
//print("3 apply swaps")
|
||||
context.coordinator.sections = appliedDeletesSwapsAndEdits // NOTE: this array already contains necessary edits, but won't be a problem for appplying swaps
|
||||
for operation in swapOperations {
|
||||
applyOperation(operation, tableView: tableView)
|
||||
}
|
||||
} completion: { _ in
|
||||
tableSemaphore.signal()
|
||||
//print("3 finished swaps")
|
||||
}
|
||||
}
|
||||
tableSemaphore.wait()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
tableView.performBatchUpdates {
|
||||
// step 4
|
||||
// check only sections that are already in the table for existing rows that changed and apply only them to table's dataSource without animation
|
||||
//print("4 apply edits")
|
||||
context.coordinator.sections = appliedDeletesSwapsAndEdits
|
||||
for operation in editOperations {
|
||||
applyOperation(operation, tableView: tableView)
|
||||
}
|
||||
} completion: { _ in
|
||||
tableSemaphore.signal()
|
||||
//print("4 finished edits")
|
||||
}
|
||||
}
|
||||
tableSemaphore.wait()
|
||||
|
||||
if isScrolledToBottom || isScrolledToTop {
|
||||
DispatchQueue.main.sync {
|
||||
// step 5
|
||||
// apply the rest of the changes to table's dataSource, i.e. inserts
|
||||
//print("5 apply inserts")
|
||||
context.coordinator.sections = sections
|
||||
context.coordinator.ids = ids
|
||||
|
||||
tableView.beginUpdates()
|
||||
for operation in insertOperations {
|
||||
applyOperation(operation, tableView: tableView)
|
||||
}
|
||||
tableView.endUpdates()
|
||||
|
||||
updateSemaphore.signal()
|
||||
}
|
||||
} else {
|
||||
context.coordinator.ids = ids
|
||||
updateSemaphore.signal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Operations
|
||||
|
||||
enum Operation {
|
||||
case deleteSection(Int)
|
||||
case insertSection(Int)
|
||||
|
||||
case delete(Int, Int) // delete with animation
|
||||
case insert(Int, Int) // insert with animation
|
||||
case swap(Int, Int, Int) // delete first with animation, then insert it into new position with animation. do not do anything with the second for now
|
||||
case edit(Int, Int) // reload the element without animation
|
||||
}
|
||||
|
||||
func applyOperation(_ operation: Operation, tableView: UITableView) {
|
||||
switch operation {
|
||||
case .deleteSection(let section):
|
||||
tableView.deleteSections([section], with: .top)
|
||||
case .insertSection(let section):
|
||||
tableView.insertSections([section], with: .top)
|
||||
|
||||
case .delete(let section, let row):
|
||||
tableView.deleteRows(at: [IndexPath(row: row, section: section)], with: .top)
|
||||
case .insert(let section, let row):
|
||||
tableView.insertRows(at: [IndexPath(row: row, section: section)], with: .top)
|
||||
case .edit(let section, let row):
|
||||
tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
|
||||
case .swap(let section, let rowFrom, let rowTo):
|
||||
tableView.deleteRows(at: [IndexPath(row: rowFrom, section: section)], with: .top)
|
||||
tableView.insertRows(at: [IndexPath(row: rowTo, section: section)], with: .top)
|
||||
}
|
||||
}
|
||||
|
||||
func operationsSplit(oldSections: [MessagesSection], newSections: [MessagesSection]) -> ([MessagesSection], [MessagesSection], [Operation], [Operation], [Operation], [Operation]) {
|
||||
var appliedDeletes = oldSections // start with old sections, remove rows that need to be deleted
|
||||
var appliedDeletesSwapsAndEdits = newSections // take new sections and remove rows that need to be inserted for now, then we'll get array with all the changes except for inserts
|
||||
// appliedDeletesSwapsEditsAndInserts == newSection
|
||||
|
||||
var deleteOperations = [Operation]()
|
||||
var swapOperations = [Operation]()
|
||||
var editOperations = [Operation]()
|
||||
var insertOperations = [Operation]()
|
||||
|
||||
// 1 compare sections
|
||||
|
||||
let oldDates = oldSections.map { $0.date }
|
||||
let newDates = newSections.map { $0.date }
|
||||
let commonDates = Array(Set(oldDates + newDates)).sorted(by: >)
|
||||
for date in commonDates {
|
||||
let oldIndex = appliedDeletes.firstIndex(where: { $0.date == date } )
|
||||
let newIndex = appliedDeletesSwapsAndEdits.firstIndex(where: { $0.date == date } )
|
||||
if oldIndex == nil, let newIndex {
|
||||
// operationIndex is not the same as newIndex because appliedDeletesSwapsAndEdits is being changed as we go, but to apply changes to UITableView we should have initial index
|
||||
if let operationIndex = newSections.firstIndex(where: { $0.date == date } ) {
|
||||
appliedDeletesSwapsAndEdits.remove(at: newIndex)
|
||||
insertOperations.append(.insertSection(operationIndex))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if newIndex == nil, let oldIndex {
|
||||
if let operationIndex = oldSections.firstIndex(where: { $0.date == date } ) {
|
||||
appliedDeletes.remove(at: oldIndex)
|
||||
deleteOperations.append(.deleteSection(operationIndex))
|
||||
}
|
||||
continue
|
||||
}
|
||||
guard let newIndex, let oldIndex else { continue }
|
||||
|
||||
// 2 compare section rows
|
||||
// isolate deletes and inserts, and remove them from row arrays, leaving only rows that are in both arrays: 'duplicates'
|
||||
// this will allow to compare relative position changes of rows - swaps
|
||||
|
||||
var oldRows = appliedDeletes[oldIndex].rows
|
||||
var newRows = appliedDeletesSwapsAndEdits[newIndex].rows
|
||||
let oldRowIDs = Set(oldRows.map { $0.id })
|
||||
let newRowIDs = Set(newRows.map { $0.id })
|
||||
let rowIDsToDelete = oldRowIDs.subtracting(newRowIDs)
|
||||
let rowIDsToInsert = newRowIDs.subtracting(oldRowIDs) // TODO is order important?
|
||||
for rowId in rowIDsToDelete {
|
||||
if let index = oldRows.firstIndex(where: { $0.id == rowId }) {
|
||||
oldRows.remove(at: index)
|
||||
deleteOperations.append(.delete(oldIndex, index)) // this row was in old section, should not be in final result
|
||||
}
|
||||
}
|
||||
for rowId in rowIDsToInsert {
|
||||
if let index = newRows.firstIndex(where: { $0.id == rowId }) {
|
||||
// this row was not in old section, should add it to final result
|
||||
insertOperations.append(.insert(newIndex, index))
|
||||
}
|
||||
}
|
||||
|
||||
for rowId in rowIDsToInsert {
|
||||
if let index = newRows.firstIndex(where: { $0.id == rowId }) {
|
||||
// remove for now, leaving only 'duplicates'
|
||||
newRows.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
// 3 isolate swaps and edits
|
||||
|
||||
for row in 0..<oldRows.count {
|
||||
let oldRow = oldRows[row]
|
||||
let newRow = newRows[row]
|
||||
if oldRow.id != newRow.id { // a swap: rows in same position are not actually the same rows
|
||||
if let index = newRows.firstIndex(where: { $0.id == oldRow.id }) {
|
||||
if !swapsContain(swaps: swapOperations, section: oldIndex, index: row) ||
|
||||
!swapsContain(swaps: swapOperations, section: oldIndex, index: index) {
|
||||
swapOperations.append(.swap(oldIndex, row, index))
|
||||
}
|
||||
}
|
||||
} else if oldRow != newRow { // same ids om same positions but something changed - reload rows without animation
|
||||
editOperations.append(.edit(oldIndex, row))
|
||||
}
|
||||
}
|
||||
|
||||
// 4 store row changes in sections
|
||||
|
||||
appliedDeletes[oldIndex].rows = oldRows
|
||||
appliedDeletesSwapsAndEdits[newIndex].rows = newRows
|
||||
}
|
||||
|
||||
return (appliedDeletes, appliedDeletesSwapsAndEdits, deleteOperations, swapOperations, editOperations, insertOperations)
|
||||
}
|
||||
|
||||
func swapsContain(swaps: [Operation], section: Int, index: Int) -> Bool {
|
||||
!swaps.filter {
|
||||
if case let .swap(section, rowFrom, rowTo) = $0 {
|
||||
return section == section && (rowFrom == index || rowTo == index)
|
||||
}
|
||||
return false
|
||||
}.isEmpty
|
||||
}
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(
|
||||
conversationViewModel: conversationViewModel,
|
||||
viewModel: viewModel,
|
||||
paginationState: paginationState,
|
||||
isScrolledToBottom: $isScrolledToBottom,
|
||||
isScrolledToTop: $isScrolledToTop,
|
||||
showMessageMenuOnLongPress: showMessageMenuOnLongPress,
|
||||
sections: sections,
|
||||
ids: ids
|
||||
)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
|
||||
|
||||
@ObservedObject var viewModel: ChatViewModel
|
||||
@ObservedObject var paginationState: PaginationState
|
||||
@ObservedObject var conversationViewModel: ConversationViewModel
|
||||
|
||||
@Binding var isScrolledToBottom: Bool
|
||||
@Binding var isScrolledToTop: Bool
|
||||
|
||||
let showMessageMenuOnLongPress: Bool
|
||||
var sections: [MessagesSection]
|
||||
var ids: [String]
|
||||
|
||||
init(conversationViewModel: ConversationViewModel, viewModel: ChatViewModel, paginationState: PaginationState, isScrolledToBottom: Binding<Bool>, isScrolledToTop: Binding<Bool>, showMessageMenuOnLongPress: Bool, sections: [MessagesSection], ids: [String]) {
|
||||
self.conversationViewModel = conversationViewModel
|
||||
self.viewModel = viewModel
|
||||
self.paginationState = paginationState
|
||||
self._isScrolledToBottom = isScrolledToBottom
|
||||
self._isScrolledToTop = isScrolledToTop
|
||||
self.showMessageMenuOnLongPress = showMessageMenuOnLongPress
|
||||
self.sections = sections
|
||||
self.ids = ids
|
||||
}
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
sections.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
sections[section].rows.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||
return progressView(section)
|
||||
}
|
||||
|
||||
func progressView(_ section: Int) -> UIView? {
|
||||
if section > conversationViewModel.conversationMessagesSection.count
|
||||
&& conversationViewModel.conversationMessagesSection[section].rows.count < conversationViewModel.displayedConversationHistorySize {
|
||||
let header = UIHostingController(rootView:
|
||||
ProgressView()
|
||||
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
|
||||
).view
|
||||
header?.backgroundColor = UIColor(.white)
|
||||
return header
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
||||
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
|
||||
tableViewCell.selectionStyle = .none
|
||||
tableViewCell.backgroundColor = UIColor(.white)
|
||||
|
||||
let row = sections[indexPath.section].rows[indexPath.row]
|
||||
if #available(iOS 16.0, *) {
|
||||
tableViewCell.contentConfiguration = UIHostingConfiguration {
|
||||
ChatBubbleView(conversationViewModel: conversationViewModel, message: row)
|
||||
.padding(.vertical, 1)
|
||||
.padding(.horizontal, 10)
|
||||
.onTapGesture { }
|
||||
}
|
||||
.minSize(width: 0, height: 0)
|
||||
.margins(.all, 0)
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
|
||||
tableViewCell.transform = CGAffineTransformMakeScale(1, -1)
|
||||
|
||||
return tableViewCell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
let row = sections[indexPath.section].rows[indexPath.row]
|
||||
paginationState.handle(row, ids: ids)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
isScrolledToBottom = scrollView.contentOffset.y <= 10
|
||||
|
||||
if isScrolledToBottom && conversationViewModel.displayedConversationUnreadMessagesCount > 0 {
|
||||
conversationViewModel.markAsRead()
|
||||
}
|
||||
|
||||
if !isScrolledToTop && scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.height - 200 {
|
||||
self.conversationViewModel.getOldMessages()
|
||||
}
|
||||
isScrolledToTop = scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.height - 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MessagesSection: Equatable {
|
||||
|
||||
let date: Date
|
||||
var rows: [Message]
|
||||
|
||||
static var formatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEEE, MMMM d"
|
||||
return formatter
|
||||
}()
|
||||
|
||||
init(date: Date, rows: [Message]) {
|
||||
self.date = date
|
||||
self.rows = rows
|
||||
}
|
||||
|
||||
var formattedDate: String {
|
||||
MessagesSection.formatter.string(from: date)
|
||||
}
|
||||
|
||||
static func == (lhs: MessagesSection, rhs: MessagesSection) -> Bool {
|
||||
lhs.date == rhs.date && lhs.rows == rhs.rows
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class PaginationState: ObservableObject {
|
||||
var onEvent: ChatPaginationClosure?
|
||||
var offset: Int
|
||||
|
||||
var shouldHandlePagination: Bool {
|
||||
onEvent != nil
|
||||
}
|
||||
|
||||
init(onEvent: ChatPaginationClosure? = nil, offset: Int = 0) {
|
||||
self.onEvent = onEvent
|
||||
self.offset = offset
|
||||
}
|
||||
|
||||
func handle(_ message: Message, ids: [String]) {
|
||||
guard shouldHandlePagination else {
|
||||
return
|
||||
}
|
||||
if ids.prefix(offset + 1).contains(message.id) {
|
||||
onEvent?(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias ChatPaginationClosure = (Message) -> Void
|
||||
|
||||
final class ChatViewModel: ObservableObject {
|
||||
|
||||
@Published private(set) var fullscreenAttachmentItem: Optional<Attachment> = nil
|
||||
@Published var fullscreenAttachmentPresented = false
|
||||
|
||||
@Published var messageMenuRow: Message?
|
||||
|
||||
public var didSendMessage: (DraftMessage) -> Void = {_ in}
|
||||
|
||||
func presentAttachmentFullScreen(_ attachment: Attachment) {
|
||||
fullscreenAttachmentItem = attachment
|
||||
fullscreenAttachmentPresented = true
|
||||
}
|
||||
|
||||
func dismissAttachmentFullScreen() {
|
||||
fullscreenAttachmentPresented = false
|
||||
fullscreenAttachmentItem = nil
|
||||
}
|
||||
|
||||
func sendMessage(_ message: DraftMessage) {
|
||||
didSendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable large_tuple
|
||||
61
Linphone/UI/Main/Conversations/Model/Attachment.swift
Normal file
61
Linphone/UI/Main/Conversations/Model/Attachment.swift
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2023 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-iphone
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum AttachmentType: String, Codable {
|
||||
case image
|
||||
case video
|
||||
|
||||
public var title: String {
|
||||
switch self {
|
||||
case .image:
|
||||
return "Image"
|
||||
default:
|
||||
return "Video"
|
||||
}
|
||||
}
|
||||
|
||||
public init(mediaType: MediaType) {
|
||||
switch mediaType {
|
||||
case .image:
|
||||
self = .image
|
||||
default:
|
||||
self = .video
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Attachment: Codable, Identifiable, Hashable {
|
||||
public let id: String
|
||||
public let thumbnail: URL
|
||||
public let full: URL
|
||||
public let type: AttachmentType
|
||||
|
||||
public init(id: String, thumbnail: URL, full: URL, type: AttachmentType) {
|
||||
self.id = id
|
||||
self.thumbnail = thumbnail
|
||||
self.full = full
|
||||
self.type = type
|
||||
}
|
||||
|
||||
public init(id: String, url: URL, type: AttachmentType) {
|
||||
self.init(id: id, thumbnail: url, full: url, type: type)
|
||||
}
|
||||
}
|
||||
|
|
@ -88,7 +88,7 @@ class ConversationModel: ObservableObject {
|
|||
|
||||
//self.dateTime = chatRoom.date
|
||||
|
||||
self.unreadMessagesCount = chatRoom.unreadMessagesCount
|
||||
self.unreadMessagesCount = 0
|
||||
|
||||
self.avatarModel = ContactAvatarModel(friend: nil, name: "", withPresence: false)
|
||||
|
||||
|
|
@ -98,9 +98,10 @@ class ConversationModel: ObservableObject {
|
|||
|
||||
getContentTextMessage()
|
||||
getChatRoomSubject()
|
||||
getUnreadMessagesCount()
|
||||
}
|
||||
|
||||
func leave(){
|
||||
func leave() {
|
||||
coreContext.doOnCoreQueue { _ in
|
||||
self.chatRoom.leave()
|
||||
}
|
||||
|
|
@ -214,6 +215,13 @@ class ConversationModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
func getUnreadMessagesCount() {
|
||||
coreContext.doOnCoreQueue { _ in
|
||||
self.unreadMessagesCount = self.chatRoom.unreadMessagesCount
|
||||
}
|
||||
}
|
||||
|
||||
func refreshAvatarModel() {
|
||||
coreContext.doOnCoreQueue { _ in
|
||||
let addressFriend =
|
||||
|
|
|
|||
296
Linphone/UI/Main/Conversations/Model/Message.swift
Normal file
296
Linphone/UI/Main/Conversations/Model/Message.swift
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2023 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-iphone
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct Message: Identifiable, Hashable {
|
||||
|
||||
public enum Status: Equatable, Hashable {
|
||||
case sending
|
||||
case sent
|
||||
case read
|
||||
case error(DraftMessage)
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .sending:
|
||||
return hasher.combine("sending")
|
||||
case .sent:
|
||||
return hasher.combine("sent")
|
||||
case .read:
|
||||
return hasher.combine("read")
|
||||
case .error:
|
||||
return hasher.combine("error")
|
||||
}
|
||||
}
|
||||
|
||||
public static func == (lhs: Message.Status, rhs: Message.Status) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.sending, .sending):
|
||||
return true
|
||||
case (.sent, .sent):
|
||||
return true
|
||||
case (.read, .read):
|
||||
return true
|
||||
case ( .error(_), .error(_)):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var id: String
|
||||
public var status: Status?
|
||||
public var createdAt: Date
|
||||
public var isOutgoing: Bool
|
||||
|
||||
public var text: String
|
||||
public var attachments: [Attachment]
|
||||
public var recording: Recording?
|
||||
public var replyMessage: ReplyMessage?
|
||||
|
||||
public init(id: String,
|
||||
status: Status? = nil,
|
||||
createdAt: Date = Date(),
|
||||
isOutgoing: Bool,
|
||||
text: String = "",
|
||||
attachments: [Attachment] = [],
|
||||
recording: Recording? = nil,
|
||||
replyMessage: ReplyMessage? = nil) {
|
||||
|
||||
self.id = id
|
||||
self.status = status
|
||||
self.createdAt = createdAt
|
||||
self.isOutgoing = isOutgoing
|
||||
self.text = text
|
||||
self.attachments = attachments
|
||||
self.recording = recording
|
||||
self.replyMessage = replyMessage
|
||||
}
|
||||
|
||||
public static func makeMessage(
|
||||
id: String,
|
||||
status: Status? = nil,
|
||||
draft: DraftMessage) async -> Message {
|
||||
let attachments = await draft.medias.asyncCompactMap { media -> Attachment? in
|
||||
guard let thumbnailURL = await media.getThumbnailURL() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch media.type {
|
||||
case .image:
|
||||
return Attachment(id: UUID().uuidString, url: thumbnailURL, type: .image)
|
||||
case .video:
|
||||
guard let fullURL = await media.getURL() else {
|
||||
return nil
|
||||
}
|
||||
return Attachment(id: UUID().uuidString, thumbnail: thumbnailURL, full: fullURL, type: .video)
|
||||
}
|
||||
}
|
||||
|
||||
return Message(
|
||||
id: id,
|
||||
status: status,
|
||||
createdAt: draft.createdAt,
|
||||
isOutgoing: draft.isOutgoing,
|
||||
text: draft.text,
|
||||
attachments: attachments,
|
||||
recording: draft.recording,
|
||||
replyMessage: draft.replyMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Message {
|
||||
var time: String {
|
||||
DateFormatter.timeFormatter.string(from: createdAt)
|
||||
}
|
||||
}
|
||||
|
||||
extension Message: Equatable {
|
||||
public static func == (lhs: Message, rhs: Message) -> Bool {
|
||||
lhs.id == rhs.id && lhs.status == rhs.status
|
||||
}
|
||||
}
|
||||
|
||||
public struct Recording: Codable, Hashable {
|
||||
public var duration: Double
|
||||
public var waveformSamples: [CGFloat]
|
||||
public var url: URL?
|
||||
|
||||
public init(duration: Double = 0.0, waveformSamples: [CGFloat] = [], url: URL? = nil) {
|
||||
self.duration = duration
|
||||
self.waveformSamples = waveformSamples
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
|
||||
public struct ReplyMessage: Codable, Identifiable, Hashable {
|
||||
public static func == (lhs: ReplyMessage, rhs: ReplyMessage) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public var id: String
|
||||
|
||||
public var text: String
|
||||
public var isOutgoing: Bool
|
||||
public var attachments: [Attachment]
|
||||
public var recording: Recording?
|
||||
|
||||
public init(id: String,
|
||||
text: String = "",
|
||||
isOutgoing: Bool,
|
||||
attachments: [Attachment] = [],
|
||||
recording: Recording? = nil) {
|
||||
|
||||
self.id = id
|
||||
self.text = text
|
||||
self.isOutgoing = isOutgoing
|
||||
self.attachments = attachments
|
||||
self.recording = recording
|
||||
}
|
||||
|
||||
func toMessage() -> Message {
|
||||
Message(id: id, isOutgoing: isOutgoing, text: text, attachments: attachments, recording: recording)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Message {
|
||||
|
||||
func toReplyMessage() -> ReplyMessage {
|
||||
ReplyMessage(id: id, text: text, isOutgoing: isOutgoing, attachments: attachments, recording: recording)
|
||||
}
|
||||
}
|
||||
|
||||
public struct DraftMessage {
|
||||
public var id: String?
|
||||
public let isOutgoing: Bool
|
||||
public let text: String
|
||||
public let medias: [Media]
|
||||
public let recording: Recording?
|
||||
public let replyMessage: ReplyMessage?
|
||||
public let createdAt: Date
|
||||
|
||||
public init(id: String? = nil,
|
||||
isOutgoing: Bool,
|
||||
text: String,
|
||||
medias: [Media],
|
||||
recording: Recording?,
|
||||
replyMessage: ReplyMessage?,
|
||||
createdAt: Date) {
|
||||
self.id = id
|
||||
self.isOutgoing = isOutgoing
|
||||
self.text = text
|
||||
self.medias = medias
|
||||
self.recording = recording
|
||||
self.replyMessage = replyMessage
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
}
|
||||
|
||||
public enum MediaType {
|
||||
case image
|
||||
case video
|
||||
}
|
||||
|
||||
public struct Media: Identifiable, Equatable {
|
||||
public var id = UUID()
|
||||
internal let source: MediaModelProtocol
|
||||
|
||||
public static func == (lhs: Media, rhs: Media) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
public extension Media {
|
||||
|
||||
var type: MediaType {
|
||||
source.mediaType ?? .image
|
||||
}
|
||||
|
||||
var duration: CGFloat? {
|
||||
source.duration
|
||||
}
|
||||
|
||||
func getURL() async -> URL? {
|
||||
await source.getURL()
|
||||
}
|
||||
|
||||
func getThumbnailURL() async -> URL? {
|
||||
await source.getThumbnailURL()
|
||||
}
|
||||
|
||||
func getData() async -> Data? {
|
||||
try? await source.getData()
|
||||
}
|
||||
|
||||
func getThumbnailData() async -> Data? {
|
||||
await source.getThumbnailData()
|
||||
}
|
||||
}
|
||||
|
||||
protocol MediaModelProtocol {
|
||||
var mediaType: MediaType? { get }
|
||||
var duration: CGFloat? { get }
|
||||
|
||||
func getURL() async -> URL?
|
||||
func getThumbnailURL() async -> URL?
|
||||
|
||||
func getData() async throws -> Data?
|
||||
func getThumbnailData() async -> Data?
|
||||
}
|
||||
|
||||
extension Sequence {
|
||||
func asyncCompactMap<T>(
|
||||
_ transform: (Element) async throws -> T?
|
||||
) async rethrows -> [T] {
|
||||
var values = [T]()
|
||||
|
||||
for element in self {
|
||||
if let el = try await transform(element) {
|
||||
values.append(el)
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
extension DateFormatter {
|
||||
static let timeFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
|
||||
formatter.dateStyle = .none
|
||||
formatter.timeStyle = .short
|
||||
|
||||
return formatter
|
||||
}()
|
||||
|
||||
static func timeString(_ seconds: Int) -> String {
|
||||
let hour = Int(seconds) / 3600
|
||||
let minute = Int(seconds) / 60 % 60
|
||||
let second = Int(seconds) % 60
|
||||
|
||||
if hour > 0 {
|
||||
return String(format: "%02i:%02i:%02i", hour, minute, second)
|
||||
}
|
||||
return String(format: "%02i:%02i", minute, second)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,12 +28,16 @@ class ConversationViewModel: ObservableObject {
|
|||
|
||||
@Published var displayedConversation: ConversationModel?
|
||||
@Published var displayedConversationHistorySize: Int = 0
|
||||
@Published var displayedConversationUnreadMessagesCount: Int = 0
|
||||
|
||||
|
||||
@Published var messageText: String = ""
|
||||
|
||||
private var chatRoomSuscriptions = Set<AnyCancellable?>()
|
||||
|
||||
@Published var conversationMessagesList: [LinphoneCustomEventLog] = []
|
||||
@Published var conversationMessagesSection: [MessagesSection] = []
|
||||
@Published var conversationMessagesIds: [String] = []
|
||||
|
||||
init() {}
|
||||
|
||||
|
|
@ -66,26 +70,60 @@ class ConversationViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func getUnreadMessagesCount() {
|
||||
coreContext.doOnCoreQueue { _ in
|
||||
if self.displayedConversation != nil {
|
||||
let unreadMessagesCount = self.displayedConversation!.chatRoom.unreadMessagesCount
|
||||
DispatchQueue.main.async {
|
||||
self.displayedConversationUnreadMessagesCount = unreadMessagesCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markAsRead() {
|
||||
coreContext.doOnCoreQueue { _ in
|
||||
self.displayedConversation!.chatRoom.markAsRead()
|
||||
let unreadMessagesCount = self.displayedConversation!.chatRoom.unreadMessagesCount
|
||||
DispatchQueue.main.async {
|
||||
self.displayedConversationUnreadMessagesCount = unreadMessagesCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getMessages() {
|
||||
self.getHistorySize()
|
||||
self.getUnreadMessagesCount()
|
||||
coreContext.doOnCoreQueue { _ in
|
||||
if self.displayedConversation != nil {
|
||||
let historyEvents = self.displayedConversation!.chatRoom.getHistoryRangeEvents(begin: self.conversationMessagesList.count, end: self.conversationMessagesList.count + 30)
|
||||
|
||||
//For List
|
||||
/*
|
||||
historyEvents.reversed().forEach { eventLog in
|
||||
DispatchQueue.main.async {
|
||||
self.conversationMessagesList.append(LinphoneCustomEventLog(eventLog: eventLog))
|
||||
}
|
||||
}
|
||||
historyEvents.reversed().forEach { eventLog in
|
||||
DispatchQueue.main.async {
|
||||
self.conversationMessagesList.append(LinphoneCustomEventLog(eventLog: eventLog))
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
//For ScrollView
|
||||
historyEvents.forEach { eventLog in
|
||||
var conversationMessage: [Message] = []
|
||||
historyEvents.enumerated().forEach { index, eventLog in
|
||||
DispatchQueue.main.async {
|
||||
self.conversationMessagesList.append(LinphoneCustomEventLog(eventLog: eventLog))
|
||||
}
|
||||
conversationMessage.append(Message(
|
||||
id: UUID().uuidString,
|
||||
isOutgoing: eventLog.chatMessage?.isOutgoing ?? false,
|
||||
text: eventLog.chatMessage?.utf8Text ?? ""))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if index == historyEvents.count - 1 {
|
||||
self.conversationMessagesSection.append(MessagesSection(date: Date(), rows: conversationMessage.reversed()))
|
||||
self.conversationMessagesIds.append(UUID().uuidString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -103,38 +141,83 @@ class ConversationViewModel: ObservableObject {
|
|||
self.conversationMessagesList.append(LinphoneCustomEventLog(eventLog: eventLog))
|
||||
}
|
||||
}
|
||||
*/
|
||||
*/
|
||||
|
||||
//For ScrollView
|
||||
var conversationMessagesListTmp: [LinphoneCustomEventLog] = []
|
||||
var conversationMessagesTmp: [Message] = []
|
||||
|
||||
historyEvents.reversed().forEach { eventLog in
|
||||
conversationMessagesListTmp.insert(LinphoneCustomEventLog(eventLog: eventLog), at: 0)
|
||||
|
||||
conversationMessagesTmp.insert(
|
||||
Message(
|
||||
id: UUID().uuidString,
|
||||
isOutgoing: eventLog.chatMessage?.isOutgoing ?? false,
|
||||
text: eventLog.chatMessage?.utf8Text ?? ""
|
||||
), at: 0
|
||||
)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.conversationMessagesList.insert(contentsOf: conversationMessagesListTmp, at: 0)
|
||||
if !conversationMessagesTmp.isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
self.conversationMessagesList.insert(contentsOf: conversationMessagesListTmp, at: 0)
|
||||
//self.conversationMessagesSection.append(MessagesSection(date: Date(), rows: conversationMessagesTmp.reversed()))
|
||||
//self.conversationMessagesIds.append(UUID().uuidString)
|
||||
self.conversationMessagesSection[0].rows.append(contentsOf: conversationMessagesTmp.reversed())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getNewMessages(eventLogs: [EventLog]) {
|
||||
eventLogs.forEach { eventLog in
|
||||
var conversationMessage: [Message] = []
|
||||
eventLogs.enumerated().forEach { index, eventLog in
|
||||
DispatchQueue.main.async {
|
||||
withAnimation {
|
||||
//For List
|
||||
//self.conversationMessagesList.insert(LinphoneCustomEventLog(eventLog: eventLog), at: 0)
|
||||
|
||||
//For ScrollView
|
||||
self.conversationMessagesList.append(LinphoneCustomEventLog(eventLog: eventLog))
|
||||
//withAnimation {
|
||||
//For List
|
||||
//self.conversationMessagesList.insert(LinphoneCustomEventLog(eventLog: eventLog), at: 0)
|
||||
|
||||
//For ScrollView
|
||||
self.conversationMessagesList.append(LinphoneCustomEventLog(eventLog: eventLog))
|
||||
|
||||
/*
|
||||
conversationMessage.append(Message(
|
||||
id: UUID().uuidString,
|
||||
isOutgoing: eventLog.chatMessage?.isOutgoing ?? false,
|
||||
text: eventLog.chatMessage?.utf8Text ?? ""
|
||||
)
|
||||
)
|
||||
*/
|
||||
}
|
||||
let message = Message(
|
||||
id: UUID().uuidString,
|
||||
isOutgoing: eventLog.chatMessage?.isOutgoing ?? false,
|
||||
text: eventLog.chatMessage?.utf8Text ?? ""
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if self.conversationMessagesSection.isEmpty {
|
||||
self.conversationMessagesSection.append(MessagesSection(date: Date(), rows: [message]))
|
||||
} else {
|
||||
self.conversationMessagesSection[0].rows.insert(message, at: 0)
|
||||
}
|
||||
|
||||
if !message.isOutgoing {
|
||||
self.displayedConversationUnreadMessagesCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
if self.displayedConversation != nil {
|
||||
self.displayedConversation!.markAsRead()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resetMessage() {
|
||||
conversationMessagesList = []
|
||||
conversationMessagesSection = []
|
||||
}
|
||||
|
||||
func sendMessage() {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class ConversationsListViewModel: ObservableObject {
|
|||
|
||||
if !self.conversationsList.isEmpty {
|
||||
for (index, element) in conversationsListTmp.enumerated() {
|
||||
if index > 0 && element.id != self.conversationsList[index].id {
|
||||
if index > 0 && index < self.conversationsList.count && element.id != self.conversationsList[index].id {
|
||||
DispatchQueue.main.async {
|
||||
self.conversationsList[index] = element
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue