From 0a162390a3f9fbce45423cc86ce19fa9c742b214 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 29 Oct 2024 17:25:48 +0100 Subject: [PATCH] Add ephemeral message selector view --- Linphone.xcodeproj/project.pbxproj | 4 + .../ephemeral.imageset/Contents.json | 21 +++ .../ephemeral.imageset/ephemeral.svg | 16 ++ Linphone/Localizable.xcstrings | 124 +++++++++++++- .../Fragments/ConversationFragment.swift | 23 ++- .../Fragments/EphemeralFragment.swift | 157 ++++++++++++++++++ .../ViewModel/ConversationViewModel.swift | 72 ++++++++ 7 files changed, 409 insertions(+), 8 deletions(-) create mode 100644 Linphone/Assets.xcassets/ephemeral.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/ephemeral.imageset/ephemeral.svg create mode 100644 Linphone/UI/Main/Conversations/Fragments/EphemeralFragment.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 41fff5fee..07e493978 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -169,6 +169,7 @@ D7E6D0512AEBDBD500A57AAF /* ContactsListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0502AEBDBD500A57AAF /* ContactsListBottomSheet.swift */; }; D7E6D0552AEBFCCE00A57AAF /* ContactsInnerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E6D0542AEBFCCE00A57AAF /* ContactsInnerFragment.swift */; }; D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EAACCE2AD6ED8000AA6A8A /* PermissionsFragment.swift */; }; + D7EFD1E42CD11F70005E67CD /* EphemeralFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EFD1E32CD11F53005E67CD /* EphemeralFragment.swift */; }; D7F4D9CB2B5FD27200CDCD76 /* CallsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F4D9CA2B5FD27200CDCD76 /* CallsListFragment.swift */; }; D7F5F6412C359F3B007FCF2F /* SipAddressesPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7F5F6402C359F3B007FCF2F /* SipAddressesPopup.swift */; }; D7FB55112AD447FD00A5AB15 /* RegisterFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FB55102AD447FD00A5AB15 /* RegisterFragment.swift */; }; @@ -358,6 +359,7 @@ D7E6D0502AEBDBD500A57AAF /* ContactsListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsListBottomSheet.swift; sourceTree = ""; }; D7E6D0542AEBFCCE00A57AAF /* ContactsInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsInnerFragment.swift; sourceTree = ""; }; D7EAACCE2AD6ED8000AA6A8A /* PermissionsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsFragment.swift; sourceTree = ""; }; + D7EFD1E32CD11F53005E67CD /* EphemeralFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralFragment.swift; sourceTree = ""; }; D7F4D9CA2B5FD27200CDCD76 /* CallsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallsListFragment.swift; sourceTree = ""; }; D7F5F6402C359F3B007FCF2F /* SipAddressesPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SipAddressesPopup.swift; sourceTree = ""; }; D7FB55102AD447FD00A5AB15 /* RegisterFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterFragment.swift; sourceTree = ""; }; @@ -845,6 +847,7 @@ D7CEE0392B7A232200FD79B7 /* Fragments */ = { isa = PBXGroup; children = ( + D7EFD1E32CD11F53005E67CD /* EphemeralFragment.swift */, D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */, D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */, D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */, @@ -1168,6 +1171,7 @@ D75759322B56D40900E7AC10 /* ZRTPPopup.swift in Sources */, D78E062E2BEA69F400CE3783 /* AudioRouteBottomSheet.swift in Sources */, D7A0ACBB2C415D630043AE79 /* StartGroupConversationFragment.swift in Sources */, + D7EFD1E42CD11F70005E67CD /* EphemeralFragment.swift in Sources */, D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */, D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */, D70C82A52C85EDCA0087F43F /* ConversationForwardMessageFragment.swift in Sources */, diff --git a/Linphone/Assets.xcassets/ephemeral.imageset/Contents.json b/Linphone/Assets.xcassets/ephemeral.imageset/Contents.json new file mode 100644 index 000000000..dd7b7c7fb --- /dev/null +++ b/Linphone/Assets.xcassets/ephemeral.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ephemeral.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/ephemeral.imageset/ephemeral.svg b/Linphone/Assets.xcassets/ephemeral.imageset/ephemeral.svg new file mode 100644 index 000000000..4726a9647 --- /dev/null +++ b/Linphone/Assets.xcassets/ephemeral.imageset/ephemeral.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 112190c0e..1dd997c28 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -1031,22 +1031,134 @@ } }, "conversation_ephemeral_messages_duration_disabled" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disabled" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Désactiver" + } + } + } }, "conversation_ephemeral_messages_duration_one_day" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 day" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 jour" + } + } + } }, "conversation_ephemeral_messages_duration_one_hour" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 hour" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 heure" + } + } + } }, "conversation_ephemeral_messages_duration_one_minute" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 minute" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 minute" + } + } + } }, "conversation_ephemeral_messages_duration_one_week" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 week" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "1 semaine" + } + } + } }, "conversation_ephemeral_messages_duration_three_days" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 days" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 jours" + } + } + } + }, + "conversation_ephemeral_messages_subtitle" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New messages will be automatically deleted once read by everyone.\nChoose a duration:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les messages éphémères seront automatiquement supprimés une fois lu par tout le monde et après un certain délai :" + } + } + } + }, + "conversation_ephemeral_messages_title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ephemeral messages" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Messages éphémères" + } + } + } }, "conversation_event_admin_set" : { "extractionState" : "manual", diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index a147414a5..57f6c98b1 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -55,6 +55,7 @@ struct ConversationFragment: View { @State private var voiceRecordingInProgress = false @State private var isShowConversationForwardMessageFragment = false + @State private var isShowEphemeralFragment = false @Binding var isShowConversationFragment: Bool @Binding var isShowStartCallGroupPopup: Bool @@ -202,7 +203,7 @@ struct ConversationFragment: View { .padding(.top, 4) .lineLimit(1) - if isMuted || conversationViewModel.displayedConversation!.isEphemeral { + if isMuted || conversationViewModel.ephemeralTime != NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") { HStack { if isMuted { Image("bell-slash") @@ -212,12 +213,18 @@ struct ConversationFragment: View { .frame(width: 16, height: 16, alignment: .trailing) } - if conversationViewModel.displayedConversation!.isEphemeral { + 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() @@ -279,6 +286,9 @@ struct ConversationFragment: View { Button { isMenuOpen = false + withAnimation { + isShowEphemeralFragment = true + } } label: { HStack { Text("conversation_menu_configure_ephemeral_messages") @@ -946,6 +956,15 @@ struct ConversationFragment: View { .zIndex(5) .transition(.move(edge: .trailing)) } + + if isShowEphemeralFragment { + EphemeralFragment( + conversationViewModel: conversationViewModel, + isShowEphemeralFragment: $isShowEphemeralFragment + ) + .zIndex(5) + .transition(.move(edge: .trailing)) + } } } // swiftlint:enable cyclomatic_complexity diff --git a/Linphone/UI/Main/Conversations/Fragments/EphemeralFragment.swift b/Linphone/UI/Main/Conversations/Fragments/EphemeralFragment.swift new file mode 100644 index 000000000..a143ed72c --- /dev/null +++ b/Linphone/UI/Main/Conversations/Fragments/EphemeralFragment.swift @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import SwiftUI +import linphonesw + +struct EphemeralFragment: View { + @ObservedObject var conversationViewModel: ConversationViewModel + + @State private var selectedOption = NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") + let options = [ + NSLocalizedString("conversation_ephemeral_messages_duration_one_minute", comment: ""), + NSLocalizedString("conversation_ephemeral_messages_duration_one_hour", comment: ""), + NSLocalizedString("conversation_ephemeral_messages_duration_one_day", comment: ""), + NSLocalizedString("conversation_ephemeral_messages_duration_three_days", comment: ""), + NSLocalizedString("conversation_ephemeral_messages_duration_one_week", comment: ""), + NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") + ] + + @Binding var isShowEphemeralFragment: Bool + + var body: some View { + NavigationView { + GeometryReader { geometry in + ZStack { + VStack(spacing: 1) { + + Rectangle() + .foregroundStyle(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + + HStack { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) + .padding(.top, 2) + .padding(.leading, -10) + .onTapGesture { + withAnimation { + isShowEphemeralFragment = false + conversationViewModel.setEphemeralTime(lifetimeString: selectedOption) + } + } + + Text("conversation_ephemeral_messages_title") + .multilineTextAlignment(.leading) + .default_text_style_orange_800(styleSize: 16) + + Spacer() + + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + VStack(spacing: 0) { + ScrollView { + VStack(spacing: 20) { + Image("ephemeral") + .resizable() + .scaledToFit() + .frame(width: geometry.size.width/2.5) + + Text("conversation_ephemeral_messages_subtitle") + .default_text_style(styleSize: 14) + .multilineTextAlignment(.center) + + VStack { + ForEach(options, id: \.self) { option in + Button(action: { + selectedOption = option + }) { + VStack { + HStack { + Image(selectedOption == option ? "radio-button-fill" : "radio-button") + + Text(option) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.top, 2) + + if option != NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") { + Divider() + } + } + .background(.white) + .frame(maxWidth: .infinity) + } + .background(.white) + .frame(maxWidth: .infinity) + .buttonStyle(PlainButtonStyle()) + } + } + .padding() + .background(.white) + .cornerRadius(10) + } + .padding(.horizontal, 10) + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + .padding(.horizontal, 10) + } + .frame(maxWidth: .infinity) + } + .frame(maxWidth: .infinity) + } + .background(Color.gray100) + } + .navigationTitle("") + .navigationBarHidden(true) + .onAppear { + conversationViewModel.getEphemeralTime() + selectedOption = conversationViewModel.ephemeralTime + } + .onChange(of: conversationViewModel.ephemeralTime) { _ in + selectedOption = conversationViewModel.ephemeralTime + } + .onDisappear { + withAnimation { + isShowEphemeralFragment = false + } + } + } + } + .navigationViewStyle(StackNavigationViewStyle()) + } +} + +#Preview { + EphemeralFragment( + conversationViewModel: ConversationViewModel(), + isShowEphemeralFragment: .constant(true) + ) +} diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 213f415f9..6723d3b4f 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -23,6 +23,7 @@ import SwiftUI import AVFoundation // swiftlint:disable line_length +// swiftlint:disable file_length // swiftlint:disable type_body_length // swiftlint:disable cyclomatic_complexity @@ -37,6 +38,9 @@ class ConversationViewModel: ObservableObject { @Published var messageText: String = "" @Published var composingLabel: String = "" + @Published var isEphemeral: Bool = false + @Published var ephemeralTime: String = NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") + // Used to keep track of a ChatRoom callback without having to worry about life cycle // Init will add the delegate, deinit will remove it class ChatRoomDelegateHolder { @@ -122,6 +126,8 @@ class ConversationViewModel: ObservableObject { self.getNewMessages(eventLogs: [eventLogs]) }, onSubjectChanged: { (_: ChatRoom, eventLogs: EventLog) in self.getNewMessages(eventLogs: [eventLogs]) + }, onEphemeralEvent: {(_: ChatRoom, eventLogs: EventLog) in + self.getNewMessages(eventLogs: [eventLogs]) }, onEphemeralMessageDeleted: {(_: ChatRoom, eventLog: EventLog) in self.removeMessage(eventLog) }) @@ -329,6 +335,7 @@ class ConversationViewModel: ObservableObject { self.getUnreadMessagesCount() self.getParticipantConversationModel() self.computeComposingLabel() + self.getEphemeralTime() self.mediasToSend.removeAll() self.messageToReply = nil @@ -1990,6 +1997,70 @@ class ConversationViewModel: ObservableObject { } } } + + func setEphemeralTime(lifetimeString: String) { + coreContext.doOnCoreQueue { _ in + if self.displayedConversation != nil { + var lifetime: Int = 0 + + switch lifetimeString { + case NSLocalizedString("conversation_ephemeral_messages_duration_one_minute", comment: ""): + lifetime = 60 + case NSLocalizedString("conversation_ephemeral_messages_duration_one_hour", comment: ""): + lifetime = 3600 + case NSLocalizedString("conversation_ephemeral_messages_duration_one_day", comment: ""): + lifetime = 86400 + case NSLocalizedString("conversation_ephemeral_messages_duration_three_days", comment: ""): + lifetime = 259200 + case NSLocalizedString("conversation_ephemeral_messages_duration_one_week", comment: ""): + lifetime = 604800 + default: + lifetime = 0 + } + + if lifetime == 0 { + self.displayedConversation!.chatRoom.ephemeralEnabled = false + self.displayedConversation!.chatRoom.ephemeralLifetime = lifetime + } else { + self.displayedConversation!.chatRoom.ephemeralEnabled = true + self.displayedConversation!.chatRoom.ephemeralLifetime = lifetime + } + + self.getEphemeralTime() + } + } + } + + func getEphemeralTime() { + coreContext.doOnCoreQueue { _ in + if self.displayedConversation != nil { + + let lifetime = self.displayedConversation!.chatRoom.ephemeralLifetime + DispatchQueue.main.async { + switch lifetime { + case 60: + self.isEphemeral = true + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_minute", comment: "") + case 3600: + self.isEphemeral = true + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_hour", comment: "") + case 86400: + self.isEphemeral = true + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_day", comment: "") + case 259200: + self.isEphemeral = true + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_three_days", comment: "") + case 604800: + self.isEphemeral = true + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_one_week", comment: "") + default: + self.isEphemeral = false + self.ephemeralTime = NSLocalizedString("conversation_ephemeral_messages_duration_disabled", comment: "") + } + } + } + } + } } // swiftlint:enable line_length // swiftlint:enable type_body_length @@ -2278,3 +2349,4 @@ class AudioRecorder: NSObject, ObservableObject { return headsetCard ?? bluetoothCard ?? microphoneCard } } +// swiftlint:enable file_length