Added file picker, updated unsecured chatroom icon, and replaced floating button with a scroll-down icon in the chatroom

This commit is contained in:
Benoit Martins 2025-05-06 16:39:17 +02:00
parent 7d6b2d8e0b
commit 4f1fcbbcf6
23 changed files with 568 additions and 180 deletions

View file

@ -7,7 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
4ED1F0A881A9ACB5977A8987 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
4ED1F0A881A9ACB5977A8987 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
660AAF7F2B839272004C0FA6 /* msgNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 660AAF7B2B839271004C0FA6 /* msgNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
660D8A712B517D260092694D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 660D8A702B517D260092694D /* GoogleService-Info.plist */; };
6613A0AE2BAEB7DF008923A4 /* MeetingFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6613A0AD2BAEB7DF008923A4 /* MeetingFragment.swift */; };
@ -56,6 +56,7 @@
C6A5A9482C10B6A30070FEA4 /* AuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A5A9472C10B6A30070FEA4 /* AuthState.swift */; };
C6DC4E3D2C199C4E009096FD /* BundleExtenion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DC4E3C2C199C4E009096FD /* BundleExtenion.swift */; };
C6DC4E3F2C19C289009096FD /* SideMenuEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DC4E3E2C19C289009096FD /* SideMenuEntry.swift */; };
D703F7082DC8C605005B8F75 /* FilePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D703F7072DC8C5FF005B8F75 /* FilePicker.swift */; };
D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */; };
D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */; };
D70A26EE2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */; };
@ -262,6 +263,7 @@
C6A5A9472C10B6A30070FEA4 /* AuthState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthState.swift; sourceTree = "<group>"; };
C6DC4E3C2C199C4E009096FD /* BundleExtenion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtenion.swift; sourceTree = "<group>"; };
C6DC4E3E2C19C289009096FD /* SideMenuEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuEntry.swift; sourceTree = "<group>"; };
D703F7072DC8C5FF005B8F75 /* FilePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePicker.swift; sourceTree = "<group>"; };
D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationModel.swift; sourceTree = "<group>"; };
D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListBottomSheet.swift; sourceTree = "<group>"; };
@ -413,7 +415,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4ED1F0A881A9ACB5977A8987 /* (null) in Frameworks */,
4ED1F0A881A9ACB5977A8987 /* BuildFile in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -542,6 +544,7 @@
D717071C2AC591EF0037746F /* Utils */ = {
isa = PBXGroup;
children = (
D703F7072DC8C5FF005B8F75 /* FilePicker.swift */,
D717A10D2CEB770D00849D92 /* ShareSheetController.swift */,
66C491F72B24D25A00CEA16D /* Extensions */,
66C491FC2B24D36500CEA16D /* AudioRouteUtils.swift */,
@ -1346,6 +1349,7 @@
D74DA0122C047F0700A8561D /* HistoryModel.swift in Sources */,
66D382052CEB7E0A0063E1C5 /* ShortcutModel.swift in Sources */,
D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */,
D703F7082DC8C605005B8F75 /* FilePicker.swift in Sources */,
C6DC4E3F2C19C289009096FD /* SideMenuEntry.swift in Sources */,
D714DE622C1C4636006C1F1D /* RegisterCodeConfirmationFragment.swift in Sources */,
66C468FB2D2BE54800A836F7 /* PIPViewModel.swift in Sources */,

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#4e6074" viewBox="0 0 256 256"><path d="M208,56H180.28L166.65,35.56A8,8,0,0,0,160,32H96a8,8,0,0,0-6.65,3.56L75.71,56H48A24,24,0,0,0,24,80V192a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V80A24,24,0,0,0,208,56Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H80a8,8,0,0,0,6.66-3.56L100.28,48h55.43l13.63,20.44A8,8,0,0,0,176,72h32a8,8,0,0,1,8,8ZM128,88a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,88Zm0,72a28,28,0,1,1,28-28A28,28,0,0,1,128,160Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M208,56H180.28L166.65,35.56A8,8,0,0,0,160,32H96a8,8,0,0,0-6.65,3.56L75.71,56H48A24,24,0,0,0,24,80V192a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V80A24,24,0,0,0,208,56Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H80a8,8,0,0,0,6.66-3.56L100.28,48h55.43l13.63,20.44A8,8,0,0,0,176,72h32a8,8,0,0,1,8,8ZM128,88a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,88Zm0,72a28,28,0,1,1,28-28A28,28,0,0,1,128,160Z"></path></svg>

Before

Width:  |  Height:  |  Size: 523 B

After

Width:  |  Height:  |  Size: 523 B

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "caret-double-down.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M213.66,130.34a8,8,0,0,1,0,11.32l-80,80a8,8,0,0,1-11.32,0l-80-80a8,8,0,0,1,11.32-11.32L128,204.69l74.34-74.35A8,8,0,0,1,213.66,130.34Zm-91.32,11.32a8,8,0,0,0,11.32,0l80-80a8,8,0,0,0-11.32-11.32L128,124.69,53.66,50.34A8,8,0,0,0,42.34,61.66Z"></path></svg>

After

Width:  |  Height:  |  Size: 363 B

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "image.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,16V158.75l-26.07-26.06a16,16,0,0,0-22.63,0l-20,20-44-44a16,16,0,0,0-22.62,0L40,149.37V56ZM40,172l52-52,80,80H40Zm176,28H194.63l-36-36,20-20L216,181.38V200ZM144,100a12,12,0,1,1,12,12A12,12,0,0,1,144,100Z"></path></svg>

After

Width:  |  Height:  |  Size: 424 B

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "lock-simple-open-bold.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M208,76H100V56a28,28,0,0,1,28-28c13.51,0,25.65,9.62,28.24,22.39a12,12,0,1,0,23.52-4.78C174.87,21.5,153.1,4,128,4A52.06,52.06,0,0,0,76,56V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76Zm-4,128H52V100H204Z"></path></svg>

After

Width:  |  Height:  |  Size: 370 B

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "lock-simple-open.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M208,80H96V56a32,32,0,0,1,32-32c15.37,0,29.2,11,32.16,25.59a8,8,0,0,0,15.68-3.18C171.32,24.15,151.2,8,128,8A48.05,48.05,0,0,0,80,56V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80Zm0,128H48V96H208V208Z"></path></svg>

After

Width:  |  Height:  |  Size: 367 B

View file

@ -46,6 +46,7 @@ final class CoreContext: ObservableObject {
var mCore: Core!
var bearerAuthInfoPendingPasswordUpdate: AuthInfo?
var imdnToEverybodyThreshold: Bool = true
let monitor = NWPathMonitor()
var networkStatusIsConnected: Bool = true // updated on core queue
@ -154,7 +155,7 @@ final class CoreContext: ObservableObject {
self.mCore.config!.setString(section: "misc", key: "version_check_url_root", value: "https://download.linphone.org/releases")
self.mCore.imdnToEverybodyThreshold = 1
self.imdnToEverybodyThreshold = self.mCore.imdnToEverybodyThreshold == 1
//self.copyDatabaseFileToDocumentsDirectory()
let shortcutsCount = self.mCore.config!.getInt(section: "ui", key: "shortcut_count", defaultValue: 0)

View file

@ -340,7 +340,7 @@ struct ChatBubbleView: View {
.progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500))
.frame(width: 10, height: 10)
.padding(.top, 1)
} else if eventLogMessage.message.status != nil {
} else if eventLogMessage.message.status != nil && !(CoreContext.shared.imdnToEverybodyThreshold && !eventLogMessage.message.isOutgoing) {
Image(conversationViewModel.getImageIMDN(status: eventLogMessage.message.status!))
.renderingMode(.template)
.resizable()
@ -374,9 +374,11 @@ struct ChatBubbleView: View {
}
}
.onTapGesture {
if !(CoreContext.shared.imdnToEverybodyThreshold && !eventLogMessage.message.isOutgoing) {
conversationViewModel.selectedMessageToDisplayDetails = eventLogMessage
conversationViewModel.prepareBottomSheetForDeliveryStatus()
}
}
.disabled(conversationViewModel.selectedMessage != nil)
.padding(.top, -4)
}
@ -542,7 +544,6 @@ struct ChatBubbleView: View {
onImageTapped: {
selectedURLAttachment = eventLogMessage.message.attachments.first!.full
})
.overlay(
Group {
if eventLogMessage.message.attachments.first!.type == .video {
@ -645,10 +646,24 @@ struct ChatBubbleView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
if eventLogMessage.message.attachments.first!.size > 0 {
Text(eventLogMessage.message.attachments.first!.size.formatBytes())
.default_text_style_300(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
} else {
if let size = self.getFileSize(atPath: eventLogMessage.message.attachments.first!.full.path) {
Text(size.formatBytes())
.default_text_style_300(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
} else {
Text(eventLogMessage.message.attachments.first!.size.formatBytes())
.default_text_style_300(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
}
}
}
.padding(.horizontal, 10)
.frame(maxWidth: .infinity, alignment: .leading)
@ -762,10 +777,24 @@ struct ChatBubbleView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
if attachment.size > 0 {
Text(attachment.size.formatBytes())
.default_text_style_300(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
} else {
if let size = self.getFileSize(atPath: attachment.full.path) {
Text(size.formatBytes())
.default_text_style_300(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
} else {
Text(attachment.size.formatBytes())
.default_text_style_300(styleSize: 14)
.frame(maxWidth: .infinity, alignment: .leading)
.lineLimit(1)
}
}
}
.padding(.horizontal, 10)
.frame(maxWidth: .infinity, alignment: .leading)
@ -830,6 +859,18 @@ struct ChatBubbleView: View {
}
}
}
private func getFileSize(atPath path: String) -> Int? {
do {
let attributes = try FileManager.default.attributesOfItem(atPath: path)
if let fileSize = attributes[.size] as? Int {
return fileSize
}
} catch {
print("Error: \(error)")
}
return nil
}
}
struct DynamicLinkText: View {

View file

@ -55,8 +55,11 @@ struct ConversationFragment: View {
@State private var displayFloatingButton = false
@State private var areFilePickersOpen = false
@State private var isShowPhotoLibrary = false
@State private var isShowCamera = false
@State private var isShowFilePicker = false
@State private var mediasIsLoading = false
@State private var voiceRecordingInProgress = false
@ -117,6 +120,23 @@ struct ConversationFragment: View {
}
.edgesIgnoringSafeArea(.all)
})
.sheet(isPresented: $isShowFilePicker, onDismiss: {
isShowFilePicker = false
}, content: {
FilePicker(onDocumentsPicked: { urlList in
FilePicker.convertToAttachmentArray(fromResults: urlList) { mediasOrNil, errorOrNil in
if let error = errorOrNil {
print(error)
}
if let medias = mediasOrNil {
conversationViewModel.mediasToSend.append(contentsOf: medias)
}
self.mediasIsLoading = false
}
})
.edgesIgnoringSafeArea(.all)
})
.fullScreenCover(isPresented: $isShowCamera) {
ImagePicker(conversationViewModel: conversationViewModel, selectedMedia: self.$conversationViewModel.mediasToSend)
.edgesIgnoringSafeArea(.all)
@ -421,7 +441,7 @@ struct ConversationFragment: View {
} label: {
ZStack {
Image("caret-down")
Image("caret-double-down")
.renderingMode(.template)
.foregroundStyle(.white)
.padding()
@ -555,6 +575,8 @@ struct ConversationFragment: View {
.fill(Color(.white))
.frame(width: 100, height: 100)
VStack {
if attachment.type == .image || attachment.type == .gif || attachment.type == .video {
AsyncImage(url: attachment.thumbnail) { image in
ZStack {
image
@ -573,6 +595,21 @@ struct ConversationFragment: View {
} placeholder: {
ProgressView()
}
} else {
VStack {
Spacer()
Text(attachment.name)
.foregroundStyle(Color.grayMain2c700)
.default_text_style_800(styleSize: 14)
.truncationMode(.middle)
.frame(maxWidth: .infinity, alignment: .center)
.multilineTextAlignment(.center)
.lineLimit(2)
Spacer()
}
.background(Color.grayMain2c200)
}
}
.layoutPriority(-1)
.onTapGesture {
if conversationViewModel.mediasToSend.count == 1 {
@ -620,46 +657,94 @@ struct ConversationFragment: View {
.transition(.move(edge: .bottom))
}
HStack(spacing: 0) {
if !voiceRecordingInProgress {
Button {
} label: {
Image("smiley")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c500)
.frame(width: 28, height: 28, alignment: .leading)
.padding(.all, 6)
.padding(.top, 4)
}
.padding(.horizontal, isMessageTextFocused ? 0 : 2)
Button {
self.isShowPhotoLibrary = true
self.mediasIsLoading = true
} label: {
Image("paperclip")
.renderingMode(.template)
.resizable()
.foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
.frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading)
.padding(.all, isMessageTextFocused ? 0 : 6)
.padding(.top, 4)
.disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading)
}
.padding(.horizontal, isMessageTextFocused ? 0 : 2)
if areFilePickersOpen {
ZStack(alignment: .top) {
HStack {
Button {
self.areFilePickersOpen.toggle()
self.isShowCamera = true
} label: {
VStack {
Image("camera")
.renderingMode(.template)
.resizable()
.foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
.frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading)
.padding(.all, isMessageTextFocused ? 0 : 6)
.frame(width: 25, height: 25, alignment: .leading)
Text("conversation_take_picture_label")
.foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
.default_text_style_300(styleSize: 15)
.frame(maxWidth: .infinity, alignment: .center)
.lineLimit(1)
}
}
.disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading)
Button {
self.areFilePickersOpen.toggle()
self.isShowPhotoLibrary = true
self.mediasIsLoading = true
} label: {
VStack {
Image("image")
.renderingMode(.template)
.resizable()
.foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
Text("conversation_pick_file_from_gallery_label")
.foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
.default_text_style_300(styleSize: 15)
.frame(maxWidth: .infinity, alignment: .center)
.lineLimit(1)
}
}
.disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading)
Button {
self.areFilePickersOpen.toggle()
self.isShowFilePicker = true
self.mediasIsLoading = true
} label: {
VStack {
Image("file")
.renderingMode(.template)
.resizable()
.foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
.frame(width: 25, height: 25, alignment: .leading)
Text("conversation_pick_any_file_label")
.foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
.default_text_style_300(styleSize: 15)
.frame(maxWidth: .infinity, alignment: .center)
.lineLimit(1)
}
}
.disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading)
}
.frame(maxWidth: .infinity)
.padding(.all, 20)
.background(Color.gray100)
}
.transition(.move(edge: .bottom))
}
HStack(spacing: 0) {
if !voiceRecordingInProgress {
Button {
withAnimation {
areFilePickersOpen.toggle()
}
} label: {
Image(areFilePickersOpen ? "x" : "paperclip")
.renderingMode(.template)
.resizable()
.foregroundStyle(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
.frame(width: 28, height: 28, alignment: .leading)
.padding(.all, 6)
.padding(.top, 4)
.disabled(conversationViewModel.maxMediaCount <= conversationViewModel.mediasToSend.count || mediasIsLoading)
.animation(.none, value: areFilePickersOpen)
}
.padding(.horizontal, isMessageTextFocused ? 0 : 2)

View file

@ -126,14 +126,6 @@ struct ConversationRow: View {
Spacer()
HStack {
if !conversation.encryptionEnabled {
Image("warning-circle")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.redDanger500)
.frame(width: 18, height: 18, alignment: .trailing)
}
Text(conversationsListViewModel.getCallTime(startDate: conversation.lastUpdateTime))
.foregroundStyle(Color.grayMain2c400)
.default_text_style(styleSize: 14)
@ -151,6 +143,14 @@ struct ConversationRow: View {
.frame(width: 18, height: 18, alignment: .trailing)
}
if !conversation.encryptionEnabled {
Image("lock-simple-open-bold")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.orangeWarning600)
.frame(width: 18, height: 18, alignment: .trailing)
}
if conversation.isMuted {
Image("bell-slash")
.renderingMode(.template)

View file

@ -49,8 +49,19 @@ class FloatingButton: UIButton {
private func setupButton() {
// Set the button's appearance
self.setImage(UIImage(named: "caret-down")?.withRenderingMode(.alwaysTemplate), for: .normal)
if let originalImage = UIImage(named: "caret-double-down")?.withRenderingMode(.alwaysTemplate) {
let newSize = CGSize(width: originalImage.size.width / 1.5, height: originalImage.size.height / 1.5)
UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0)
originalImage.draw(in: CGRect(origin: .zero, size: newSize))
var resizedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
resizedImage = resizedImage?.withRenderingMode(.alwaysTemplate)
self.setImage(resizedImage, for: .normal)
self.tintColor = .white
}
self.backgroundColor = UIColor(Color.orangeMain500)
self.layer.cornerRadius = 30
self.layer.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor

View file

@ -28,6 +28,8 @@ class ConversationModel: ObservableObject, Identifiable {
private var contactsManager = ContactsManager.shared
var chatRoom: ChatRoom
var lastMessage: ChatMessage?
let isDisabledBecauseNotSecured: Bool = false
static let TAG = "[Conversation Model]"
@ -51,6 +53,7 @@ class ConversationModel: ObservableObject, Identifiable {
@Published var avatarModel: ContactAvatarModel
private var conferenceDelegate: ConferenceDelegate?
private var chatMessageDelegate: ChatMessageDelegate?
init(chatRoom: ChatRoom) {
self.chatRoom = chatRoom
@ -77,6 +80,8 @@ class ConversationModel: ObservableObject, Identifiable {
self.encryptionEnabled = chatRoom.currentParams != nil && chatRoom.currentParams!.encryptionEnabled
self.lastMessage = nil
self.lastMessageText = ""
self.lastMessageIsOutgoing = false
@ -182,9 +187,37 @@ class ConversationModel: ObservableObject, Identifiable {
}
}
func chatMessageAddDelegate() {
if self.lastMessage != nil && self.chatMessageDelegate != nil {
self.lastMessage!.removeDelegate(delegate: self.chatMessageDelegate!)
}
self.chatMessageDelegate = ChatMessageDelegateStub(onMsgStateChanged: { (message: ChatMessage, msgState: ChatMessage.State) in
let lastMessageStateTmp = msgState.rawValue
DispatchQueue.main.async {
self.lastMessageState = lastMessageStateTmp
}
})
if self.lastMessage != nil && self.chatMessageDelegate != nil {
self.lastMessage!.addDelegate(delegate: self.chatMessageDelegate!)
}
}
func chatMessageRemoveDelegate() {
if self.lastMessage != nil && chatMessageDelegate != nil {
self.lastMessage!.removeDelegate(delegate: chatMessageDelegate!)
}
}
func getContentTextMessage(chatRoom: ChatRoom) {
let lastMessage = chatRoom.lastMessageInHistory
if lastMessage != nil {
if !(CoreContext.shared.imdnToEverybodyThreshold && !lastMessage!.isOutgoing) {
self.lastMessage = lastMessage
self.chatMessageAddDelegate()
}
var fromAddressFriend = lastMessage!.fromAddress != nil
? self.contactsManager.getFriendWithAddress(address: lastMessage!.fromAddress)?.name ?? nil
: nil

View file

@ -452,7 +452,6 @@ class ConversationViewModel: ObservableObject {
}
func getHistorySize() {
coreContext.doOnCoreQueue { _ in
if self.displayedConversation != nil {
let historySize = self.displayedConversation!.chatRoom.historyEventsSize
DispatchQueue.main.async {
@ -460,10 +459,8 @@ class ConversationViewModel: ObservableObject {
}
}
}
}
func getUnreadMessagesCount() {
coreContext.doOnCoreQueue { _ in
if self.displayedConversation != nil {
let unreadMessagesCount = self.displayedConversation!.chatRoom.unreadMessagesCount
DispatchQueue.main.async {
@ -471,7 +468,6 @@ class ConversationViewModel: ObservableObject {
}
}
}
}
func markAsRead() {
coreContext.doOnCoreQueue { _ in
@ -490,7 +486,6 @@ class ConversationViewModel: ObservableObject {
}
func getParticipantConversationModel() {
coreContext.doOnCoreQueue { _ in
if self.displayedConversation != nil {
DispatchQueue.main.async {
self.isUserAdmin = false
@ -533,7 +528,6 @@ class ConversationViewModel: ObservableObject {
}
}
}
}
func addParticipantConversationModel(address: Address) {
coreContext.doOnCoreQueue { _ in
@ -549,12 +543,6 @@ class ConversationViewModel: ObservableObject {
}
func getMessages() {
self.getHistorySize()
self.getUnreadMessagesCount()
self.getParticipantConversationModel()
self.computeComposingLabel()
self.getEphemeralTime()
self.mediasToSend.removeAll()
self.messageToReply = nil
@ -563,6 +551,12 @@ class ConversationViewModel: ObservableObject {
self.attachments.removeAll()
coreContext.doOnCoreQueue { _ in
self.getHistorySize()
self.getUnreadMessagesCount()
self.getParticipantConversationModel()
self.computeComposingLabel()
self.getEphemeralTime()
if self.displayedConversation != nil {
let historyEvents = self.displayedConversation!.chatRoom.getHistoryRangeEvents(begin: 0, end: 30)
@ -1930,18 +1924,14 @@ class ConversationViewModel: ObservableObject {
switch attachment.type {
case .image:
content.type = "image"
/*
case .audio:
content.type = "audio"
*/
case .video:
content.type = "video"
/*
case .pdf:
content.type = "application"
case .plainText:
case .text:
content.type = "text"
*/
default:
content.type = "file"
}
@ -1953,27 +1943,23 @@ class ConversationViewModel: ObservableObject {
if message != nil {
let path = FileManager.default.temporaryDirectory.appendingPathComponent((attachment.full.lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""))
let newPath = URL(string: FileUtil.sharedContainerUrl().appendingPathComponent("Library/Images").absoluteString
+ (attachment.full.lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""))
/*
let data = try Data(contentsOf: path)
let decodedData: () = try data.write(to: path)
*/
let path = FileManager.default.temporaryDirectory.appendingPathComponent(attachment.full.lastPathComponent)
if let newPath = URL(string: FileUtil.sharedContainerUrl().appendingPathComponent("Library/Images").absoluteString
+ (attachment.full.lastPathComponent)) {
do {
if FileManager.default.fileExists(atPath: newPath!.path) {
try FileManager.default.removeItem(atPath: newPath!.path)
if FileManager.default.fileExists(atPath: newPath.path) {
try FileManager.default.removeItem(atPath: newPath.path)
}
try FileManager.default.moveItem(atPath: path.path, toPath: newPath!.path)
try FileManager.default.moveItem(atPath: path.path, toPath: newPath.path)
content.filePath = newPath.path
let filePathTmp = newPath?.absoluteString
content.filePath = String(filePathTmp!.dropFirst(7))
message!.addFileContent(content: content)
} catch {
Log.error(error.localizedDescription)
}
}
}
} catch {
}
}
@ -2027,10 +2013,9 @@ class ConversationViewModel: ObservableObject {
if let newChatRoom = core.searchChatRoom(params: nilParams, localAddr: nil, remoteAddr: conversationModel.chatRoom.peerAddress, participants: nil) {
if LinphoneUtils.getChatRoomId(room: newChatRoom) == conversationModel.id {
self.addConversationDelegate(chatRoom: newChatRoom)
let conversation = ConversationModel(chatRoom: newChatRoom)
DispatchQueue.main.async {
withAnimation {
self.displayedConversation = conversation
self.displayedConversation = conversationModel
}
self.getMessages()
}
@ -2596,7 +2581,6 @@ class ConversationViewModel: ObservableObject {
}
func getEphemeralTime() {
coreContext.doOnCoreQueue { _ in
if self.displayedConversation != nil {
let lifetime = self.displayedConversation!.chatRoom.ephemeralLifetime
@ -2624,7 +2608,6 @@ class ConversationViewModel: ObservableObject {
}
}
}
}
func setNewChatRoomSubject() {
if self.displayedConversation != nil && self.conversationInfoPopupText != self.displayedConversation!.subject {

View file

@ -198,6 +198,9 @@ class ConversationsListViewModel: ObservableObject {
let model = ConversationModel(chatRoom: chatRoom)
let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom)
let index = self.conversationsList.firstIndex(where: { $0.id == idTmp })
if index != nil {
self.conversationsList[index!].chatMessageRemoveDelegate()
}
DispatchQueue.main.async {
if index != nil {
self.conversationsList.remove(at: index!)

View file

@ -0,0 +1,111 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import SwiftUI
import UniformTypeIdentifiers
struct FilePicker: UIViewControllerRepresentable {
var onDocumentsPicked: ([URL]) -> Void
func makeCoordinator() -> Coordinator {
Coordinator(onDocumentsPicked: onDocumentsPicked)
}
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.item], asCopy: true)
picker.allowsMultipleSelection = true
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {}
class Coordinator: NSObject, UIDocumentPickerDelegate {
let onDocumentsPicked: ([URL]) -> Void
init(onDocumentsPicked: @escaping ([URL]) -> Void) {
self.onDocumentsPicked = onDocumentsPicked
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
onDocumentsPicked(urls)
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
onDocumentsPicked([])
}
}
static func convertToAttachmentArray(fromResults results: [URL], onComplete: @escaping ([Attachment]?, Error?) -> Void) {
var medias = [Attachment]()
let dispatchGroup = DispatchGroup()
for urlFile in results {
dispatchGroup.enter()
do {
let mimeType = urlFile.mimeType()
if !mimeType.isEmpty {
let type = mimeType.components(separatedBy: "/").first ?? ""
let subtype = mimeType.components(separatedBy: "/").last ?? ""
let dataResult = try Data(contentsOf: urlFile)
var typeTmp: AttachmentType = .other
switch type {
case "image":
typeTmp = urlFile.lastPathComponent.lowercased().hasSuffix("gif") ? .gif : .image
case "audio":
typeTmp = .audio
case "application":
typeTmp = subtype.lowercased() == "pdf" ? .pdf : .other
case "text":
typeTmp = .text
case "video":
typeTmp = .video
default:
typeTmp = .other
}
if typeTmp == .video {
let urlImage = PhotoPicker.saveMedia(name: urlFile.lastPathComponent, data: dataResult, type: .video)
let urlThumbnail = PhotoPicker.getURLThumbnail(name: urlFile.lastPathComponent)
if urlImage != nil {
let attachment = Attachment(id: UUID().uuidString, name: urlImage!.lastPathComponent, thumbnail: urlThumbnail, full: urlImage!, type: .video)
medias.append(attachment)
}
} else {
let urlImage = PhotoPicker.saveMedia(name: urlFile.lastPathComponent, data: dataResult, type: typeTmp)
if urlImage != nil {
let attachment = Attachment(id: UUID().uuidString, name: urlImage!.lastPathComponent, url: urlImage!, type: typeTmp)
medias.append(attachment)
}
}
}
} catch {
}
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
onComplete(medias, nil)
}
}
}

View file

@ -32,6 +32,27 @@ class FileUtil: NSObject {
case unknown
}
public class func formUrlEncode(_ inputString: String) -> String {
// https://www.w3.org/TR/html5/sec-forms.html#application-x-www-form-urlencoded-encoding-algorithm
// Encode tous les caractères sauf *-._A-Za-z0-9, remplace les espaces par '+'
guard !inputString.isEmpty else {
return inputString
}
// Définir les caractères autorisés (non encodés)
var allowed = CharacterSet.alphanumerics
allowed.insert(charactersIn: "*-._")
// Appliquer l'encodage pour les autres caractères
var encoded = inputString.addingPercentEncoding(withAllowedCharacters: allowed) ?? ""
// Remplacer les espaces (déjà encodés en %20) par '+'
encoded = encoded.replacingOccurrences(of: "%20", with: "+")
return encoded
}
public class func bundleFilePath(_ file: NSString) -> String? {
return Bundle.main.path(forResource: file.deletingPathExtension, ofType: file.pathExtension)
}

View file

@ -122,8 +122,7 @@ struct PhotoPicker: UIViewControllerRepresentable {
static func saveMedia(name: String, data: Data, type: AttachmentType) -> URL? {
do {
let path = FileManager.default.temporaryDirectory.appendingPathComponent((name.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""))
let path = FileManager.default.temporaryDirectory.appendingPathComponent(name)
_ = try data.write(to: path)
if type == .video {

View file

@ -241,6 +241,10 @@
"conversation_reply_to_message_title" = "Replying to: ";
"conversation_text_field_hint" = "Say something…";
"conversations_list_empty" = "No conversation for the moment…";
"conversation_take_picture_label" = "Take picture";
"conversation_pick_file_from_gallery_label" = "Open gallery";
"conversation_pick_any_file_label" = "Pick file";
"conversation_file_cant_be_opened_error_toast" = "File can't be opened!";
"debug_logs_copied_to_clipboard_toast" = "Debug logs copied to clipboard";
"Default" = "Default";
"Default mode" = "Default mode";

View file

@ -241,6 +241,10 @@
"conversation_reply_to_message_title" = "En réponse à : ";
"conversation_text_field_hint" = "Dites quelque chose…";
"conversations_list_empty" = "Aucune conversation pour le moment…";
"conversation_take_picture_label" = "Prendre une photo";
"conversation_pick_file_from_gallery_label" = "Ouvrir la gallerie";
"conversation_pick_any_file_label" = "Choisir un fichier";
"conversation_file_cant_be_opened_error_toast" = "Impossible d'ouvrir le fichier!";
"debug_logs_copied_to_clipboard_toast" = "Les journaux ont été ajoutés au presse-papier";
"Default" = "Default";
"Default mode" = "Default mode";