From 3203cb3cccf0a5a641883e0abd8e4af2ab693d88 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Wed, 16 Oct 2024 17:10:54 +0200 Subject: [PATCH] Add Meeting invite --- Linphone/Localizable.xcstrings | 58 +++++++- Linphone/TelecomManager/TelecomManager.swift | 8 +- .../UI/Call/MeetingWaitingRoomFragment.swift | 4 +- .../Fragments/ChatBubbleView.swift | 127 ++++++++++++++++-- .../Model/MessageConferenceInfo.swift | 4 +- .../ViewModel/ConversationViewModel.swift | 22 ++- .../Fragments/HistoryContactFragment.swift | 2 +- .../Meetings/Fragments/MeetingFragment.swift | 3 +- .../Meetings/ViewModel/MeetingViewModel.swift | 8 ++ 9 files changed, 206 insertions(+), 30 deletions(-) diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 622a43465..d1c538639 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -1303,6 +1303,40 @@ } } }, + "conversation_message_meeting_cancelled_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meeting has been cancelled!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La réunion a été annulée" + } + } + } + }, + "conversation_message_meeting_updated_label" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meeting has been updated" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "La réunion a été mise à jour" + } + } + } + }, "conversation_reply_to_message_title" : { "extractionState" : "manual", "localizations" : { @@ -1384,6 +1418,9 @@ }, "Deny all" : { + }, + "Description" : { + }, "Dialer" : { @@ -1818,8 +1855,22 @@ "Meeting added to iPhone calendar" : { }, - "Meeting invite !!" : { - + "meeting_waiting_room_join" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Join" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rejoindre" + } + } + } }, "Meetings" : { @@ -2238,9 +2289,6 @@ } } } - }, - "Rejoindre" : { - }, "Remove from favourites" : { diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index 16ce445f9..0736f6fc5 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -171,8 +171,12 @@ class TelecomManager: ObservableObject { do { let meetingAddress = try Factory.Instance.createAddress(addr: address.asStringUriOnly()) - meetingWaitingRoomDisplayed = true - meetingWaitingRoomSelected = meetingAddress + DispatchQueue.main.async { + withAnimation { + self.meetingWaitingRoomDisplayed = true + self.meetingWaitingRoomSelected = meetingAddress + } + } } catch {} } else { doCallWithCore( diff --git a/Linphone/UI/Call/MeetingWaitingRoomFragment.swift b/Linphone/UI/Call/MeetingWaitingRoomFragment.swift index 1c1be011e..8f8920261 100644 --- a/Linphone/UI/Call/MeetingWaitingRoomFragment.swift +++ b/Linphone/UI/Call/MeetingWaitingRoomFragment.swift @@ -335,7 +335,7 @@ struct MeetingWaitingRoomFragment: View { Button(action: { meetingWaitingRoomViewModel.joinMeeting() }, label: { - Text("Rejoindre") + Text("meeting_waiting_room_join") .default_text_style_white_600(styleSize: 20) .frame(height: 35) .frame(maxWidth: .infinity) @@ -356,7 +356,7 @@ struct MeetingWaitingRoomFragment: View { Button(action: { meetingWaitingRoomViewModel.joinMeeting() }, label: { - Text("Rejoindre") + Text("meeting_waiting_room_join") .default_text_style_white_600(styleSize: 20) .frame(height: 35) .frame(maxWidth: .infinity) diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index fab330f10..a673a81de 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -43,7 +43,7 @@ struct ChatBubbleView: View { HStack { if eventLogMessage.eventModel.eventLogType == .ConferenceChatMessage { VStack { - if !eventLogMessage.message.text.isEmpty || !eventLogMessage.message.attachments.isEmpty { + if !eventLogMessage.message.text.isEmpty || !eventLogMessage.message.attachments.isEmpty || eventLogMessage.message.isIcalendar { HStack(alignment: .top, content: { if eventLogMessage.message.isOutgoing { Spacer() @@ -159,7 +159,7 @@ struct ChatBubbleView: View { VStack(alignment: eventLogMessage.message.isOutgoing ? .trailing : .leading) { VStack(alignment: eventLogMessage.message.isOutgoing ? .trailing : .leading) { - if !eventLogMessage.message.attachments.isEmpty { + if !eventLogMessage.message.attachments.isEmpty && !eventLogMessage.message.isIcalendar { messageAttachments() } @@ -169,20 +169,129 @@ struct ChatBubbleView: View { .default_text_style(styleSize: 14) } - if eventLogMessage.message.isIcalendar { - VStack{ + if eventLogMessage.message.isIcalendar && eventLogMessage.message.messageConferenceInfo != nil { + VStack(spacing: 0) { VStack { + if eventLogMessage.message.messageConferenceInfo!.meetingState != .new { + if eventLogMessage.message.messageConferenceInfo!.meetingState == .updated { + Text("conversation_message_meeting_updated_label") + .foregroundStyle(Color.orangeWarning600) + .default_text_style_600(styleSize: 12) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 5) + } else { + Text("conversation_message_meeting_cancelled_label") + .foregroundStyle(Color.redDanger500) + .default_text_style_600(styleSize: 12) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 5) + } + } + HStack { + VStack(spacing: 0) { + Text(eventLogMessage.message.messageConferenceInfo!.meetingDay) + .default_text_style(styleSize: 16) + + Text(eventLogMessage.message.messageConferenceInfo!.meetingDayNumber) + .foregroundStyle(.white) + .default_text_style_800(styleSize: 18) + .lineLimit(1) + .frame(width: 30, height: 30, alignment: .center) + .background(Color.orangeMain500) + .clipShape(Circle()) + + } + .padding(.all, 10) + .frame(width: 70, height: 70) + .background(.white) + .cornerRadius(15) + .shadow(color: .black.opacity(0.1), radius: 15) + + VStack { + HStack { + Image("video-conference") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + + Text(eventLogMessage.message.messageConferenceInfo!.meetingSubject) + .default_text_style_800(styleSize: 15) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxWidth: .infinity, alignment: .leading) + + Text(eventLogMessage.message.messageConferenceInfo!.meetingDate) + .default_text_style_300(styleSize: 14) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(eventLogMessage.message.messageConferenceInfo!.meetingTime) + .default_text_style_300(styleSize: 14) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.leading, 5) + } + .frame(maxWidth: .infinity) } + .padding(.all, 15) + .frame(maxWidth: .infinity) + .background(Color.gray100) - VStack { + VStack(spacing: 2) { + if !eventLogMessage.message.messageConferenceInfo!.meetingDescription.isEmpty { + Text("Description") + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(eventLogMessage.message.messageConferenceInfo!.meetingDescription) + .default_text_style_300(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + } + if eventLogMessage.message.messageConferenceInfo!.meetingState != .cancelled { + HStack { + Image("users") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 20, height: 20) + + Text(eventLogMessage.message.messageConferenceInfo!.meetingParticipants) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + + Button(action: { + conversationViewModel.joinMeetingInvite(addressUri: eventLogMessage.message.messageConferenceInfo!.meetingConferenceUri) + }, label: { + Text("meeting_waiting_room_join") + .default_text_style_white_600(styleSize: 14) + }) + .padding(.horizontal, 15) + .padding(.vertical, 10) + .background(Color.orangeMain500) + .cornerRadius(60) + } + .padding(.top, !eventLogMessage.message.messageConferenceInfo!.meetingDescription.isEmpty ? 10 : 0) + } } + .padding(.all, + eventLogMessage.message.messageConferenceInfo!.meetingState != .cancelled + || !eventLogMessage.message.messageConferenceInfo!.meetingDescription.isEmpty + ? 15 + : 0 + ) + .frame(maxWidth: .infinity) + .background(.white) } - - Text("Meeting invite !!") - .foregroundStyle(Color.grayMain2c500) - .default_text_style(styleSize: 12) + .frame(width: geometryProxy.size.width - 110) + .background(.white) + .cornerRadius(10) } HStack(alignment: .center) { diff --git a/Linphone/UI/Main/Conversations/Model/MessageConferenceInfo.swift b/Linphone/UI/Main/Conversations/Model/MessageConferenceInfo.swift index 6bd5ec3ec..489fcf210 100644 --- a/Linphone/UI/Main/Conversations/Model/MessageConferenceInfo.swift +++ b/Linphone/UI/Main/Conversations/Model/MessageConferenceInfo.swift @@ -27,7 +27,7 @@ public enum MessageConferenceState: Codable { public struct MessageConferenceInfo: Codable, Identifiable, Hashable { public let id: UUID - public let meetingConferenceUri: URL + public let meetingConferenceUri: String public let meetingSubject: String public let meetingDescription: String public let meetingState: MessageConferenceState @@ -37,7 +37,7 @@ public struct MessageConferenceInfo: Codable, Identifiable, Hashable { public let meetingDayNumber: String public let meetingParticipants: String - public init(id: UUID, meetingConferenceUri: URL, meetingSubject: String, meetingDescription: String, meetingState: MessageConferenceState, meetingDate: String, meetingTime: String, meetingDay: String, meetingDayNumber: String, meetingParticipants: String) { + public init(id: UUID, meetingConferenceUri: String, meetingSubject: String, meetingDescription: String, meetingState: MessageConferenceState, meetingDate: String, meetingTime: String, meetingDay: String, meetingDayNumber: String, meetingParticipants: String) { self.id = id self.meetingConferenceUri = meetingConferenceUri self.meetingSubject = meetingSubject diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index c1fceee5b..f54dd238d 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -1915,7 +1915,7 @@ class ConversationViewModel: ObservableObject { } func parseConferenceInvite(content: Content) -> MessageConferenceInfo? { - var meetingConferenceUriTmp: URL? + var meetingConferenceUriTmp: String = "" var meetingSubjectTmp: String = "" var meetingDescriptionTmp: String = "" var meetingStateTmp: MessageConferenceState = .new @@ -1930,7 +1930,7 @@ class ConversationViewModel: ObservableObject { if let conferenceAddress = conferenceInfo.uri { let conferenceUri = conferenceAddress.asStringUriOnly() Log.info("Found conference info with URI [\(conferenceUri)] and subject [\(conferenceInfo.subject ?? "")]") - meetingConferenceUriTmp = URL(string: conferenceAddress.asStringUriOnly()) + meetingConferenceUriTmp = conferenceAddress.asStringUriOnly() meetingSubjectTmp = conferenceInfo.subject ?? "" meetingDescriptionTmp = conferenceInfo.description ?? "" @@ -1949,7 +1949,7 @@ class ConversationViewModel: ObservableObject { dateFormatter.dateStyle = .full dateFormatter.timeStyle = .none - meetingDateTmp = dateFormatter.string(from: dateTmp) + meetingDateTmp = dateFormatter.string(from: dateTmp).capitalized let timeFormatter = DateFormatter() timeFormatter.dateFormat = Locale.current.identifier == "fr_FR" ? "HH:mm" : "h:mm a" @@ -1962,14 +1962,14 @@ class ConversationViewModel: ObservableObject { meetingTimeTmp = "\(timeTmp) - \(endTime)" meetingDayTmp = dateTmp.formatted(Date.FormatStyle().weekday(.abbreviated)).capitalized - meetingDayNumberTmp = dateTmp.formatted(Date.FormatStyle().day(.twoDigits)) + meetingDayNumberTmp = dateTmp.formatted(Date.FormatStyle().day(.defaultDigits)) - meetingParticipantsTmp = String(conferenceInfo.participantInfos.count) + meetingParticipantsTmp = String(conferenceInfo.participantInfos.count) + " participant" + (conferenceInfo.participantInfos.count > 1 ? "s" : "") - if meetingConferenceUriTmp != nil { + if !meetingConferenceUriTmp.isEmpty { return MessageConferenceInfo( id: UUID(), - meetingConferenceUri: meetingConferenceUriTmp!, + meetingConferenceUri: meetingConferenceUriTmp, meetingSubject: meetingSubjectTmp, meetingDescription: meetingDescriptionTmp, meetingState: meetingStateTmp, @@ -1985,6 +1985,14 @@ class ConversationViewModel: ObservableObject { return nil } + + func joinMeetingInvite(addressUri: String) { + coreContext.doOnCoreQueue { _ in + if let address = try? Factory.Instance.createAddress(addr: addressUri) { + TelecomManager.shared.doCallOrJoinConf(address: address) + } + } + } } // swiftlint:enable line_length // swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index 505477d01..ab49963ef 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -357,7 +357,7 @@ struct HistoryContactFragment: View { .background(Color.grayMain2c200) .cornerRadius(40) - Text("Rejoindre") + Text("meeting_waiting_room_join") .default_text_style(styleSize: 14) .frame(minWidth: 80) } diff --git a/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift b/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift index 05cfc0b30..1a5ce37d8 100644 --- a/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift +++ b/Linphone/UI/Main/Meetings/Fragments/MeetingFragment.swift @@ -299,8 +299,7 @@ struct MeetingFragment: View { Spacer() Button(action: { - TelecomManager.shared.meetingWaitingRoomSelected = try? Factory.Instance.createAddress(addr: meetingViewModel.displayedMeeting?.address ?? "") - TelecomManager.shared.meetingWaitingRoomDisplayed = true + meetingViewModel.joinMeeting(addressUri: meetingViewModel.displayedMeeting?.address ?? "") }, label: { Text("Join the meeting now") .bold() diff --git a/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift b/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift index 84ac81e15..2bfbc9007 100644 --- a/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift +++ b/Linphone/UI/Main/Meetings/ViewModel/MeetingViewModel.swift @@ -379,6 +379,14 @@ class MeetingViewModel: ObservableObject { } }) } + + func joinMeeting(addressUri: String) { + CoreContext.shared.doOnCoreQueue { _ in + if let address = try? Factory.Instance.createAddress(addr: addressUri) { + TelecomManager.shared.doCallOrJoinConf(address: address) + } + } + } } // swiftlint:enable type_body_length