mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-01-28 08:39:20 +00:00
Merge branch 'feature/search_chat_message' into 'master'
Search chat message See merge request BC/public/linphone-iphone!397
This commit is contained in:
commit
888b698da0
33 changed files with 861 additions and 338 deletions
|
|
@ -108,12 +108,11 @@ class CoreContext: ObservableObject {
|
|||
DispatchQueue.main.async {
|
||||
if isConnected {
|
||||
Log.info("Network is now satisfied")
|
||||
ToastViewModel.shared.toastMessage = "Success_toast_network_connected"
|
||||
ToastViewModel.shared.show("Success_toast_network_connected")
|
||||
} else {
|
||||
Log.error("Network is now \(path.status)")
|
||||
ToastViewModel.shared.toastMessage = "Unavailable_network"
|
||||
ToastViewModel.shared.show("Unavailable_network")
|
||||
}
|
||||
ToastViewModel.shared.displayToast = true
|
||||
}
|
||||
self.networkStatusIsConnected = isConnected
|
||||
}
|
||||
|
|
@ -317,14 +316,11 @@ class CoreContext: ObservableObject {
|
|||
Log.info("[CoreContext] Transferred call \(transferred.remoteAddress!.asStringUriOnly()) state changed \(callState)")
|
||||
DispatchQueue.main.async {
|
||||
if callState == Call.State.Connected {
|
||||
ToastViewModel.shared.toastMessage = "Success_toast_call_transfer_successful"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_toast_call_transfer_successful")
|
||||
} else if callState == Call.State.OutgoingProgress {
|
||||
ToastViewModel.shared.toastMessage = "Success_toast_call_transfer_in_progress"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_toast_call_transfer_in_progress")
|
||||
} else if callState == Call.State.End || callState == Call.State.Error {
|
||||
ToastViewModel.shared.toastMessage = "Failed_toast_call_transfer_failed"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_toast_call_transfer_failed")
|
||||
}
|
||||
}
|
||||
}, onConfiguringStatus: { (_: Core, status: ConfiguringState, message: String) in
|
||||
|
|
@ -346,8 +342,7 @@ class CoreContext: ObservableObject {
|
|||
if info.starts(with: "https") {
|
||||
DispatchQueue.main.async {
|
||||
UIPasteboard.general.setValue(info, forPasteboardType: UTType.plainText.identifier)
|
||||
ToastViewModel.shared.toastMessage = "Success_send_logs"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_send_logs")
|
||||
}
|
||||
}
|
||||
}, onAccountRegistrationStateChanged: { (core: Core, account: Account, state: RegistrationState, message: String) in
|
||||
|
|
@ -397,8 +392,7 @@ class CoreContext: ObservableObject {
|
|||
self.loggedIn = false
|
||||
if self.networkStatusIsConnected {
|
||||
// If network is disconnected, a toast message with key "Unavailable_network" should already be displayed
|
||||
ToastViewModel.shared.toastMessage = "Registration_failed"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Registration_failed")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,8 +251,11 @@
|
|||
"conversation_info_participant_is_admin_label" = "Admin";
|
||||
"conversation_info_participants_list_title" = "Group members (%d)";
|
||||
"conversation_invalid_participant_due_to_security_mode_toast" = "Can't create conversation with a participant not on the same domain due to security restrictions!";
|
||||
"conversation_menu_configure_ephemeral_messages" = "Ephemeral messages";
|
||||
"conversation_menu_search_in_messages" = "Search";
|
||||
"conversation_menu_go_to_info" = "Conversation info";
|
||||
"conversation_menu_configure_ephemeral_messages" = "Ephemeral messages";
|
||||
"conversation_menu_media_files" = "Media";
|
||||
"conversation_menu_documents_files" = "Documents";
|
||||
"conversation_message_forward_cancelled_toast" = "Message forward was cancelled";
|
||||
"conversation_message_forwarded_toast" = "Message was forwarded";
|
||||
"conversation_message_meeting_cancelled_label" = "Meeting has been cancelled!";
|
||||
|
|
@ -261,6 +264,8 @@
|
|||
"conversation_participants_list_empty" = "No participants found";
|
||||
"conversation_participants_list_header" = "Participants";
|
||||
"conversation_reply_to_message_title" = "Replying to: ";
|
||||
"conversation_search_no_match_found" = "No matching result found";
|
||||
"conversation_search_results_limit_reached_label" = "Search results limit reached, refine your search";
|
||||
"conversation_text_field_hint" = "Say something…";
|
||||
"conversations_list_empty" = "No conversation for the moment…";
|
||||
"conversation_take_picture_label" = "Take picture";
|
||||
|
|
|
|||
|
|
@ -251,8 +251,11 @@
|
|||
"conversation_info_participant_is_admin_label" = "Administrateur";
|
||||
"conversation_info_participants_list_title" = "Participants (%d)";
|
||||
"conversation_invalid_participant_due_to_security_mode_toast" = "Pour des raisons de sécurité, la création d'une conversation avec un participant d'un domaine tiers est désactivé.";
|
||||
"conversation_menu_configure_ephemeral_messages" = "Messages éphémères";
|
||||
"conversation_menu_search_in_messages" = "Chercher";
|
||||
"conversation_menu_go_to_info" = "Informations";
|
||||
"conversation_menu_configure_ephemeral_messages" = "Messages éphémères";
|
||||
"conversation_menu_media_files" = "Médias";
|
||||
"conversation_menu_documents_files" = "Documents";
|
||||
"conversation_message_forward_cancelled_toast" = "Transfert annulé";
|
||||
"conversation_message_forwarded_toast" = "Message transféré";
|
||||
"conversation_message_meeting_cancelled_label" = "La réunion a été annulée";
|
||||
|
|
@ -261,6 +264,8 @@
|
|||
"conversation_participants_list_empty" = "Aucun participant trouvé";
|
||||
"conversation_participants_list_header" = "Participants";
|
||||
"conversation_reply_to_message_title" = "En réponse à : ";
|
||||
"conversation_search_no_match_found" = "Aucun résultat trouvé";
|
||||
"conversation_search_results_limit_reached_label" = "Nombre maximal de résultats atteint, affinez votre recherche";
|
||||
"conversation_text_field_hint" = "Dites quelque chose…";
|
||||
"conversations_list_empty" = "Aucune conversation pour le moment…";
|
||||
"conversation_take_picture_label" = "Prendre une photo";
|
||||
|
|
|
|||
|
|
@ -493,38 +493,36 @@ class TelecomManager: ObservableObject {
|
|||
|
||||
let isRecordingByRemoteTmp = call.remoteParams?.isRecording ?? false
|
||||
|
||||
if isRecordingByRemoteTmp && ToastViewModel.shared.toastMessage.isEmpty {
|
||||
|
||||
var displayName = ""
|
||||
let friend = ContactsManager.shared.getFriendWithAddress(address: call.remoteAddress!)
|
||||
if friend != nil && friend!.address != nil && friend!.address!.displayName != nil {
|
||||
displayName = friend!.address!.displayName!
|
||||
} else {
|
||||
if call.remoteAddress!.displayName != nil {
|
||||
displayName = call.remoteAddress!.displayName!
|
||||
} else if call.remoteAddress!.username != nil {
|
||||
displayName = call.remoteAddress!.username!
|
||||
} else {
|
||||
displayName = String(call.remoteAddress!.asStringUriOnly().dropFirst(4))
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.isRecordingByRemote = isRecordingByRemoteTmp
|
||||
ToastViewModel.shared.toastMessage = "\(displayName) is recording"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
}
|
||||
|
||||
Log.info("[Call] Call is recording by \(call.remoteAddress!.asStringUriOnly())")
|
||||
let displayName: String
|
||||
let friend = ContactsManager.shared.getFriendWithAddress(address: call.remoteAddress)
|
||||
|
||||
if let name = friend?.address?.displayName {
|
||||
displayName = name
|
||||
} else if let name = call.remoteAddress?.displayName {
|
||||
displayName = name
|
||||
} else if let username = call.remoteAddress?.username {
|
||||
displayName = username
|
||||
} else if let uri = call.remoteAddress?.asStringUriOnly() {
|
||||
displayName = String(uri.dropFirst(4))
|
||||
} else {
|
||||
displayName = "Unknown"
|
||||
}
|
||||
|
||||
if !isRecordingByRemoteTmp && ToastViewModel.shared.toastMessage.contains("is recording") {
|
||||
DispatchQueue.main.async {
|
||||
self.isRecordingByRemote = isRecordingByRemoteTmp
|
||||
ToastViewModel.shared.toastMessage = ""
|
||||
ToastViewModel.shared.displayToast = false
|
||||
DispatchQueue.main.async {
|
||||
self.isRecordingByRemote = isRecordingByRemoteTmp
|
||||
|
||||
if isRecordingByRemoteTmp {
|
||||
ToastViewModel.shared.show("\(displayName) is recording")
|
||||
} else if let toast = ToastViewModel.shared.toast,
|
||||
toast.message.contains("is recording") {
|
||||
ToastViewModel.shared.hide()
|
||||
}
|
||||
Log.info("[Call] Recording is stopped by \(call.remoteAddress!.asStringUriOnly())")
|
||||
}
|
||||
|
||||
if isRecordingByRemoteTmp {
|
||||
Log.info("[Call] Call is recording by \(call.remoteAddress?.asStringUriOnly() ?? "")")
|
||||
} else {
|
||||
Log.info("[Call] Recording is stopped by \(call.remoteAddress?.asStringUriOnly() ?? "")")
|
||||
}
|
||||
|
||||
if cstate == Call.State.PausedByRemote {
|
||||
|
|
|
|||
|
|
@ -41,8 +41,7 @@ class AccountLoginViewModel: ObservableObject {
|
|||
guard self.coreContext.networkStatusIsConnected else {
|
||||
DispatchQueue.main.async {
|
||||
self.coreContext.loggingInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Unavailable_network"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Unavailable_network")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,15 +75,18 @@ class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
|
|||
core.stop()
|
||||
try? core.start()
|
||||
}
|
||||
ToastViewModel.shared.toastMessage = "Success_qr_code_validated"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.show("Success_qr_code_validated")
|
||||
}
|
||||
} else {
|
||||
ToastViewModel.shared.toastMessage = "Invalide URI"
|
||||
ToastViewModel.shared.displayToast.toggle()
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.show("Invalide URI")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ToastViewModel.shared.toastMessage = "Invalide URI"
|
||||
ToastViewModel.shared.displayToast.toggle()
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.show("Invalide URI")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,8 +168,7 @@ class RegisterViewModel: ObservableObject {
|
|||
|
||||
if !errorMessage.isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Error: \(errorMessage)"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Error: \(errorMessage)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -341,8 +340,7 @@ class RegisterViewModel: ObservableObject {
|
|||
DispatchQueue.main.async {
|
||||
self.createInProgress = false
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Failed_push_notification_not_received_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_push_notification_not_received_error")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -430,8 +428,7 @@ class RegisterViewModel: ObservableObject {
|
|||
Log.error("\(RegisterViewModel.TAG) Account manager services hasn't been initialized!")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Failed_account_register_unexpected_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_account_register_unexpected_error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -672,8 +672,7 @@ struct CallView: View {
|
|||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_address_copied_into_clipboard")
|
||||
}
|
||||
}, label: {
|
||||
HStack {
|
||||
|
|
|
|||
|
|
@ -976,8 +976,7 @@ class CallViewModel: ObservableObject {
|
|||
self.isNotEncrypted = false
|
||||
|
||||
if isDeviceTrusted && withToast {
|
||||
ToastViewModel.shared.toastMessage = "Info_call_securised"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Info_call_securised")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1060,8 +1059,9 @@ class CallViewModel: ObservableObject {
|
|||
try callToTransferTo!.transferToAnother(dest: self.currentCall!)
|
||||
Log.info("[CallViewModel] Attended transfer is successful")
|
||||
} catch _ {
|
||||
ToastViewModel.shared.toastMessage = "Failed_toast_call_transfer_failed"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.show("Failed_toast_call_transfer_failed")
|
||||
}
|
||||
|
||||
Log.error("[CallViewModel] Failed to make attended transfer!")
|
||||
}
|
||||
|
|
@ -1080,9 +1080,9 @@ class CallViewModel: ObservableObject {
|
|||
try self.currentCall!.transferTo(referTo: toAddress)
|
||||
Log.info("[CallViewModel] Blind call transfer is successful")
|
||||
} catch _ {
|
||||
ToastViewModel.shared.toastMessage = "Failed_toast_call_transfer_failed"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.show("Failed_toast_call_transfer_failed")
|
||||
}
|
||||
Log.error("[CallViewModel] Failed to make blind call transfer!")
|
||||
}
|
||||
}
|
||||
|
|
@ -1277,8 +1277,7 @@ class CallViewModel: ObservableObject {
|
|||
)
|
||||
DispatchQueue.main.async {
|
||||
self.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1368,8 +1367,7 @@ class CallViewModel: ObservableObject {
|
|||
self.chatRoomDelegate = nil
|
||||
DispatchQueue.main.async {
|
||||
self.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
}, onConferenceJoined: { (chatRoom: ChatRoom, _: EventLog) in
|
||||
|
|
@ -1403,8 +1401,7 @@ class CallViewModel: ObservableObject {
|
|||
self.chatRoomDelegate = nil
|
||||
DispatchQueue.main.async {
|
||||
self.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -66,8 +66,7 @@ struct ContactListBottomSheet: View {
|
|||
dismiss()
|
||||
}
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
|
||||
ToastViewModel.shared.displayToast.toggle()
|
||||
ToastViewModel.shared.show("Success_address_copied_into_clipboard")
|
||||
|
||||
} label: {
|
||||
HStack {
|
||||
|
|
|
|||
|
|
@ -91,8 +91,7 @@ class ContactsListViewModel: ObservableObject {
|
|||
|
||||
DispatchQueue.main.async {
|
||||
SharedMainViewModel.shared.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -137,8 +136,7 @@ class ContactsListViewModel: ObservableObject {
|
|||
|
||||
DispatchQueue.main.async {
|
||||
SharedMainViewModel.shared.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -168,8 +166,7 @@ class ContactsListViewModel: ObservableObject {
|
|||
}
|
||||
DispatchQueue.main.async {
|
||||
SharedMainViewModel.shared.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
}, onConferenceJoined: { (chatRoom: ChatRoom, _: EventLog) in
|
||||
|
|
@ -207,8 +204,7 @@ class ContactsListViewModel: ObservableObject {
|
|||
}
|
||||
DispatchQueue.main.async {
|
||||
SharedMainViewModel.shared.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1198,8 +1198,7 @@ struct ContentView: View {
|
|||
self.isShowDeleteAllHistoryPopup.toggle()
|
||||
sharedMainViewModel.displayedCall = nil
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Success_remove_call_logs"
|
||||
ToastViewModel.shared.displayToast.toggle()
|
||||
ToastViewModel.shared.show("Success_remove_call_logs")
|
||||
},
|
||||
titleThirdButton: Text("dialog_cancel"),
|
||||
actionThirdButton: {
|
||||
|
|
|
|||
|
|
@ -151,7 +151,8 @@ struct ChatBubbleView: View {
|
|||
.clipShape(RoundedRectangle(cornerRadius: 1))
|
||||
.roundedCorner(
|
||||
16,
|
||||
corners: eventLogMessage.message.isOutgoing ? [.topLeft, .topRight, .bottomLeft] : [.topLeft, .topRight, .bottomRight]
|
||||
corners: eventLogMessage.message.isOutgoing ? [.topLeft, .topRight, .bottomLeft] : [.topLeft, .topRight, .bottomRight],
|
||||
stroke: eventLogMessage.message.id == conversationViewModel.highlightedMessageID
|
||||
)
|
||||
}
|
||||
.onTapGesture {
|
||||
|
|
@ -179,7 +180,12 @@ struct ChatBubbleView: View {
|
|||
}
|
||||
|
||||
if !eventLogMessage.message.text.isEmpty {
|
||||
DynamicLinkText(text: eventLogMessage.message.text, participantConversationModel: conversationViewModel.participantConversationModel)
|
||||
DynamicLinkText(
|
||||
text: eventLogMessage.message.text,
|
||||
isMessageId: eventLogMessage.message.id == conversationViewModel.highlightedMessageID,
|
||||
searchText: conversationViewModel.searchText,
|
||||
participantConversationModel: conversationViewModel.participantConversationModel
|
||||
)
|
||||
} else if eventLogMessage.message.isRetracted {
|
||||
Text(eventLogMessage.message.isOutgoing ? "conversation_message_content_deleted_by_us_label" : "conversation_message_content_deleted_label")
|
||||
.italic()
|
||||
|
|
@ -415,7 +421,9 @@ struct ChatBubbleView: View {
|
|||
.roundedCorner(
|
||||
16,
|
||||
corners: eventLogMessage.message.isOutgoing && eventLogMessage.message.isFirstMessage ? [.topLeft, .topRight, .bottomLeft] :
|
||||
(!eventLogMessage.message.isOutgoing && eventLogMessage.message.isFirstMessage ? [.topRight, .bottomRight, .bottomLeft] : [.allCorners]))
|
||||
(!eventLogMessage.message.isOutgoing && eventLogMessage.message.isFirstMessage ? [.topRight, .bottomRight, .bottomLeft] : [.allCorners]),
|
||||
stroke: eventLogMessage.message.id == conversationViewModel.highlightedMessageID
|
||||
)
|
||||
|
||||
if !eventLogMessage.message.reactions.isEmpty {
|
||||
HStack {
|
||||
|
|
@ -946,6 +954,8 @@ struct ChatBubbleView: View {
|
|||
|
||||
struct DynamicLinkText: View {
|
||||
let text: String
|
||||
let isMessageId: Bool
|
||||
let searchText: String
|
||||
let participantConversationModel: [ContactAvatarModel]
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -956,6 +966,8 @@ struct DynamicLinkText: View {
|
|||
.default_text_style(styleSize: 14)
|
||||
}
|
||||
|
||||
// MARK: - Builder
|
||||
|
||||
private func makeAttributedString(from text: String) -> AttributedString {
|
||||
var result = AttributedString()
|
||||
var currentWord = ""
|
||||
|
|
@ -971,9 +983,14 @@ struct DynamicLinkText: View {
|
|||
}
|
||||
|
||||
appendWord(currentWord, to: &result)
|
||||
|
||||
highlightSearch(in: &result, originalText: text)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Word handling
|
||||
|
||||
private func appendWord(_ word: String, to result: inout AttributedString) {
|
||||
guard !word.isEmpty else { return }
|
||||
|
||||
|
|
@ -981,7 +998,7 @@ struct DynamicLinkText: View {
|
|||
if
|
||||
let encoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
|
||||
let url = URL(string: encoded),
|
||||
["http", "https"].contains(url.scheme)
|
||||
["http", "https", "sip", "sips"].contains(url.scheme)
|
||||
{
|
||||
var link = AttributedString(word)
|
||||
link.link = url
|
||||
|
|
@ -993,13 +1010,15 @@ struct DynamicLinkText: View {
|
|||
|
||||
// Mention
|
||||
if isMention(word),
|
||||
let participant = participantConversationModel.first(where: {($0.address.dropFirst(4).split(separator: "@").first ?? "") == word.dropFirst()}),
|
||||
let participant = participantConversationModel.first(
|
||||
where: { ($0.address.dropFirst(4).split(separator: "@").first ?? "") == word.dropFirst() }
|
||||
),
|
||||
let mentionURL = URL(string: "linphone-mention://\(participant.address)")
|
||||
{
|
||||
var mention = AttributedString("@" + participant.name)
|
||||
mention.link = mentionURL
|
||||
mention.foregroundColor = Color.orangeMain500
|
||||
mention.font = .system(size: 14, weight: .semibold)
|
||||
mention.font = .system(size: 14)
|
||||
result.append(mention)
|
||||
return
|
||||
}
|
||||
|
|
@ -1010,6 +1029,40 @@ struct DynamicLinkText: View {
|
|||
result.append(normal)
|
||||
}
|
||||
|
||||
// MARK: - Highlight global
|
||||
|
||||
private func highlightSearch(
|
||||
in attributed: inout AttributedString,
|
||||
originalText: String
|
||||
) {
|
||||
guard !searchText.isEmpty && isMessageId else { return }
|
||||
|
||||
let base = originalText.folding(
|
||||
options: [.caseInsensitive, .diacriticInsensitive],
|
||||
locale: .current
|
||||
)
|
||||
|
||||
let search = searchText.folding(
|
||||
options: [.caseInsensitive, .diacriticInsensitive],
|
||||
locale: .current
|
||||
)
|
||||
|
||||
var searchRange = base.startIndex..<base.endIndex
|
||||
|
||||
while let found = base.range(of: search, range: searchRange) {
|
||||
guard
|
||||
let start = AttributedString.Index(found.lowerBound, within: attributed),
|
||||
let end = AttributedString.Index(found.upperBound, within: attributed)
|
||||
else { break }
|
||||
|
||||
attributed[start..<end].font = .system(size: 14, weight: .bold)
|
||||
|
||||
searchRange = found.upperBound..<base.endIndex
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mention validation
|
||||
|
||||
private func isMention(_ word: String) -> Bool {
|
||||
guard word.first == "@", word.count > 1 else { return false }
|
||||
|
||||
|
|
@ -1020,7 +1073,6 @@ struct DynamicLinkText: View {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
enum URLType {
|
||||
case name(String) // local file name of gif
|
||||
case url(URL) // remote url
|
||||
|
|
@ -1096,8 +1148,12 @@ struct RoundedCorner: Shape {
|
|||
}
|
||||
|
||||
extension View {
|
||||
func roundedCorner(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
||||
func roundedCorner(_ radius: CGFloat, corners: UIRectCorner, stroke: Bool? = false) -> some View {
|
||||
clipShape(RoundedCorner(radius: radius, corners: corners) )
|
||||
.overlay(
|
||||
RoundedCorner(radius: radius, corners: corners)
|
||||
.stroke(Color.orangeMain500, lineWidth: (stroke ?? false) ? 1 : 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ struct ConversationFragment: View {
|
|||
@State var isMenuOpen = false
|
||||
@State private var isMuted: Bool = false
|
||||
|
||||
@FocusState var isSearchTextFocused: Bool
|
||||
@FocusState var isMessageTextFocused: Bool
|
||||
|
||||
@State var offset: CGPoint = .zero
|
||||
|
|
@ -81,11 +82,13 @@ struct ConversationFragment: View {
|
|||
@Binding var isShowConversationInfoPopup: Bool
|
||||
@Binding var conversationInfoPopupText: String
|
||||
|
||||
@State var searchText: String = ""
|
||||
@State var messageText: String = ""
|
||||
|
||||
@State private var chosen: String?
|
||||
@State private var showPicker = false
|
||||
@State private var isSheetVisible = false
|
||||
@State private var isSearchVisible = false
|
||||
|
||||
@State private var isImdnOrReactionsSheetVisible = false
|
||||
|
||||
|
|
@ -292,122 +295,105 @@ struct ConversationFragment: View {
|
|||
.edgesIgnoringSafeArea(.top)
|
||||
.frame(height: 0)
|
||||
|
||||
HStack {
|
||||
if (!(orientation == .landscapeLeft || orientation == .landscapeRight
|
||||
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height)) || isShowConversationFragment {
|
||||
Image("caret-left")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 4)
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
if isShowConversationFragment {
|
||||
isShowConversationFragment = false
|
||||
}
|
||||
SharedMainViewModel.shared.displayedConversation = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Avatar(contactAvatarModel: SharedMainViewModel.shared.displayedConversation?.avatarModel ?? cachedConversation!.avatarModel, avatarSize: 50)
|
||||
.padding(.top, 4)
|
||||
|
||||
VStack(spacing: 1) {
|
||||
Text(SharedMainViewModel.shared.displayedConversation?.subject ?? cachedConversation!.subject)
|
||||
.default_text_style(styleSize: 16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 4)
|
||||
.lineLimit(1)
|
||||
|
||||
if isMuted || conversationViewModel.ephemeralTime != NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") {
|
||||
HStack {
|
||||
if isMuted {
|
||||
Image("bell-slash")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 16, height: 16, alignment: .trailing)
|
||||
}
|
||||
|
||||
if conversationViewModel.ephemeralTime != NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") {
|
||||
Image("clock-countdown")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 16, height: 16, alignment: .trailing)
|
||||
|
||||
Text(conversationViewModel.ephemeralTime)
|
||||
.default_text_style(styleSize: 12)
|
||||
.padding(.leading, -2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(.white)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
isShowInfoConversationFragment = true
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
|
||||
Spacer()
|
||||
|
||||
if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) {
|
||||
Button {
|
||||
if SharedMainViewModel.shared.displayedConversation!.isGroup {
|
||||
isShowStartCallGroupPopup.toggle()
|
||||
} else {
|
||||
SharedMainViewModel.shared.displayedConversation!.call()
|
||||
}
|
||||
} label: {
|
||||
Image("phone")
|
||||
if !isSearchVisible {
|
||||
HStack {
|
||||
if (!(orientation == .landscapeLeft || orientation == .landscapeRight
|
||||
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height)) || isShowConversationFragment {
|
||||
Image("caret-left")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 4)
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
if isShowConversationFragment {
|
||||
isShowConversationFragment = false
|
||||
}
|
||||
SharedMainViewModel.shared.displayedConversation = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
Button {
|
||||
isMenuOpen = false
|
||||
|
||||
Avatar(contactAvatarModel: SharedMainViewModel.shared.displayedConversation?.avatarModel ?? cachedConversation!.avatarModel, avatarSize: 50)
|
||||
.padding(.top, 4)
|
||||
|
||||
VStack(spacing: 1) {
|
||||
Text(SharedMainViewModel.shared.displayedConversation?.subject ?? cachedConversation!.subject)
|
||||
.default_text_style(styleSize: 16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 4)
|
||||
.lineLimit(1)
|
||||
|
||||
if isMuted || conversationViewModel.ephemeralTime != NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") {
|
||||
HStack {
|
||||
if isMuted {
|
||||
Image("bell-slash")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 16, height: 16, alignment: .trailing)
|
||||
}
|
||||
|
||||
if conversationViewModel.ephemeralTime != NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") {
|
||||
Image("clock-countdown")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.orangeMain500)
|
||||
.frame(width: 16, height: 16, alignment: .trailing)
|
||||
|
||||
Text(conversationViewModel.ephemeralTime)
|
||||
.default_text_style(styleSize: 12)
|
||||
.padding(.leading, -2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(.white)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
isShowInfoConversationFragment = true
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text("conversation_menu_go_to_info")
|
||||
Spacer()
|
||||
Image("info")
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
|
||||
Spacer()
|
||||
|
||||
if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) {
|
||||
Button {
|
||||
if SharedMainViewModel.shared.displayedConversation!.isGroup {
|
||||
isShowStartCallGroupPopup.toggle()
|
||||
} else {
|
||||
SharedMainViewModel.shared.displayedConversation!.call()
|
||||
}
|
||||
} label: {
|
||||
Image("phone")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
|
||||
if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) {
|
||||
Menu {
|
||||
Button {
|
||||
isMenuOpen = false
|
||||
SharedMainViewModel.shared.displayedConversation!.toggleMute()
|
||||
isMuted = !isMuted
|
||||
withAnimation {
|
||||
isShowInfoConversationFragment = true
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text(isMuted ? "conversation_action_unmute" : "conversation_action_mute")
|
||||
Text("conversation_menu_go_to_info")
|
||||
Spacer()
|
||||
Image(isMuted ? "bell-simple" : "bell-simple-slash")
|
||||
Image("info")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
|
|
@ -419,13 +405,14 @@ struct ConversationFragment: View {
|
|||
Button {
|
||||
isMenuOpen = false
|
||||
withAnimation {
|
||||
isShowEphemeralFragment = true
|
||||
isSearchVisible = true
|
||||
}
|
||||
isSearchTextFocused = true
|
||||
} label: {
|
||||
HStack {
|
||||
Text("conversation_menu_configure_ephemeral_messages")
|
||||
Text("conversation_menu_search_in_messages")
|
||||
Spacer()
|
||||
Image("clock-countdown")
|
||||
Image("magnifying-glass")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
|
|
@ -433,29 +420,128 @@ struct ConversationFragment: View {
|
|||
.padding(.all, 10)
|
||||
}
|
||||
}
|
||||
|
||||
if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) {
|
||||
Button {
|
||||
isMenuOpen = false
|
||||
SharedMainViewModel.shared.displayedConversation!.toggleMute()
|
||||
isMuted = !isMuted
|
||||
} label: {
|
||||
HStack {
|
||||
Text(isMuted ? "conversation_action_unmute" : "conversation_action_mute")
|
||||
Spacer()
|
||||
Image(isMuted ? "bell-simple" : "bell-simple-slash")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
isMenuOpen = false
|
||||
withAnimation {
|
||||
isShowEphemeralFragment = true
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text("conversation_menu_configure_ephemeral_messages")
|
||||
Spacer()
|
||||
Image("clock-countdown")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image("dots-three-vertical")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 4)
|
||||
.onChange(of: isMuted) { _ in }
|
||||
.onAppear {
|
||||
isMuted = SharedMainViewModel.shared.displayedConversation!.isMuted
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image("dots-three-vertical")
|
||||
.onTapGesture {
|
||||
isMenuOpen = true
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 4)
|
||||
.background(.white)
|
||||
} else {
|
||||
HStack {
|
||||
Image("caret-left")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 4)
|
||||
.onChange(of: isMuted) { _ in }
|
||||
.onAppear {
|
||||
isMuted = SharedMainViewModel.shared.displayedConversation!.isMuted
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
searchText = ""
|
||||
conversationViewModel.searchText = ""
|
||||
conversationViewModel.latestMatch = nil
|
||||
conversationViewModel.canSearchDown = false
|
||||
conversationViewModel.highlightedMessageID = nil
|
||||
withAnimation {
|
||||
isSearchVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
TextField("conversation_menu_search_in_messages", text: $searchText)
|
||||
.default_text_style(styleSize: 15)
|
||||
.focused($isSearchTextFocused)
|
||||
.padding(.vertical, 5)
|
||||
.submitLabel(.search)
|
||||
.onSubmit {
|
||||
conversationViewModel.searchChatMessage(direction: .Up, textToSearch: searchText)
|
||||
}
|
||||
|
||||
Button {
|
||||
conversationViewModel.searchChatMessage(direction: .Up, textToSearch: searchText)
|
||||
} label: {
|
||||
Image("caret-up")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(searchText.isEmpty ? Color.grayMain2c300 : Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.disabled(searchText.isEmpty)
|
||||
|
||||
Button {
|
||||
conversationViewModel.searchChatMessage(direction: .Down, textToSearch: searchText)
|
||||
} label: {
|
||||
Image("caret-down")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle((searchText.isEmpty || !conversationViewModel.canSearchDown) ? Color.grayMain2c300 : Color.grayMain2c500)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.disabled(searchText.isEmpty || !conversationViewModel.canSearchDown)
|
||||
|
||||
}
|
||||
.onTapGesture {
|
||||
isMenuOpen = true
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 4)
|
||||
.background(.white)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 4)
|
||||
.background(.white)
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
|
|
@ -593,7 +679,7 @@ struct ConversationFragment: View {
|
|||
.transition(.move(edge: .bottom))
|
||||
}
|
||||
|
||||
if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) {
|
||||
if !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) && !isSearchVisible {
|
||||
if conversationViewModel.messageToReply != nil {
|
||||
ZStack(alignment: .top) {
|
||||
HStack {
|
||||
|
|
@ -1307,8 +1393,7 @@ struct ConversationFragment: View {
|
|||
forPasteboardType: UTType.plainText.identifier
|
||||
)
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Success_message_copied_into_clipboard"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_message_copied_into_clipboard")
|
||||
|
||||
conversationViewModel.selectedMessage = nil
|
||||
} label: {
|
||||
|
|
@ -1457,6 +1542,22 @@ struct ConversationFragment: View {
|
|||
.zIndex(5)
|
||||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
|
||||
if conversationViewModel.searchInProgress {
|
||||
PopupLoadingView()
|
||||
.background(.black.opacity(0.65))
|
||||
.onDisappear {
|
||||
if conversationViewModel.targetIndex >= 0 {
|
||||
NotificationCenter.default.post(
|
||||
name: NSNotification.Name("onScrollToIndex"),
|
||||
object: nil,
|
||||
userInfo: ["index": conversationViewModel.targetIndex, "animated": true]
|
||||
)
|
||||
|
||||
conversationViewModel.targetIndex = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -240,8 +240,7 @@ class ConversationModel: ObservableObject, Identifiable {
|
|||
} else if state == .CreationFailed {
|
||||
Log.error("\(ConversationModel.TAG) Failed to create group call!")
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_group_call_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_group_call_error")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -134,8 +134,7 @@ class ConversationForwardMessageViewModel: ObservableObject {
|
|||
|
||||
DispatchQueue.main.async {
|
||||
self.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -180,8 +179,7 @@ class ConversationForwardMessageViewModel: ObservableObject {
|
|||
|
||||
DispatchQueue.main.async {
|
||||
self.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -211,8 +209,7 @@ class ConversationForwardMessageViewModel: ObservableObject {
|
|||
self.chatRoomDelegate = nil
|
||||
DispatchQueue.main.async {
|
||||
self.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
}, onConferenceJoined: { (chatRoom: ChatRoom, _: EventLog) in
|
||||
|
|
@ -250,8 +247,7 @@ class ConversationForwardMessageViewModel: ObservableObject {
|
|||
self.chatRoomDelegate = nil
|
||||
DispatchQueue.main.async {
|
||||
self.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -106,6 +106,14 @@ class ConversationViewModel: ObservableObject {
|
|||
|
||||
@Published var isSwiping = false
|
||||
|
||||
@Published var searchText = ""
|
||||
@Published var targetIndex: Int = -1
|
||||
@Published var canSearchDown = false
|
||||
@Published var searchInProgress = false
|
||||
@Published var highlightedMessageID: String?
|
||||
|
||||
var latestMatch: EventLogMessage?
|
||||
|
||||
struct SheetCategory: Identifiable {
|
||||
let id = UUID()
|
||||
let name: String
|
||||
|
|
@ -120,10 +128,10 @@ class ConversationViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
init() {
|
||||
if let chatroom = self.sharedMainViewModel.displayedConversation?.chatRoom {
|
||||
self.addConversationDelegate(chatRoom: chatroom)
|
||||
self.getMessages()
|
||||
}
|
||||
if let chatroom = self.sharedMainViewModel.displayedConversation?.chatRoom {
|
||||
self.addConversationDelegate(chatRoom: chatroom)
|
||||
self.getMessages()
|
||||
}
|
||||
}
|
||||
|
||||
func addConversationDelegate(chatRoom: ChatRoom) {
|
||||
|
|
@ -300,7 +308,7 @@ class ConversationViewModel: ObservableObject {
|
|||
if let displayedConversation = self.sharedMainViewModel.displayedConversation {
|
||||
displayedConversation.getContentTextMessage(chatRoom: displayedConversation.chatRoom)
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if indexMessage != nil {
|
||||
self.conversationMessagesSection[0].rows[indexMessage!].message.text = ""
|
||||
|
|
@ -444,7 +452,7 @@ class ConversationViewModel: ObservableObject {
|
|||
Log.error("[ConversationViewModel] Invalid contentIndex")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
self.conversationMessagesSection[0].rows[indexMessage].message.attachments[contentIndex] = newAttachment
|
||||
let attachmentIndex = self.getAttachmentIndex(attachment: newAttachment)
|
||||
|
||||
|
|
@ -678,7 +686,7 @@ class ConversationViewModel: ObservableObject {
|
|||
self.getUnreadMessagesCount()
|
||||
self.getParticipantConversationModel()
|
||||
self.computeComposingLabel()
|
||||
self.getEphemeralTime()
|
||||
self.getEphemeralTime()
|
||||
|
||||
if self.sharedMainViewModel.displayedConversation != nil {
|
||||
let historyEvents = self.sharedMainViewModel.displayedConversation!.chatRoom.getHistoryRangeEvents(begin: 0, end: 30)
|
||||
|
|
@ -1068,7 +1076,9 @@ class ConversationViewModel: ObservableObject {
|
|||
let addressPrecCleaned = index > 0 ? historyEvents[index - 1].chatMessage?.fromAddress?.clone() : chatMessage.fromAddress?.clone()
|
||||
addressPrecCleaned?.clean()
|
||||
|
||||
let addressNextCleaned = index <= historyEvents.count - 2 ? historyEvents[index + 1].chatMessage?.fromAddress?.clone() : chatMessage.fromAddress?.clone()
|
||||
let addressNextCleaned = index <= historyEvents.count - 2
|
||||
? historyEvents[index + 1].chatMessage?.fromAddress?.clone()
|
||||
: self.conversationMessagesSection[0].rows.last?.eventModel.eventLog.chatMessage?.fromAddress?.clone()
|
||||
addressNextCleaned?.clean()
|
||||
|
||||
let addressCleaned = chatMessage.fromAddress?.clone()
|
||||
|
|
@ -1079,7 +1089,7 @@ class ConversationViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
let isFirstMessageIncomingTmp = index > 0 ? addressPrecCleaned?.asStringUriOnly() != addressCleaned?.asStringUriOnly() : true
|
||||
let isFirstMessageOutgoingTmp = index <= historyEvents.count - 2 ? addressNextCleaned?.asStringUriOnly() != addressCleaned?.asStringUriOnly() : true
|
||||
let isFirstMessageOutgoingTmp = addressNextCleaned?.asStringUriOnly() != addressCleaned?.asStringUriOnly()
|
||||
|
||||
let isFirstMessageTmp = chatMessage.isOutgoing ? isFirstMessageOutgoingTmp : isFirstMessageIncomingTmp
|
||||
|
||||
|
|
@ -1713,13 +1723,13 @@ class ConversationViewModel: ObservableObject {
|
|||
|
||||
if let eventLogMessage = conversationMessagesTmp.last {
|
||||
DispatchQueue.main.async {
|
||||
Log.info("[ConversationViewModel] Send first message")
|
||||
if self.conversationMessagesSection.isEmpty && self.sharedMainViewModel.displayedConversation != nil {
|
||||
self.conversationMessagesSection.append(MessagesSection(date: Date(), chatRoomID: self.sharedMainViewModel.displayedConversation!.id, rows: conversationMessagesTmp))
|
||||
} else {
|
||||
self.conversationMessagesSection[0].rows.append(eventLogMessage)
|
||||
}
|
||||
}
|
||||
Log.info("[ConversationViewModel] Send first message")
|
||||
if self.conversationMessagesSection.isEmpty && self.sharedMainViewModel.displayedConversation != nil {
|
||||
self.conversationMessagesSection.append(MessagesSection(date: Date(), chatRoomID: self.sharedMainViewModel.displayedConversation!.id, rows: conversationMessagesTmp))
|
||||
} else {
|
||||
self.conversationMessagesSection[0].rows.append(eventLogMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getHistorySize()
|
||||
|
|
@ -1762,18 +1772,18 @@ class ConversationViewModel: ObservableObject {
|
|||
conversationMessagesSection = []
|
||||
}
|
||||
|
||||
func replyToMessage(index: Int, isMessageTextFocused: Binding<Bool>) {
|
||||
func replyToMessage(index: Int, isMessageTextFocused: Binding<Bool>) {
|
||||
if self.messageToEdit != nil {
|
||||
self.messageToEdit = nil
|
||||
}
|
||||
coreContext.doOnCoreQueue { _ in
|
||||
let messageToReplyTmp = self.conversationMessagesSection[0].rows[index]
|
||||
DispatchQueue.main.async {
|
||||
withAnimation(.linear(duration: 0.15)) {
|
||||
self.messageToReply = messageToReplyTmp
|
||||
}
|
||||
isMessageTextFocused.wrappedValue = true
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
withAnimation(.linear(duration: 0.15)) {
|
||||
self.messageToReply = messageToReplyTmp
|
||||
}
|
||||
isMessageTextFocused.wrappedValue = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1889,7 +1899,7 @@ class ConversationViewModel: ObservableObject {
|
|||
} else {
|
||||
if content.type != "video" {
|
||||
let filePathSep = content.filePath!.components(separatedBy: "/Library/Images/")
|
||||
let path = URL(string: self.getNewFilePath(name: filePathSep[1]))
|
||||
let path = URL(string: self.getNewFilePath(name: filePathSep[1]))
|
||||
var typeTmp: AttachmentType = .other
|
||||
|
||||
switch content.type {
|
||||
|
|
@ -1928,7 +1938,7 @@ class ConversationViewModel: ObservableObject {
|
|||
}
|
||||
} else if content.type == "video" {
|
||||
let filePathSep = content.filePath!.components(separatedBy: "/Library/Images/")
|
||||
let path = URL(string: self.getNewFilePath(name: filePathSep[1]))
|
||||
let path = URL(string: self.getNewFilePath(name: filePathSep[1]))
|
||||
let pathThumbnail = URL(string: self.generateThumbnail(name: filePathSep[1]))
|
||||
|
||||
if path != nil && pathThumbnail != nil {
|
||||
|
|
@ -2964,6 +2974,375 @@ class ConversationViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func searchChatMessage(direction: SearchDirection, textToSearch: String) {
|
||||
let textToSearch = textToSearch.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let displayedConversation = self.sharedMainViewModel.displayedConversation {
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
if let match = displayedConversation.chatRoom.searchChatMessageByText(text: textToSearch, from: self.latestMatch?.eventModel.eventLog ?? nil, direction: direction) {
|
||||
|
||||
Log.info("\(ConversationViewModel.TAG) Found result \(match.chatMessage?.messageId ?? "No message id") while looking up for message with text \(textToSearch) in direction \(direction) starting from message \(self.latestMatch?.eventModel.eventLog.chatMessage?.messageId ?? "No message id")"
|
||||
)
|
||||
|
||||
if let sectionIndex = self.conversationMessagesSection.firstIndex(where: {
|
||||
$0.chatRoomID == displayedConversation.id
|
||||
}),
|
||||
let rowIndex = self.conversationMessagesSection[sectionIndex].rows.firstIndex(where: {
|
||||
$0.eventModel.eventLogId == match.chatMessage?.messageId
|
||||
}) {
|
||||
self.latestMatch = self.conversationMessagesSection[sectionIndex].rows[rowIndex]
|
||||
|
||||
Log.info("\(ConversationViewModel.TAG) Found result is already in history, no need to load more history")
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.searchText = textToSearch
|
||||
self.highlightedMessageID = match.chatMessage?.messageId
|
||||
|
||||
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onScrollToIndex"), object: nil, userInfo: ["index": rowIndex, "animated": true])
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.canSearchDown = true
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.searchInProgress = true
|
||||
self.canSearchDown = true
|
||||
}
|
||||
|
||||
Log.info("\(ConversationViewModel.TAG) Found result isn't in currently loaded history, loading missing events")
|
||||
self.loadMessagesUpTo(targetEvent: match, textToSearch: textToSearch)
|
||||
}
|
||||
} else {
|
||||
Log.info("\(ConversationViewModel.TAG) No match found while looking up for message with text \(textToSearch) in direction \(direction) starting from message \(self.latestMatch?.eventModel.eventLog.chatMessage?.messageId ?? "No message id")"
|
||||
)
|
||||
|
||||
if self.latestMatch == nil {
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.show("Failed_search_no_match_found")
|
||||
}
|
||||
} else {
|
||||
// Scroll to last matching event anyway, user may have scrolled away
|
||||
if let sectionIndex = self.conversationMessagesSection.firstIndex(where: {
|
||||
$0.chatRoomID == displayedConversation.id
|
||||
}), let latestMatchTmp = self.latestMatch,
|
||||
let rowIndex = self.conversationMessagesSection[sectionIndex].rows.firstIndex(of: latestMatchTmp) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.searchText = textToSearch
|
||||
self.highlightedMessageID = latestMatchTmp.message.id
|
||||
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "onScrollToIndex"), object: nil, userInfo: ["index": rowIndex, "animated": true])
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.show("Failed_search_results_limit_reached")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadMessagesUpTo(targetEvent: EventLog, textToSearch: String) {
|
||||
if self.conversationMessagesSection[0].rows.last != nil {
|
||||
let firstEventLog = self.sharedMainViewModel.displayedConversation?.chatRoom.getHistoryRangeEvents(
|
||||
begin: self.conversationMessagesSection[0].rows.count - 1,
|
||||
end: self.conversationMessagesSection[0].rows.count
|
||||
)
|
||||
|
||||
if let chatMessageTmp = targetEvent.chatMessage {
|
||||
let lastEventLog = self.sharedMainViewModel.displayedConversation!.chatRoom.findEventLog(messageId: chatMessageTmp.messageId)
|
||||
|
||||
var historyEvents = self.sharedMainViewModel.displayedConversation!.chatRoom.getHistoryRangeBetween(
|
||||
firstEvent: firstEventLog!.first,
|
||||
lastEvent: lastEventLog,
|
||||
filters: UInt(ChatRoom.HistoryFilter([.ChatMessage, .InfoNoDevice]).rawValue)
|
||||
)
|
||||
|
||||
let historyEventsAfter = self.sharedMainViewModel.displayedConversation!.chatRoom.getHistoryRangeEvents(
|
||||
begin: self.conversationMessagesSection[0].rows.count + historyEvents.count + 1,
|
||||
end: self.conversationMessagesSection[0].rows.count + historyEvents.count + 30
|
||||
)
|
||||
|
||||
if lastEventLog != nil {
|
||||
historyEvents.insert(lastEventLog!, at: 0)
|
||||
}
|
||||
|
||||
historyEvents.insert(contentsOf: historyEventsAfter, at: 0)
|
||||
|
||||
var conversationMessagesTmp: [EventLogMessage] = []
|
||||
|
||||
historyEvents.enumerated().reversed().forEach { index, eventLog in
|
||||
var attachmentNameList: String = ""
|
||||
var attachmentList: [Attachment] = []
|
||||
var contentText = ""
|
||||
|
||||
guard let chatMessage = eventLog.chatMessage else {
|
||||
conversationMessagesTmp.insert(
|
||||
EventLogMessage(
|
||||
eventModel: EventModel(eventLog: eventLog),
|
||||
message: Message(
|
||||
id: UUID().uuidString,
|
||||
status: nil,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
isRetractable: false,
|
||||
isEdited: false,
|
||||
isRetracted: false,
|
||||
dateReceived: 0,
|
||||
address: "",
|
||||
isFirstMessage: false,
|
||||
text: "",
|
||||
attachments: [],
|
||||
ownReaction: "",
|
||||
reactions: []
|
||||
)
|
||||
), at: 0
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if !chatMessage.contents.isEmpty {
|
||||
chatMessage.contents.forEach { content in
|
||||
if content.isText && content.name == nil {
|
||||
contentText = content.utf8Text ?? ""
|
||||
} else if content.name != nil && !content.name!.isEmpty {
|
||||
if content.filePath == nil || content.filePath!.isEmpty {
|
||||
// self.downloadContent(chatMessage: chatMessage, content: content)
|
||||
let path = URL(string: self.getNewFilePath(name: content.name ?? ""))
|
||||
|
||||
if path != nil {
|
||||
let attachment =
|
||||
Attachment(
|
||||
id: UUID().uuidString,
|
||||
name: content.name!,
|
||||
url: path!,
|
||||
type: .fileTransfer,
|
||||
size: content.fileSize,
|
||||
transferProgressIndication: content.filePath != nil && !content.filePath!.isEmpty ? 100 : -1
|
||||
)
|
||||
attachmentNameList += ", \(content.name!)"
|
||||
attachmentList.append(attachment)
|
||||
}
|
||||
} else {
|
||||
if content.type != "video" {
|
||||
let filePathSep = content.filePath!.components(separatedBy: "/Library/Images/")
|
||||
let path = URL(string: self.getNewFilePath(name: filePathSep[1]))
|
||||
var typeTmp: AttachmentType = .other
|
||||
|
||||
switch content.type {
|
||||
case "image":
|
||||
typeTmp = (content.name?.lowercased().hasSuffix("gif"))! ? .gif : .image
|
||||
case "audio":
|
||||
typeTmp = content.isVoiceRecording ? .voiceRecording : .audio
|
||||
case "application":
|
||||
typeTmp = content.subtype.lowercased() == "pdf" ? .pdf : .other
|
||||
case "text":
|
||||
typeTmp = .text
|
||||
default:
|
||||
typeTmp = .other
|
||||
}
|
||||
|
||||
if path != nil {
|
||||
let attachment =
|
||||
Attachment(
|
||||
id: UUID().uuidString,
|
||||
name: content.name!,
|
||||
url: path!,
|
||||
type: typeTmp,
|
||||
duration: typeTmp == . voiceRecording ? content.fileDuration : 0,
|
||||
size: content.fileSize,
|
||||
transferProgressIndication: content.filePath != nil && !content.filePath!.isEmpty ? 100 : -1
|
||||
)
|
||||
attachmentNameList += ", \(content.name!)"
|
||||
attachmentList.append(attachment)
|
||||
if typeTmp != .voiceRecording {
|
||||
DispatchQueue.main.async {
|
||||
if !attachment.full.pathExtension.isEmpty {
|
||||
self.attachments.append(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if content.type == "video" {
|
||||
let filePathSep = content.filePath!.components(separatedBy: "/Library/Images/")
|
||||
let path = URL(string: self.getNewFilePath(name: filePathSep[1]))
|
||||
let pathThumbnail = URL(string: self.generateThumbnail(name: filePathSep[1]))
|
||||
|
||||
if path != nil && pathThumbnail != nil {
|
||||
let attachment =
|
||||
Attachment(
|
||||
id: UUID().uuidString,
|
||||
name: content.name!,
|
||||
thumbnail: pathThumbnail!,
|
||||
full: path!,
|
||||
type: .video,
|
||||
size: content.fileSize,
|
||||
transferProgressIndication: content.filePath != nil && !content.filePath!.isEmpty ? 100 : -1
|
||||
)
|
||||
attachmentNameList += ", \(content.name!)"
|
||||
attachmentList.append(attachment)
|
||||
DispatchQueue.main.async {
|
||||
if !attachment.full.pathExtension.isEmpty {
|
||||
self.attachments.append(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let addressPrecCleaned = index > 0 ? historyEvents[index - 1].chatMessage?.fromAddress?.clone() : chatMessage.fromAddress?.clone()
|
||||
addressPrecCleaned?.clean()
|
||||
|
||||
let addressNextCleaned = index <= historyEvents.count - 2
|
||||
? historyEvents[index + 1].chatMessage?.fromAddress?.clone()
|
||||
: self.conversationMessagesSection[0].rows.last?.eventModel.eventLog.chatMessage?.fromAddress?.clone()
|
||||
addressNextCleaned?.clean()
|
||||
|
||||
let addressCleaned = chatMessage.fromAddress?.clone()
|
||||
addressCleaned?.clean()
|
||||
|
||||
if addressCleaned != nil && self.participantConversationModel.first(where: {$0.address == addressCleaned!.asStringUriOnly()}) == nil {
|
||||
self.addParticipantConversationModel(address: addressCleaned!)
|
||||
}
|
||||
|
||||
let isFirstMessageIncomingTmp = index > 0 ? addressPrecCleaned?.asStringUriOnly() != addressCleaned?.asStringUriOnly() : true
|
||||
let isFirstMessageOutgoingTmp = addressNextCleaned?.asStringUriOnly() != addressCleaned?.asStringUriOnly()
|
||||
|
||||
let isFirstMessageTmp = chatMessage.isOutgoing ? isFirstMessageOutgoingTmp : isFirstMessageIncomingTmp
|
||||
|
||||
var statusTmp: Message.Status? = .sending
|
||||
switch chatMessage.state {
|
||||
case .InProgress:
|
||||
statusTmp = .sending
|
||||
case .Delivered:
|
||||
statusTmp = .sent
|
||||
case .DeliveredToUser:
|
||||
statusTmp = .received
|
||||
case .Displayed:
|
||||
statusTmp = .read
|
||||
case .NotDelivered:
|
||||
statusTmp = .error
|
||||
default:
|
||||
statusTmp = .sending
|
||||
}
|
||||
|
||||
var reactionsTmp: [String] = []
|
||||
chatMessage.reactions.forEach({ chatMessageReaction in
|
||||
reactionsTmp.append(chatMessageReaction.body)
|
||||
})
|
||||
|
||||
if !attachmentNameList.isEmpty {
|
||||
attachmentNameList = String(attachmentNameList.dropFirst(2))
|
||||
}
|
||||
|
||||
var replyMessageTmp: ReplyMessage?
|
||||
if chatMessage.replyMessage != nil {
|
||||
let addressReplyCleaned = chatMessage.replyMessage?.fromAddress?.clone()
|
||||
addressReplyCleaned?.clean()
|
||||
|
||||
if addressReplyCleaned != nil && self.participantConversationModel.first(where: {$0.address == addressReplyCleaned!.asStringUriOnly()}) == nil {
|
||||
self.addParticipantConversationModel(address: addressReplyCleaned!)
|
||||
}
|
||||
|
||||
let contentReplyText = chatMessage.replyMessage?.utf8Text ?? ""
|
||||
|
||||
let isReplyRetracted = chatMessage.replyMessage?.isRetracted ?? false
|
||||
|
||||
var attachmentNameReplyList: String = ""
|
||||
|
||||
chatMessage.replyMessage?.contents.forEach { content in
|
||||
if !content.isText {
|
||||
attachmentNameReplyList += ", \(content.name!)"
|
||||
}
|
||||
}
|
||||
|
||||
if !attachmentNameReplyList.isEmpty {
|
||||
attachmentNameReplyList = String(attachmentNameReplyList.dropFirst(2))
|
||||
}
|
||||
|
||||
replyMessageTmp = ReplyMessage(
|
||||
id: chatMessage.replyMessage!.messageId,
|
||||
address: addressReplyCleaned?.asStringUriOnly() ?? "",
|
||||
isFirstMessage: false,
|
||||
text: contentReplyText,
|
||||
isOutgoing: chatMessage.replyMessage!.isOutgoing,
|
||||
isEditable: false,
|
||||
isRetractable: false,
|
||||
isEdited: false,
|
||||
isRetracted: isReplyRetracted,
|
||||
dateReceived: 0,
|
||||
attachmentsNames: attachmentNameReplyList,
|
||||
attachments: []
|
||||
)
|
||||
}
|
||||
|
||||
conversationMessagesTmp.insert(
|
||||
EventLogMessage(
|
||||
eventModel: EventModel(eventLog: eventLog),
|
||||
message: Message(
|
||||
id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString,
|
||||
status: statusTmp,
|
||||
isOutgoing: chatMessage.isOutgoing,
|
||||
isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false,
|
||||
isRetractable: chatMessage.isOutgoing ? chatMessage.isRetractable : false,
|
||||
isEdited: chatMessage.isEdited,
|
||||
isRetracted: chatMessage.isRetracted,
|
||||
dateReceived: chatMessage.time,
|
||||
address: addressCleaned?.asStringUriOnly() ?? "",
|
||||
isFirstMessage: isFirstMessageTmp,
|
||||
text: contentText,
|
||||
attachmentsNames: attachmentNameList,
|
||||
attachments: attachmentList,
|
||||
replyMessage: replyMessageTmp,
|
||||
isForward: chatMessage.isForward,
|
||||
ownReaction: chatMessage.ownReaction?.body ?? "",
|
||||
reactions: reactionsTmp,
|
||||
isEphemeral: chatMessage.isEphemeral,
|
||||
ephemeralExpireTime: chatMessage.ephemeralExpireTime,
|
||||
ephemeralLifetime: chatMessage.ephemeralLifetime,
|
||||
isIcalendar: chatMessage.contents.first?.isIcalendar ?? false,
|
||||
messageConferenceInfo: chatMessage.contents.first != nil && chatMessage.contents.first!.isIcalendar == true ? self.parseConferenceInvite(content: chatMessage.contents.first!) : nil
|
||||
)
|
||||
), at: 0
|
||||
)
|
||||
|
||||
self.addChatMessageDelegate(message: chatMessage)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.searchInProgress = false
|
||||
|
||||
|
||||
guard !conversationMessagesTmp.isEmpty else { return }
|
||||
|
||||
if let lastRow = self.conversationMessagesSection[0].rows.last,
|
||||
lastRow.message.address == conversationMessagesTmp.last?.message.address {
|
||||
self.conversationMessagesSection[0].rows[self.conversationMessagesSection[0].rows.count - 1].message.isFirstMessage = false
|
||||
}
|
||||
|
||||
self.conversationMessagesSection[0].rows.append(contentsOf: conversationMessagesTmp.reversed())
|
||||
|
||||
if self.conversationMessagesSection[0].rows.count > historyEventsAfter.count {
|
||||
self.targetIndex = self.conversationMessagesSection[0].rows.count - historyEventsAfter.count - 1
|
||||
self.searchText = textToSearch
|
||||
self.highlightedMessageID = targetEvent.chatMessage?.messageId
|
||||
self.latestMatch = self.conversationMessagesSection[0].rows[self.targetIndex]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.searchInProgress = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.searchInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// swiftlint:enable line_length
|
||||
// swiftlint:enable type_body_length
|
||||
|
|
|
|||
|
|
@ -149,8 +149,7 @@ class StartConversationViewModel: ObservableObject {
|
|||
|
||||
DispatchQueue.main.async {
|
||||
self.operationGroupInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -206,8 +205,7 @@ class StartConversationViewModel: ObservableObject {
|
|||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.operationOneToOneInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -252,8 +250,7 @@ class StartConversationViewModel: ObservableObject {
|
|||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
self.operationOneToOneInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -284,8 +281,7 @@ class StartConversationViewModel: ObservableObject {
|
|||
DispatchQueue.main.async {
|
||||
self.operationOneToOneInProgress = false
|
||||
self.operationGroupInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
}, onConferenceJoined: { (chatRoom: ChatRoom, _: EventLog) in
|
||||
|
|
@ -314,8 +310,7 @@ class StartConversationViewModel: ObservableObject {
|
|||
DispatchQueue.main.async {
|
||||
self.operationOneToOneInProgress = false
|
||||
self.operationGroupInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ class HelpView { // TODO (basic debug moved here until halp view is implemented)
|
|||
CoreContext.shared.doOnCoreQueue { _ in
|
||||
Core.resetLogCollection()
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "help_troubleshooting_debug_logs_cleaned_toast_message"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("help_troubleshooting_debug_logs_cleaned_toast_message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,33 +25,39 @@ struct ToastView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if toastViewModel.displayToast {
|
||||
if let toast = toastViewModel.toast {
|
||||
HStack {
|
||||
if toastViewModel.toastMessage.contains("toast_call_transfer") {
|
||||
if toast.message.contains("Failed_search") {
|
||||
Image("magnifying-glass")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.foregroundStyle(Color.redDanger500)
|
||||
} else if toast.message.contains("toast_call_transfer") {
|
||||
Image("phone-transfer")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.foregroundStyle(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500)
|
||||
} else if toastViewModel.toastMessage.contains("is recording") {
|
||||
.foregroundStyle(toast.message.contains("Success") ? Color.greenSuccess500 : Color.redDanger500)
|
||||
} else if toast.message.contains("is recording") {
|
||||
Image("record-fill")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.foregroundStyle(Color.redDanger500)
|
||||
} else if toastViewModel.toastMessage.contains("Info_") {
|
||||
} else if toast.message.contains("Info_") {
|
||||
Image("trusted")
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
} else {
|
||||
Image(toastViewModel.toastMessage.contains("Success") ? "check" : "warning-circle")
|
||||
Image(toast.message.contains("Success") ? "check" : "warning-circle")
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
.foregroundStyle(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500)
|
||||
.foregroundStyle(toast.message.contains("Success") ? Color.greenSuccess500 : Color.redDanger500)
|
||||
}
|
||||
|
||||
switch toastViewModel.toastMessage {
|
||||
switch toast.message {
|
||||
case "Success_qr_code_validated":
|
||||
Text("qr_code_validated")
|
||||
.multilineTextAlignment(.center)
|
||||
|
|
@ -116,7 +122,7 @@ struct ToastView: View {
|
|||
.padding(8)
|
||||
|
||||
case let str where str.contains("is recording"):
|
||||
Text(toastViewModel.toastMessage)
|
||||
Text(toast.message)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(Color.redDanger500)
|
||||
.default_text_style(styleSize: 15)
|
||||
|
|
@ -249,7 +255,7 @@ struct ToastView: View {
|
|||
.padding(8)
|
||||
|
||||
case let str where str.contains("Error: "):
|
||||
Text(toastViewModel.toastMessage)
|
||||
Text(toast.message)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(Color.redDanger500)
|
||||
.default_text_style(styleSize: 15)
|
||||
|
|
@ -316,6 +322,20 @@ struct ToastView: View {
|
|||
.default_text_style(styleSize: 15)
|
||||
.padding(8)
|
||||
|
||||
case "Failed_search_no_match_found":
|
||||
Text("conversation_search_no_match_found")
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(Color.redDanger500)
|
||||
.default_text_style(styleSize: 15)
|
||||
.padding(8)
|
||||
|
||||
case "Failed_search_results_limit_reached":
|
||||
Text("conversation_search_results_limit_reached_label")
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(Color.redDanger500)
|
||||
.default_text_style(styleSize: 15)
|
||||
.padding(8)
|
||||
|
||||
case "Success_settings_contacts_carddav_sync_successful_toast":
|
||||
Text("settings_contacts_carddav_sync_successful_toast")
|
||||
.multilineTextAlignment(.center)
|
||||
|
|
@ -351,25 +371,14 @@ struct ToastView: View {
|
|||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 50)
|
||||
.inset(by: 0.5)
|
||||
.stroke(toastViewModel.toastMessage.contains("Success")
|
||||
? Color.greenSuccess500 : (toastViewModel.toastMessage.contains("Info_")
|
||||
.stroke(toast.message.contains("Success")
|
||||
? Color.greenSuccess500 : (toast.message.contains("Info_")
|
||||
? Color.blueInfo500 : Color.redDanger500), lineWidth: 1)
|
||||
)
|
||||
.onTapGesture {
|
||||
if !toastViewModel.toastMessage.contains("is recording") {
|
||||
if !toast.message.contains("is recording") {
|
||||
withAnimation {
|
||||
toastViewModel.toastMessage = ""
|
||||
toastViewModel.displayToast = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !toastViewModel.toastMessage.contains("is recording") {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
withAnimation {
|
||||
toastViewModel.toastMessage = ""
|
||||
toastViewModel.displayToast = false
|
||||
}
|
||||
toastViewModel.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,8 +109,7 @@ struct DebugFragment: View {
|
|||
helpViewModel.version,
|
||||
forPasteboardType: UTType.plainText.identifier
|
||||
)
|
||||
ToastViewModel.shared.toastMessage = "Success_text_copied_into_clipboard"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_text_copied_into_clipboard")
|
||||
} label: {
|
||||
HStack {
|
||||
Image("app-store-logo")
|
||||
|
|
@ -145,8 +144,7 @@ struct DebugFragment: View {
|
|||
helpViewModel.sdkVersion,
|
||||
forPasteboardType: UTType.plainText.identifier
|
||||
)
|
||||
ToastViewModel.shared.toastMessage = "Success_text_copied_into_clipboard"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_text_copied_into_clipboard")
|
||||
} label: {
|
||||
HStack {
|
||||
Image("package")
|
||||
|
|
|
|||
|
|
@ -92,14 +92,12 @@ class HelpViewModel: ObservableObject {
|
|||
case .UpToDate:
|
||||
Log.info("\(self.TAG): This version is up-to-date")
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Success_version_up_to_date"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_version_up_to_date")
|
||||
}
|
||||
default:
|
||||
Log.info("\(self.TAG): Can't check for update, an error happened [\(result)]")
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,8 +140,7 @@ class HelpViewModel: ObservableObject {
|
|||
Core.resetLogCollection()
|
||||
Log.info("\(self.TAG) Debug logs have been cleaned")
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Success_clear_logs"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_clear_logs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,8 +120,7 @@ struct HistoryContactFragment: View {
|
|||
)
|
||||
}
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
|
||||
ToastViewModel.shared.displayToast.toggle()
|
||||
ToastViewModel.shared.show("Success_address_copied_into_clipboard")
|
||||
|
||||
} label: {
|
||||
HStack {
|
||||
|
|
|
|||
|
|
@ -152,8 +152,7 @@ struct HistoryListBottomSheet: View {
|
|||
dismiss()
|
||||
}
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
|
||||
ToastViewModel.shared.displayToast.toggle()
|
||||
ToastViewModel.shared.show("Success_address_copied_into_clipboard")
|
||||
|
||||
} label: {
|
||||
HStack {
|
||||
|
|
|
|||
|
|
@ -280,8 +280,7 @@ class HistoryListViewModel: ObservableObject {
|
|||
|
||||
DispatchQueue.main.async {
|
||||
SharedMainViewModel.shared.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -326,8 +325,7 @@ class HistoryListViewModel: ObservableObject {
|
|||
|
||||
DispatchQueue.main.async {
|
||||
SharedMainViewModel.shared.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -357,8 +355,7 @@ class HistoryListViewModel: ObservableObject {
|
|||
}
|
||||
DispatchQueue.main.async {
|
||||
SharedMainViewModel.shared.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
}, onConferenceJoined: { (chatRoom: ChatRoom, _: EventLog) in
|
||||
|
|
@ -396,8 +393,7 @@ class HistoryListViewModel: ObservableObject {
|
|||
}
|
||||
DispatchQueue.main.async {
|
||||
SharedMainViewModel.shared.operationInProgress = false
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_conversation_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_conversation_error")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -139,8 +139,7 @@ class StartCallViewModel: ObservableObject {
|
|||
} else if state == .CreationFailed {
|
||||
Log.error("\(StartCallViewModel.TAG) Failed to create group call!")
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Failed_to_create_group_call_error"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_to_create_group_call_error")
|
||||
self.operationInProgress = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,8 +199,7 @@ struct MeetingFragment: View {
|
|||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_address_copied_into_clipboard")
|
||||
}
|
||||
}, label: {
|
||||
HStack {
|
||||
|
|
|
|||
|
|
@ -201,8 +201,7 @@ class MeetingViewModel: ObservableObject {
|
|||
DispatchQueue.main.async {
|
||||
self.operationInProgress = false
|
||||
self.errorMsg = (SharedMainViewModel.shared.displayedMeeting != nil) ? "Could not edit conference" : "Could not create conference"
|
||||
ToastViewModel.shared.toastMessage = (SharedMainViewModel.shared.displayedMeeting != nil) ? "meeting_failed_to_edit_toast" : "meeting_failed_to_schedule_toast"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show((SharedMainViewModel.shared.displayedMeeting != nil) ? "meeting_failed_to_edit_toast" : "meeting_failed_to_schedule_toast")
|
||||
}
|
||||
} else if state == ConferenceScheduler.State.Ready {
|
||||
if let confInfo = scheduler.info, let conferenceAddress = confInfo.uri {
|
||||
|
|
@ -210,8 +209,7 @@ class MeetingViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Success_meeting_info_created_toast"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_meeting_info_created_toast")
|
||||
}
|
||||
|
||||
if SharedMainViewModel.shared.displayedMeeting != nil {
|
||||
|
|
@ -243,8 +241,7 @@ class MeetingViewModel: ObservableObject {
|
|||
} else if failedInvitations.count == self.participants.count {
|
||||
Log.error("\(MeetingViewModel.TAG) No invitation sent!")
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "meeting_failed_to_send_invites_toast"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("meeting_failed_to_send_invites_toast")
|
||||
}
|
||||
} else {
|
||||
var failInvList = ""
|
||||
|
|
@ -256,8 +253,7 @@ class MeetingViewModel: ObservableObject {
|
|||
}
|
||||
Log.warn("\(MeetingViewModel.TAG) \(failedInvitations.count) invitations couldn't have been sent to: \(failInvList)")
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "meeting_failed_to_send_part_of_invites_toast"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("meeting_failed_to_send_part_of_invites_toast")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -273,16 +269,14 @@ class MeetingViewModel: ObservableObject {
|
|||
guard !subject.isEmpty && !participants.isEmpty else {
|
||||
Log.error("\(MeetingViewModel.TAG) Either no subject was set or no participant was selected, can't schedule meeting.")
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Failed_no_subject_or_participant"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Failed_no_subject_or_participant")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard CoreContext.shared.networkStatusIsConnected else {
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "Unavailable_network"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Unavailable_network")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -427,12 +421,10 @@ class MeetingViewModel: ObservableObject {
|
|||
do {
|
||||
try self.eventStore.save(event, span: .thisEvent)
|
||||
Log.info("\(MeetingViewModel.TAG) Meeting '\(self.subject)' added to calendar")
|
||||
ToastViewModel.shared.toastMessage = "Meeting_added_to_calendar"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Meeting_added_to_calendar")
|
||||
} catch let error as NSError {
|
||||
Log.error("\(MeetingViewModel.TAG) Failed to add meeting to calendar: \(error)")
|
||||
ToastViewModel.shared.toastMessage = "Error: \(error)"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Error: \(error)")
|
||||
}
|
||||
} else {
|
||||
Log.error("\(MeetingViewModel.TAG) Failed to add meeting to calendar: \(error?.localizedDescription ?? "")")
|
||||
|
|
|
|||
|
|
@ -143,8 +143,7 @@ class MeetingsListViewModel: ObservableObject {
|
|||
// Only remaining meeting is the fake TodayMeeting, remove it too
|
||||
self.meetingsList.removeAll()
|
||||
}
|
||||
ToastViewModel.shared.toastMessage = "Success_toast_meeting_deleted"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_toast_meeting_deleted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -180,8 +179,7 @@ class MeetingsListViewModel: ObservableObject {
|
|||
}
|
||||
Log.warn("\(MeetingViewModel.TAG) \(failedInvitations.count) invitations couldn't have been sent to: \(failInvList)")
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = "meeting_failed_to_send_part_of_invites_toast"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("meeting_failed_to_send_part_of_invites_toast")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -287,8 +287,7 @@ struct AccountProfileFragment: View {
|
|||
forPasteboardType: UTType.plainText.identifier
|
||||
)
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Success_address_copied_into_clipboard"
|
||||
ToastViewModel.shared.displayToast.toggle()
|
||||
ToastViewModel.shared.show("Success_address_copied_into_clipboard")
|
||||
}, label: {
|
||||
Image("copy")
|
||||
.resizable()
|
||||
|
|
|
|||
|
|
@ -128,8 +128,7 @@ class CardDavViewModel: ObservableObject {
|
|||
NotificationCenter.default.post(name: NSNotification.Name("ContactLoaded"), object: nil)
|
||||
self.cardDavServerOperationSuccessful = true
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Success_settings_contacts_carddav_deleted_toast"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_settings_contacts_carddav_deleted_toast")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -227,8 +226,7 @@ class CardDavViewModel: ObservableObject {
|
|||
DispatchQueue.main.async {
|
||||
self.cardDavServerOperationInProgress = false
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Success_settings_contacts_carddav_sync_successful_toast"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("Success_settings_contacts_carddav_sync_successful_toast")
|
||||
}
|
||||
|
||||
let name = self.displayName
|
||||
|
|
@ -256,8 +254,7 @@ class CardDavViewModel: ObservableObject {
|
|||
DispatchQueue.main.async {
|
||||
self.cardDavServerOperationInProgress = false
|
||||
|
||||
ToastViewModel.shared.toastMessage = "settings_contacts_carddav_sync_error_toast"
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show("settings_contacts_carddav_sync_error_toast")
|
||||
}
|
||||
if !self.isEdit {
|
||||
Log.error("\(CardDavViewModel.TAG) Synchronization failed, removing Friend list from Core")
|
||||
|
|
|
|||
|
|
@ -19,13 +19,39 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
class ToastViewModel: ObservableObject {
|
||||
@MainActor
|
||||
final class ToastViewModel: ObservableObject {
|
||||
|
||||
static let shared = ToastViewModel()
|
||||
|
||||
var toastMessage: String = ""
|
||||
@Published var displayToast = false
|
||||
@Published var toast: ToastData?
|
||||
|
||||
private init() {
|
||||
private var hideWorkItem: DispatchWorkItem?
|
||||
|
||||
private init() {}
|
||||
|
||||
func show(_ message: String, duration: TimeInterval = 2.0) {
|
||||
hideWorkItem?.cancel()
|
||||
|
||||
toast = ToastData(message: message)
|
||||
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
self?.toast = nil
|
||||
}
|
||||
hideWorkItem = workItem
|
||||
|
||||
if !message.contains("is recording") {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: workItem)
|
||||
}
|
||||
}
|
||||
|
||||
func hide() {
|
||||
hideWorkItem?.cancel()
|
||||
toast = nil
|
||||
}
|
||||
}
|
||||
|
||||
struct ToastData: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let message: String
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,8 +170,7 @@ class URIHandler {
|
|||
|
||||
private static func toast(_ message: String) {
|
||||
DispatchQueue.main.async {
|
||||
ToastViewModel.shared.toastMessage = message
|
||||
ToastViewModel.shared.displayToast = true
|
||||
ToastViewModel.shared.show(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue