diff --git a/Monal/Classes/AVCallUI.swift b/Monal/Classes/AVCallUI.swift index 9cc9c7a191..bd1ad177f6 100644 --- a/Monal/Classes/AVCallUI.swift +++ b/Monal/Classes/AVCallUI.swift @@ -572,7 +572,7 @@ struct AVCallUI: View { dismissButton: .default(Text("OK")) ) } - .richAlert(isPresented:$showSecurityHelpAlert, title:Text("Call security help").foregroundColor(.black)) { + .richAlert(isPresented:$showSecurityHelpAlert, title:Text("Call security help")) { VStack(alignment: .leading) { HStack { Image(systemName: "xmark.shield.fill") @@ -606,7 +606,7 @@ struct AVCallUI: View { }.font(Font.body.weight(showSecurityHelpAlert == .trusted ? .heavy : .medium)) Text("This means your call is encrypted and the remote party was verified using OMEMO encryption.\nYou manually verified the used OMEMO keys and no Man-In-The-Middle can take place.") Spacer().frame(height: 20) - }.foregroundColor(.black) + } } .onAppear { //force portrait mode and lock ui there diff --git a/Monal/Classes/ActiveChatsViewController.h b/Monal/Classes/ActiveChatsViewController.h index 92bd62652b..0b3eb189e3 100644 --- a/Monal/Classes/ActiveChatsViewController.h +++ b/Monal/Classes/ActiveChatsViewController.h @@ -21,12 +21,13 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong) UITableView* chatListTable; @property (nonatomic, weak) IBOutlet UIBarButtonItem* settingsButton; +@property (weak, nonatomic) IBOutlet UIBarButtonItem* spinnerButton; @property (nonatomic, weak) IBOutlet UIBarButtonItem* composeButton; @property (nonatomic, strong) chatViewController* currentChatViewController; @property (nonatomic, strong) UIActivityIndicatorView* spinner; -(void) showCallContactNotFoundAlert:(NSString*) jid; --(void) callContact:(MLContact*) contact; +-(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender; -(void) callContact:(MLContact*) contact withCallType:(MLCallType) callType; -(void) presentAccountPickerForContacts:(NSArray*) contacts andCallType:(MLCallType) callType; -(void) presentCall:(MLCall*) call; diff --git a/Monal/Classes/ActiveChatsViewController.m b/Monal/Classes/ActiveChatsViewController.m index 07b1b4aac4..db05d4e368 100755 --- a/Monal/Classes/ActiveChatsViewController.m +++ b/Monal/Classes/ActiveChatsViewController.m @@ -70,14 +70,17 @@ -(id) initWithNibName:(NSString*) nibNameOrNil bundle:(NSBundle*) nibBundleOrNil -(void) configureComposeButton { - UIImage* composeImage = [[UIImage systemImageNamed:@"person.2.fill"] imageWithTintColor:UIColor.monalGreen]; - UITapGestureRecognizer* composeTapRecoginzer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showContacts:)]; - self.composeButton.customView = [HelperTools - buttonWithNotificationBadgeForImage:composeImage - hasNotification:[[DataLayer sharedInstance] allContactRequests].count > 0 - withTapHandler:composeTapRecoginzer]; - [self.composeButton.customView setIsAccessibilityElement:YES]; - [self.composeButton.customView setAccessibilityLabel:@"Open contacts list"]; + UIImage* composeImage = [[UIImage systemImageNamed:@"person.2.fill"] imageWithTintColor:UIColor.monalGreen]; + if([[DataLayer sharedInstance] allContactRequests].count > 0) + { + self.composeButton.image = [HelperTools imageWithNotificationBadgeForImage:composeImage]; + } + else + { + self.composeButton.image = composeImage; + } + [self.composeButton setAccessibilityLabel:@"Open contacts list"]; + [self.composeButton setAccessibilityHint:NSLocalizedString(@"Open contact list", @"")]; } -(void) viewDidLoad @@ -120,9 +123,7 @@ -(void) viewDidLoad self.settingsButton.image = [UIImage systemImageNamed:@"gearshape.fill"]; [self configureComposeButton]; - UIBarButtonItem* spinnerButton = [[UIBarButtonItem alloc] initWithCustomView:self.spinner]; - [spinnerButton setIsAccessibilityElement:NO]; - [self.navigationItem setRightBarButtonItems:@[self.composeButton, spinnerButton] animated:NO]; + self.spinnerButton.customView = self.spinner; self.chatListTable.emptyDataSetSource = self; self.chatListTable.emptyDataSetDelegate = self; @@ -269,24 +270,23 @@ -(void) handleContactRemoved:(NSNotification*) notification { MLContact* removedContact = [notification.userInfo objectForKey:@"contact"]; if(removedContact == nil) - { unreachable(); - } - //update red dot dispatch_async(dispatch_get_main_queue(), ^{ + DDLogInfo(@"Contact removed, refreshing active chats..."); + + //update red dot [self configureComposeButton]; - }); - - // ignore all removals that aren't in foreground - if([removedContact isEqualToContact:[MLNotificationManager sharedInstance].currentContact] == NO) - return; - dispatch_async(dispatch_get_main_queue(), ^{ - DDLogInfo(@"Contact removed, closing chat view..."); + // remove contact from activechats table [self refreshDisplay]; - // open placeholder - [self presentChatWithContact:nil]; + + // open placeholder if the removed contact was "in foreground" + if([removedContact isEqualToContact:[MLNotificationManager sharedInstance].currentContact]) + { + DDLogInfo(@"Contact removed, closing chat view..."); + [self presentChatWithContact:nil]; + } }); } @@ -509,7 +509,7 @@ -(void) callContact:(MLContact*) contact withCallType:(MLCallType) callType [self presentCall:[appDelegate.voipProcessor initiateCallWithType:callType toContact:contact]]; } --(void) callContact:(MLContact*) contact +-(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender { MonalAppDelegate* appDelegate = (MonalAppDelegate *)[[UIApplication sharedApplication] delegate]; MLCall* activeCall = [appDelegate.voipProcessor getActiveCallWithContact:contact]; @@ -530,7 +530,15 @@ -(void) callContact:(MLContact*) contact [self dismissViewControllerAnimated:YES completion:nil]; }]]; UIPopoverPresentationController* popPresenter = [alert popoverPresentationController]; - popPresenter.sourceView = self.view; + if(sender != nil) + { + if(@available(iOS 16.0, macCatalyst 16.0, *)) + popPresenter.sourceItem = sender; + else + popPresenter.barButtonItem = sender; + } + else + popPresenter.sourceView = self.view; [self presentViewController:alert animated:YES completion:nil]; } } diff --git a/Monal/Classes/AddContactMenu.swift b/Monal/Classes/AddContactMenu.swift index 4f6d13ad29..fc63decb89 100644 --- a/Monal/Classes/AddContactMenu.swift +++ b/Monal/Classes/AddContactMenu.swift @@ -25,12 +25,14 @@ struct AddContactMenu: View { @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) @State private var invitationResult: [String:AnyObject]? = nil - @ObservedObject private var overlay = LoadingOverlayState() + @StateObject private var overlay = LoadingOverlayState() @State private var showQRCodeScanner = false @State private var success = false @State private var newContact : MLContact? - + + @State private var isEditingJid = false + private let dismissWithNewContact: (MLContact) -> () private let preauthToken: String? @@ -181,12 +183,13 @@ struct AddContactMenu: View { } .pickerStyle(.menu) } - TextField(NSLocalizedString("Contact or Group/Channel Jid", comment: "placeholder when adding jid"), text: $toAdd) + + TextField(NSLocalizedString("Contact or Group/Channel Jid", comment: "placeholder when adding jid"), text: $toAdd, onEditingChanged: { isEditingJid = $0 }) //ios15: .textInputAutocapitalization(.never) .autocapitalization(.none) .autocorrectionDisabled() .keyboardType(.emailAddress) - .addClearButton(text:$toAdd) + .addClearButton(isEditing: isEditingJid, text:$toAdd) .disabled(scannedFingerprints != nil) .foregroundColor(scannedFingerprints != nil ? .secondary : .primary) .onChange(of: toAdd) { _ in @@ -245,14 +248,12 @@ struct AddContactMenu: View { } .richAlert(isPresented: $invitationResult, title:Text("Invitation for \(splitJid["host"]!) created")) { data in VStack { - Text("Direct your buddy to this webpage for instructions on how to setup an xmpp client. You will then automatically be added to their contact list.") - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - Link(data["landing"] as! String, destination:URL(string:data["landing"] as! String)!) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 10) - .padding(.bottom, 10) + Image(uiImage: createQrCode(value: data["landing"] as! String)) + .interpolation(.none) + .resizable() + .scaledToFit() + .aspectRatio(1, contentMode: .fit) + if let expires = data["expires"] as? Date { HStack { if #available(iOS 15, *) { @@ -271,8 +272,12 @@ struct AddContactMenu: View { UIPasteboard.general.setValue(data["landing"] as! String, forPasteboardType:UTType.utf8PlainText.identifier as String) invitationResult = nil }) { - Text("Copy link to clipboard") - .frame(maxWidth: .infinity) + if #available(iOS 16, *) { + ShareLink("Share invitation link", item: URL(string: data["landing"] as! String)!) + } else { + Text("Copy invitation link to clipboard") + .frame(maxWidth: .infinity) + } } Button(action: { invitationResult = nil diff --git a/Monal/Classes/ChannelMemberList.swift b/Monal/Classes/ChannelMemberList.swift new file mode 100644 index 0000000000..a3cbd0444d --- /dev/null +++ b/Monal/Classes/ChannelMemberList.swift @@ -0,0 +1,61 @@ +// +// ChannelMemberList.swift +// Monal +// +// Created by Friedrich Altheide on 17.02.24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +import SwiftUI +import monalxmpp +import OrderedCollections + +struct ChannelMemberList: View { + @State private var channelMembers: OrderedDictionary + @StateObject var channel: ObservableKVOWrapper + private let account: xmpp + + init(mucContact: ObservableKVOWrapper) { + self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp + _channel = StateObject(wrappedValue: mucContact) + + let jidList = Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: mucContact.contactJid, forAccountId: mucContact.accountId)) + var nickSet : OrderedDictionary = OrderedDictionary() + for jidDict in jidList { + if let nick = jidDict["room_nick"] as? String { + nickSet.updateValue((jidDict["affiliation"] as? String) ?? "none", forKey:nick) + } + } + _channelMembers = State(wrappedValue: nickSet) + } + + var body: some View { + List { + Section(header: Text(self.channel.obj.contactDisplayName)) { + ForEach(self.channelMembers.sorted(by: <), id: \.self.key) { + member in + ZStack(alignment: .topLeading) { + HStack(alignment: .center) { + Text(member.key) + Spacer() + if member.value == "owner" { + Text(NSLocalizedString("Owner", comment: "")) + } else if member.value == "admin" { + Text(NSLocalizedString("Admin", comment: "")) + } else { + Text(NSLocalizedString("Member", comment: "")) + } + } + } + } + } + } + .navigationBarTitle("Channel Members", displayMode: .inline) + } +} + +struct ChannelMemberList_Previews: PreviewProvider { + static var previews: some View { + ChannelMemberList(mucContact:ObservableKVOWrapper(MLContact.makeDummyContact(3))); + } +} diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index a8aea957b3..1f3af26879 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -12,6 +12,8 @@ import monalxmpp struct ContactDetails: View { var delegate: SheetDismisserProtocol + private var account: xmpp + private var isGroupModerator = false @StateObject var contact: ObservableKVOWrapper @State private var showingBlockContactConfirmation = false @State private var showingCannotBlockAlert = false @@ -21,6 +23,18 @@ struct ContactDetails: View { @State private var showingResetOmemoSessionConfirmation = false @State private var showingCannotEncryptAlert = false @State private var showingShouldDisableEncryptionAlert = false + @State private var isEditingNickname = false + + init(delegate: SheetDismisserProtocol, contact: ObservableKVOWrapper) { + self.delegate = delegate + _contact = StateObject(wrappedValue: contact) + self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! + + if contact.isGroup { + let ownRole = DataLayer.sharedInstance().getOwnRole(inGroupOrChannel: contact.obj) ?? "none" + self.isGroupModerator = (ownRole == "moderator") + } + } var body: some View { Form { @@ -30,7 +44,7 @@ struct ContactDetails: View { // info/nondestructive buttons Section { - Button(action: { + Button { if(contact.isGroup) { if(!contact.isMuted && !contact.isMentionOnly) { contact.obj.toggleMentionOnly(true) @@ -44,45 +58,52 @@ struct ContactDetails: View { } else { contact.obj.toggleMute(!contact.isMuted) } - }) { - HStack { - if(contact.isMuted) { + } label: { + if(contact.isMuted) { + Label { + contact.isGroup ? Text("Notifications disabled") : Text("Contact is muted") + } icon: { Image(systemName: "bell.slash.fill") .foregroundColor(.red) - contact.isGroup ? Text("Notifications disabled") : Text("Contact is muted") - } else if(contact.isGroup && contact.isMentionOnly) { - Image(systemName: "bell.badge") - .foregroundColor(.accentColor) + } + } else if(contact.isGroup && contact.isMentionOnly) { + Label { Text("Notify only when mentioned") - } else { + } icon: { + Image(systemName: "bell.badge") + } + } else { + Label { + contact.isGroup ? Text("Notify on all messages") : Text("Contact is not muted") + } icon: { Image(systemName: "bell.fill") .foregroundColor(.green) - contact.isGroup ? Text("Notify on all messages") : Text("Contact is not muted") } } } - //.buttonStyle(BorderlessButtonStyle()) #if !DISABLE_OMEMO if((!contact.isGroup || (contact.isGroup && contact.mucType == "group")) && !HelperTools.isContactBlacklisted(forEncryption:contact.obj)) { - Button(action: { + Button { if(contact.isEncrypted) { showingShouldDisableEncryptionAlert = true } else { showingCannotEncryptAlert = !contact.obj.toggleEncryption(!contact.isEncrypted) } - }) { - HStack { - if contact.isEncrypted { + } label: { + if contact.isEncrypted { + Label { + Text("Messages are encrypted") + } icon: { Image(systemName: "lock.fill") .foregroundColor(.green) - Text("Messages are encrypted") - .foregroundColor(.accentColor) - } else { + } + } else { + Label { + Text("Messages are NOT encrypted") + } icon: { Image(systemName: "lock.open.fill") .foregroundColor(.red) - Text("Messages are NOT encrypted") - .foregroundColor(.accentColor) } } } @@ -112,19 +133,30 @@ struct ContactDetails: View { #endif if(!contact.isGroup && !contact.isSelfChat) { - TextField(NSLocalizedString("Rename Contact", comment: "placeholder text in contact details"), text: $contact.nickNameView) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .addClearButton(text:$contact.nickNameView) + TextField(NSLocalizedString("Rename Contact", comment: "placeholder text in contact details"), text: $contact.nickNameView, onEditingChanged: { + isEditingNickname = $0 + }) + .accessibilityLabel("Nickname") + .addClearButton(isEditing: isEditingNickname, text: $contact.nickNameView) } - Button(contact.isPinned ? "Unpin Chat" : "Pin Chat") { - contact.obj.togglePinnedChat(!contact.isPinned); - } + Toggle("Pin Chat", isOn: Binding(get: { + contact.isPinned + }, set: { + contact.obj.togglePinnedChat($0) + })) +// Button(contact.isPinned ? "Unpin Chat" : "Pin Chat") { +// contact.obj.togglePinnedChat(!contact.isPinned); +// } if(contact.obj.isGroup && contact.obj.mucType == "group") { - NavigationLink(destination: LazyClosureView(MemberList(mucContact: contact))) { + NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) { Text("Group Members") } + } else if(contact.obj.isGroup && contact.obj.mucType == "channel") { + NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) { + Text("Channel Members") + } } #if !DISABLE_OMEMO if(!HelperTools.isContactBlacklisted(forEncryption:contact.obj)) { @@ -315,9 +347,7 @@ struct ContactDetails: View { .destructive( Text("Yes"), action: { - if let account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId) { - account.omemo.clearAllSessions(forJid:contact.contactJid); - } + self.account.omemo.clearAllSessions(forJid:contact.contactJid); } ) ] @@ -328,7 +358,21 @@ struct ContactDetails: View { #endif } .frame(maxWidth: .infinity, maxHeight: .infinity) - .navigationBarTitle(contact.contactDisplayName as String, displayMode: .inline) + .navigationBarTitle(contact.contactDisplayName as String, displayMode:.inline) + .applyClosure { view in + if contact.isGroup && isGroupModerator && self.account.accountState.rawValue >= xmppState.stateBound.rawValue { + view.toolbar { + ToolbarItem(placement:.navigationBarTrailing) { + let ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? "none" + NavigationLink(destination:LazyClosureView(GroupDetailsEdit(contact:contact, ownAffiliation:ownAffiliation))) { + Text("Edit") + } + } + } + } else { + view + } + } } } diff --git a/Monal/Classes/ContactDetailsHeader.swift b/Monal/Classes/ContactDetailsHeader.swift index c657df9b09..e8676bd51d 100644 --- a/Monal/Classes/ContactDetailsHeader.swift +++ b/Monal/Classes/ContactDetailsHeader.swift @@ -17,40 +17,37 @@ struct ContactDetailsHeader: View { @State private var navigationAction: String? var body: some View { - VStack(alignment: .center) { - HStack { - Spacer() - Image(uiImage: contact.avatar) - .resizable() - .frame(width: 150, height: 150, alignment: .center) - .scaledToFit() - .shadow(radius: 7) - Spacer() - } + VStack(spacing: 20) { + Image(uiImage: contact.avatar) + .resizable() + .scaledToFit() + .accessibilityLabel("Avatar") + .frame(width: 150, height: 150, alignment: .center) + .shadow(radius: 7) + - Spacer() - .frame(height: 20) - HStack { - Text(contact.contactJid as String) - //for ios >= 15.0 - //.textSelection(.enabled) - Spacer().frame(width: 10) - Button(action: { - UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String) - }) { + Button { + UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String) + UIAccessibility.post(notification: .announcement, argument: "JID Copied") + } label: { + HStack { + Text(contact.contactJid as String) + Image(systemName: "doc.on.doc") .foregroundColor(.primary) + .accessibilityHidden(true) } - .buttonStyle(BorderlessButtonStyle()) + .accessibilityHint("Copies JID") } + .buttonStyle(.borderless) + + //only show account jid if more than one is configured if MLXMPPManager.sharedInstance().connectedXMPP.count > 1 && !contact.isSelfChat { Text("Account: \(MLXMPPManager.sharedInstance().getConnectedAccount(forID:contact.accountId)!.connectionProperties.identity.jid)") } if !contact.isSelfChat && !contact.isGroup { - Spacer() - .frame(height: 20) if let lastInteractionTime = contact.lastInteractionTime as Date? { if lastInteractionTime.timeIntervalSince1970 > 0 { Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), @@ -64,27 +61,29 @@ struct ContactDetailsHeader: View { } if(!contact.isGroup && (contact.statusMessage as String).count > 0) { - Spacer() - .frame(height: 20) - Text("Status message:") - Text(contact.statusMessage as String) - .fixedSize(horizontal: false, vertical: true) + VStack { + Text("Status message:") + Text(contact.statusMessage as String) + .fixedSize(horizontal: false, vertical: true) + } } if(contact.isGroup && (contact.groupSubject as String).count > 0) { - Spacer() - .frame(height: 20) - if(contact.obj.mucType == "group") { - Text("Group subject:") - } else { - Text("Channel subject:") + VStack { + if(contact.obj.mucType == "group") { + Text("Group subject:") + } else { + Text("Channel subject:") + } + + Text(contact.groupSubject as String) + .fixedSize(horizontal: false, vertical: true) } - Text(contact.groupSubject as String) - .fixedSize(horizontal: false, vertical: true) } } .foregroundColor(.primary) .padding([.top, .bottom]) + .frame(maxWidth: .infinity) } } diff --git a/Monal/Classes/ContactEntry.swift b/Monal/Classes/ContactEntry.swift new file mode 100644 index 0000000000..9ff3eac03f --- /dev/null +++ b/Monal/Classes/ContactEntry.swift @@ -0,0 +1,43 @@ +// +// ContactEntry.swift +// Monal +// +// Created by Friedrich Altheide on 28.11.23. +// Copyright © 2023 monal-im.org. All rights reserved. +// + +import SwiftUI + +struct ContactEntry: View { + let contact : ObservableKVOWrapper + + var body:some View { + ZStack(alignment: .topLeading) { + HStack(alignment: .center) { + Image(uiImage: contact.avatar) + .resizable() + .frame(width: 40, height: 40, alignment: .center) + VStack(alignment: .leading) { + Text(contact.contactDisplayName as String) + Text(contact.contactJid as String).font(.footnote).opacity(0.6) + } + } + } + } +} + +#Preview { + ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(0))) +} + +#Preview { + ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(1))) +} + +#Preview { + ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(2))) +} + +#Preview { + ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(3))) +} diff --git a/Monal/Classes/ContactPicker.swift b/Monal/Classes/ContactPicker.swift index f7877f8cf6..a58279fde0 100644 --- a/Monal/Classes/ContactPicker.swift +++ b/Monal/Classes/ContactPicker.swift @@ -10,32 +10,18 @@ import SwiftUI import monalxmpp import OrderedCollections -struct ContactEntry: View { // TODO move - let contact : MLContact - - var body:some View { - ZStack(alignment: .topLeading) { - HStack(alignment: .center) { - Image(uiImage: contact.avatar) - .resizable() - .frame(width: 40, height: 40, alignment: .center) - VStack(alignment: .leading) { - Text(contact.contactDisplayName as String) - Text(contact.contactJid as String).font(.footnote).opacity(0.6) - } - } - } - } -} - struct ContactPickerEntry: View { - let contact : MLContact + let contact : ObservableKVOWrapper let isPicked: Bool + let isExistingMember: Bool var body:some View { ZStack(alignment: .topLeading) { HStack(alignment: .center) { - if(isPicked) { + if(isExistingMember) { + Image(systemName: "checkmark.circle") + .foregroundColor(.gray) + } else if(isPicked) { Image(systemName: "checkmark.circle") .foregroundColor(.blue) } else { @@ -56,17 +42,42 @@ struct ContactPickerEntry: View { struct ContactPicker: View { @Environment(\.presentationMode) private var presentationMode - let contacts : [MLContact] - @Binding var selectedContacts : OrderedSet // already selected when going into the view - @State var searchFieldInput = "" + @State var contacts: OrderedSet> + let account: xmpp + @Binding var selectedContacts: OrderedSet> + let existingMembers: OrderedSet> + @State var searchText = "" + + @State var isEditingSearchInput: Bool = false - func matchesSearch(contact : MLContact) -> Bool { - // TODO better lookup - if searchFieldInput.isEmpty == true { - return true + init(account: xmpp, selectedContacts: Binding>>) { + self.init(account: account, selectedContacts: selectedContacts, existingMembers: OrderedSet()) + } + + init(account: xmpp, selectedContacts: Binding>>, existingMembers: OrderedSet>) { + self.account = account + self._selectedContacts = selectedContacts + self.existingMembers = existingMembers + + var contactsTmp: OrderedSet> = OrderedSet() + for contact in DataLayer.sharedInstance().possibleGroupMembers(forAccount: account.accountNo) { + contactsTmp.append(ObservableKVOWrapper(contact)) + } + _contacts = State(wrappedValue: contactsTmp) + } + + private var searchResults : OrderedSet> { + if searchText.isEmpty { + return self.contacts } else { - return contact.contactDisplayName.lowercased().contains(searchFieldInput.lowercased()) || - contact.contactJid.contains(searchFieldInput.lowercased()) + var filteredContacts: OrderedSet> = OrderedSet() + for contact in self.contacts { + if (contact.contactDisplayName as String).lowercased().contains(searchText.lowercased()) || + (contact.contactJid as String).contains(searchText.lowercased()) { + filteredContacts.append(contact) + } + } + return filteredContacts } } @@ -76,21 +87,27 @@ struct ContactPicker: View { .navigationTitle("Contact Lists") } else { List { - Section { - TextField(NSLocalizedString("Search contacts", comment: "placeholder in contact picker"), text: $searchFieldInput) - } - ForEach(Array(contacts.enumerated()), id: \.element) { idx, contact in - if matchesSearch(contact: contact) { - let contactIsSelected = self.selectedContacts.contains(contact); - ContactPickerEntry(contact: contact, isPicked: contactIsSelected) - .onTapGesture(perform: { + ForEach(searchResults, id: \.self.obj) { contact in + let contactIsSelected = self.selectedContacts.contains(contact); + let contactIsAlreadyMember = self.existingMembers.contains(contact); + ContactPickerEntry(contact: contact, isPicked: contactIsSelected, isExistingMember: contactIsAlreadyMember) + .onTapGesture(perform: { + // only allow changes to members that are not already part of the group + if(!contactIsAlreadyMember) { if(contactIsSelected) { self.selectedContacts.remove(contact) } else { self.selectedContacts.append(contact) } - }) - } + } + }) + } + } + .applyClosure { view in + if #available(iOS 15.0, *) { + view.searchable(text: $searchText, placement: .automatic, prompt: nil) + } else { + view } } .listStyle(.inset) @@ -105,9 +122,4 @@ struct ContactPicker: View { } } } - - init(selectedContacts: Binding>) { - self._selectedContacts = selectedContacts - self.contacts = DataLayer.sharedInstance().contactList() as! [MLContact] - } } diff --git a/Monal/Classes/ContactResources.swift b/Monal/Classes/ContactResources.swift index 50d500edd1..cc691e7c8f 100644 --- a/Monal/Classes/ContactResources.swift +++ b/Monal/Classes/ContactResources.swift @@ -67,7 +67,7 @@ struct ContactResources: View { } } } - .richAlert(isPresented:$showCaps, title:Text("XMPP Capabilities").foregroundColor(.black)) { resource in + .richAlert(isPresented:$showCaps, title:Text("XMPP Capabilities")) { resource in VStack(alignment: .leading) { Text("The resource '\(resource)' has the following capabilities:") .font(Font.body.weight(.semibold)) @@ -77,7 +77,7 @@ struct ContactResources: View { let capsVer = DataLayer.sharedInstance().getVerForUser(self.contact.contactJid, andResource:resource, onAccountNo:self.contact.accountId) Text("Caps hash: \(String(describing:capsVer))") Divider() - if let capsSet = DataLayer.sharedInstance().getCapsforVer(capsVer) as? Set { + if let capsSet = DataLayer.sharedInstance().getCapsforVer(capsVer, onAccountNo:contact.obj.accountId) as? Set { let caps = Array(capsSet) VStack(alignment: .leading) { ForEach(caps, id: \.self) { cap in @@ -90,7 +90,7 @@ struct ContactResources: View { } } } - }.foregroundColor(.black) + } } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalXmppUserSoftWareVersionRefresh")).receive(on: RunLoop.main)) { notification in if let xmppAccount = notification.object as? xmpp, let softwareInfo = notification.userInfo?["versionInfo"] as? MLContactSoftwareVersionInfo { diff --git a/Monal/Classes/ContactsViewController.m b/Monal/Classes/ContactsViewController.m index c582966a14..95339d6c64 100644 --- a/Monal/Classes/ContactsViewController.m +++ b/Monal/Classes/ContactsViewController.m @@ -108,14 +108,10 @@ -(void) viewDidLoad addContact.image = [UIImage systemImageNamed:@"person.fill.badge.plus"]; [addContact setAction:@selector(openAddContacts:)]; -#ifdef IS_ALPHA UIBarButtonItem* createGroup = [[UIBarButtonItem alloc] init]; createGroup.image = [UIImage systemImageNamed:@"person.3.fill"]; [createGroup setAction:@selector(openCreateGroup:)]; self.navigationItem.rightBarButtonItems = [[NSArray alloc] initWithObjects:addContact, [[UIBarButtonItem alloc] init], createGroup, nil]; -#else - self.navigationItem.rightBarButtonItems = [[NSArray alloc] initWithObjects:addContact, [UIBarButtonItem new], nil]; -#endif [self configureContactRequestsImage]; @@ -231,7 +227,7 @@ -(void) performSegueWithIdentifier:(NSString*) identifier sender:(id) sender -(void) loadContactsWithFilter:(NSString*) filter { - NSMutableArray* contacts; + NSArray* contacts; if(filter && [filter length] > 0) self.contacts = [[DataLayer sharedInstance] searchContactsWithString:filter]; else diff --git a/Monal/Classes/CreateGroupMenu.swift b/Monal/Classes/CreateGroupMenu.swift index 881d61c1ad..d88db79a82 100644 --- a/Monal/Classes/CreateGroupMenu.swift +++ b/Monal/Classes/CreateGroupMenu.swift @@ -13,54 +13,38 @@ import monalxmpp import OrderedCollections struct CreateGroupMenu: View { - var delegate: SheetDismisserProtocol + private var appDelegate: MonalAppDelegate @State private var connectedAccounts: [xmpp] - @State private var selectedAccount: Int + @State private var selectedAccount: xmpp? @State private var groupName: String = "" @State private var showAlert = false // note: dismissLabel is not accessed but defined at the .alert() section @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) - @State private var selectedContacts : OrderedSet = [] + @State private var selectedContacts : OrderedSet> = [] + + @State private var isEditingGroupName = false - @ObservedObject private var overlay = LoadingOverlayState() + @StateObject private var overlay = LoadingOverlayState() + + private var delegate: SheetDismisserProtocol - @State private var success = false - - private let dismissWithNewGroup: (MLContact) -> () - - init(delegate: SheetDismisserProtocol, dismissWithNewGroup: @escaping (MLContact) -> (), prefillJid: String = "", preauthToken:String? = nil) { - // FIXME + init(delegate: SheetDismisserProtocol) { + self.appDelegate = UIApplication.shared.delegate as! MonalAppDelegate self.delegate = delegate - self.dismissWithNewGroup = dismissWithNewGroup - self.groupName = prefillJid - // self.preauthToken = preauthToken let connectedAccounts = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp] self.connectedAccounts = connectedAccounts - self.selectedAccount = connectedAccounts.first != nil ? 0 : -1; + _selectedAccount = State(wrappedValue: connectedAccounts.first) } - // FIXME duplicate code from WelcomeLogIn.swift, maybe move to SwiftuiHelpers - private func errorAlert(title: Text, message: Text = Text("")) { alertPrompt.title = title alertPrompt.message = message showAlert = true } - private func successAlert(title: Text, message: Text) { - alertPrompt.title = title - alertPrompt.message = message - self.success = true // < dismiss entire view on close - showAlert = true - } - - private var buttonColor: Color { - return Color(UIColor.systemBlue) - } - var body: some View { Form { if(connectedAccounts.isEmpty) { @@ -70,48 +54,71 @@ struct CreateGroupMenu: View { else { Section() { - if(connectedAccounts.count > 1) { - Picker("Use account", selection: $selectedAccount) { - ForEach(Array(self.connectedAccounts.enumerated()), id: \.element) { idx, account in - Text(account.connectionProperties.identity.jid).tag(idx) - } + Picker("Use account", selection: $selectedAccount) { + ForEach(Array(self.connectedAccounts.enumerated()), id: \.element) { idx, account in + Text(account.connectionProperties.identity.jid).tag(account as xmpp?) } - .pickerStyle(.menu) } - TextField(NSLocalizedString("Group Name (optional)", comment: "placeholder when creating new group"), text: $groupName) + .pickerStyle(.menu) + + TextField(NSLocalizedString("Group Name (optional)", comment: "placeholder when creating new group"), text: $groupName, onEditingChanged: { isEditingGroupName = $0 }) .autocorrectionDisabled() .autocapitalization(.none) - .addClearButton(text:$groupName) + .addClearButton(isEditing: isEditingGroupName, text:$groupName) - NavigationLink(destination: LazyClosureView(ContactPicker(selectedContacts: $selectedContacts)), label: { - Text("Group Members") + NavigationLink(destination: LazyClosureView(ContactPicker(account: self.selectedAccount!, selectedContacts: $selectedContacts)), label: { + Text("Change Group Members") }) + Button(action: { + showLoadingOverlay(overlay, headline: NSLocalizedString("Creating Group", comment: "")) + let roomJid = self.selectedAccount!.mucProcessor.createGroup(nil) + if(roomJid == nil) { + let groupContact = MLContact.createContact(fromJid: roomJid!, andAccountNo: self.selectedAccount!.accountNo) + hideLoadingOverlay(overlay) + self.delegate.dismissWithoutAnimation() + if let activeChats = self.appDelegate.activeChats { + activeChats.presentChat(with:groupContact) + } + } else { + self.selectedAccount!.mucProcessor.addUIHandler({data in + let success : Bool = (data as! NSDictionary)["success"] as! Bool; + if(success) { + self.selectedAccount!.mucProcessor.changeName(ofMuc: roomJid!, to: self.groupName) + for user in self.selectedContacts { + self.selectedAccount!.mucProcessor.setAffiliation("member", ofUser: user.contactJid, inMuc: roomJid!) + self.selectedAccount!.mucProcessor.inviteUser(user.contactJid, inMuc: roomJid!) + } + let groupContact = MLContact.createContact(fromJid: roomJid!, andAccountNo: self.selectedAccount!.accountNo) + hideLoadingOverlay(overlay) + self.delegate.dismissWithoutAnimation() + if let activeChats = self.appDelegate.activeChats { + activeChats.presentChat(with:groupContact) + } + } else { + hideLoadingOverlay(overlay) + errorAlert(title: Text("Error creating group!")) + } + }, forMuc: roomJid!) + } + }, label: { + Text("Create new group") + }) } if(self.selectedContacts.count > 0) { Section(header: Text("Selected Group Members")) { - ForEach(self.selectedContacts, id: \.contactJid) { contact in + ForEach(self.selectedContacts, id: \.obj.contactJid) { contact in ContactEntry(contact: contact) } .onDelete(perform: { indexSet in self.selectedContacts.remove(at: indexSet.first!) }) } - Section { - Button(action: { - - }, label: { - Text("Create new group") - }) - } } } } .alert(isPresented: $showAlert) { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: { showAlert = false - if self.success == true { - // TODO dismissWithNewGroup - } })) } .addLoadingOverlay(overlay) @@ -123,7 +130,6 @@ struct CreateGroupMenu: View { struct CreateGroupMenu_Previews: PreviewProvider { static var delegate = SheetDismisserProtocol() static var previews: some View { - CreateGroupMenu(delegate: delegate, dismissWithNewGroup: { c in - }) + CreateGroupMenu(delegate: delegate) } } diff --git a/Monal/Classes/DataLayer.h b/Monal/Classes/DataLayer.h index baf68ad8d8..e223913230 100644 --- a/Monal/Classes/DataLayer.h +++ b/Monal/Classes/DataLayer.h @@ -60,8 +60,9 @@ extern NSString* const kMessageTypeFiletransfer; -(NSMutableArray*) searchContactsWithString:(NSString*) search; --(NSMutableArray*) contactList; --(NSMutableArray*) contactListWithJid:(NSString*) jid; +-(NSArray*) contactList; +-(NSArray*) contactListWithJid:(NSString*) jid; +-(NSArray*) possibleGroupMembersForAccount:(NSNumber*) accountNo; -(NSArray*) resourcesForContact:(MLContact* _Nonnull)contact ; -(MLContactSoftwareVersionInfo* _Nullable) getSoftwareVersionInfoForContact:(NSString*)contact resource:(NSString*)resource andAccount:(NSNumber*)account; -(void) setSoftwareVersionInfoForContact:(NSString*)contact @@ -75,8 +76,8 @@ extern NSString* const kMessageTypeFiletransfer; -(BOOL) checkCap:(NSString*) cap forUser:(NSString*) user andResource:(NSString*) resource onAccountNo:(NSNumber*) accountNo; -(NSString*) getVerForUser:(NSString*) user andResource:(NSString*) resource onAccountNo:(NSNumber*) accountNo; -(void) setVer:(NSString*) ver forUser:(NSString*) user andResource:(NSString*) resource onAccountNo:(NSNumber*) accountNo; --(NSSet* _Nullable) getCapsforVer:(NSString*) ver; --(void) setCaps:(NSSet*) caps forVer:(NSString*) ver; +-(NSSet* _Nullable) getCapsforVer:(NSString*) ver onAccountNo:(NSNumber*) accountNo; +-(void) setCaps:(NSSet*) caps forVer:(NSString*) ver onAccountNo:(NSNumber*) accountNo; #pragma mark presence functions -(void) setResourceOnline:(XMPPPresence*) presenceObj forAccount:(NSNumber*) accountNo; @@ -114,6 +115,8 @@ extern NSString* const kMessageTypeFiletransfer; -(void) removeParticipant:(NSDictionary*) participant fromMuc:(NSString*) room forAccountId:(NSNumber*) accountNo; -(NSDictionary* _Nullable) getParticipantForNick:(NSString*) nick inRoom:(NSString*) room forAccountId:(NSNumber*) accountNo; -(NSArray*>*) getMembersAndParticipantsOfMuc:(NSString*) room forAccountId:(NSNumber*) accountNo; +-(NSString* _Nullable) getOwnAffiliationInGroupOrChannel:(MLContact*) contact; +-(NSString* _Nullable) getOwnRoleInGroupOrChannel:(MLContact*) contact; -(void) addMucFavorite:(NSString*) room forAccountId:(NSNumber*) accountNo andMucNick:(NSString* _Nullable) mucNick; -(NSString*) lastStanzaIdForMuc:(NSString* _Nonnull) room andAccount:(NSNumber* _Nonnull) accountNo; -(void) setLastStanzaId:(NSString*) lastStanzaId forMuc:(NSString* _Nonnull) room andAccount:(NSNumber* _Nonnull) accountNo; @@ -125,7 +128,7 @@ extern NSString* const kMessageTypeFiletransfer; -(BOOL) updateMucSubject:(NSString*) subject forAccount:(NSNumber*) accountNo andRoom:(NSString*) room; -(NSString*) mucSubjectforAccount:(NSNumber*) accountNo andRoom:(NSString*) room; --(NSMutableArray*) listMucsForAccount:(NSNumber*) accountNo; +-(NSSet*) listMucsForAccount:(NSNumber*) accountNo; -(BOOL) deleteMuc:(NSString*) room forAccountId:(NSNumber*) accountNo; -(void) updateMucTypeTo:(NSString*) type forRoom:(NSString*) room andAccount:(NSNumber*) accountNo; @@ -208,7 +211,7 @@ extern NSString* const kMessageTypeFiletransfer; -(void) deleteMessageHistory:(NSNumber *) messageNo; -(void) deleteMessageHistoryLocally:(NSNumber*) messageNo; -(void) updateMessageHistory:(NSNumber*) messageNo withText:(NSString*) newText; --(NSNumber* _Nullable) getHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from andAccount:(NSNumber*) accountNo; +-(NSNumber* _Nullable) getHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from actualFrom:(NSString* _Nullable) actualFrom participantJid:(NSString* _Nullable) participantJid andAccount:(NSNumber*) accountNo; -(NSDate* _Nullable) returnTimestampForQuote:(NSNumber*) historyID; -(BOOL) checkLMCEligible:(NSNumber*) historyID encrypted:(BOOL) encrypted historyBaseID:(NSNumber* _Nullable) historyBaseID; diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index 5ef936ab45..d8fea8450a 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -478,7 +478,7 @@ -(NSDictionary* _Nullable) contactDictionaryForUsername:(NSString*) username for { NSMutableDictionary* contact = [results[0] mutableCopy]; //correctly extract NSDate object or 1970, if last interaction is zero - contact[@"lastInteraction"] = [self lastInteractionOfJid:username forAccountNo:accountNo]; + contact[@"lastInteraction"] = nilWrapper([self lastInteractionOfJid:username forAccountNo:accountNo]); //if we have this muc in our favorites table, this muc is "subscribed" if([self.db executeScalar:@"SELECT room FROM muc_favorites WHERE room=? AND account_id=?;" andArguments:@[username, accountNo]] != nil) contact[@"subscription"] = @"both"; @@ -501,12 +501,24 @@ -(NSDictionary* _Nullable) contactDictionaryForUsername:(NSString*) username for }]; } --(NSMutableArray*) contactList +-(NSArray*) contactList { return [self contactListWithJid:@""]; } --(NSMutableArray*) contactListWithJid:(NSString*) jid +-(NSArray*) possibleGroupMembersForAccount:(NSNumber*) accountNo +{ + return [self.db idReadTransaction:^{ + //list all contacts and group chats + NSString* query = @"SELECT B.buddy_name, B.account_id, IFNULL(IFNULL(NULLIF(B.nick_name, ''), NULLIF(B.full_name, '')), B.buddy_name) FROM buddylist as B INNER JOIN account AS A ON A.account_id=B.account_id WHERE B.account_id=? AND B.muc=0 AND B.buddy_name != (A.username || '@' || A.domain)"; + NSMutableArray* toReturn = [NSMutableArray new]; + for(NSDictionary* dic in [self.db executeReader:query andArguments:@[accountNo]]) + [toReturn addObject:[MLContact createContactFromJid:dic[@"buddy_name"] andAccountNo:dic[@"account_id"]]]; + return toReturn; + }]; +} + +-(NSArray*) contactListWithJid:(NSString*) jid { return [self.db idReadTransaction:^{ //list all contacts and group chats @@ -523,8 +535,8 @@ -(NSDictionary* _Nullable) contactDictionaryForUsername:(NSString*) username for -(BOOL) checkCap:(NSString*) cap forUser:(NSString*) user onAccountNo:(NSNumber*) accountNo { return [self.db boolReadTransaction:^{ - NSString* query = @"SELECT COUNT(*) FROM buddylist AS a INNER JOIN buddy_resources AS b ON a.buddy_id=b.buddy_id INNER JOIN ver_info AS c ON b.ver=c.ver WHERE buddy_name=? AND account_id=? AND cap=?;"; - NSArray *params = @[user, accountNo, cap]; + NSString* query = @"SELECT COUNT(*) FROM buddylist AS a INNER JOIN buddy_resources AS b ON a.buddy_id=b.buddy_id INNER JOIN ver_info AS c ON b.ver=c.ver WHERE a.buddy_name=? AND a.account_id=? AND c.cap=? AND c.account_id=?;"; + NSArray *params = @[user, accountNo, cap, accountNo]; NSNumber* count = (NSNumber*) [self.db executeScalar:query andArguments:params]; return (BOOL)([count integerValue]>0); }]; @@ -533,8 +545,8 @@ -(BOOL) checkCap:(NSString*) cap forUser:(NSString*) user onAccountNo:(NSNumber* -(BOOL) checkCap:(NSString*) cap forUser:(NSString*) user andResource:(NSString*) resource onAccountNo:(NSNumber*) accountNo { return [self.db boolReadTransaction:^{ - NSString* query = @"SELECT COUNT(*) FROM buddylist AS a INNER JOIN buddy_resources AS b ON a.buddy_id=b.buddy_id INNER JOIN ver_info AS c ON b.ver=c.ver WHERE buddy_name=? AND resource=? AND account_id=? AND cap=?;"; - NSNumber* count = (NSNumber*) [self.db executeScalar:query andArguments:@[user, resource, accountNo, cap]]; + NSString* query = @"SELECT COUNT(*) FROM buddylist AS a INNER JOIN buddy_resources AS b ON a.buddy_id=b.buddy_id INNER JOIN ver_info AS c ON b.ver=c.ver WHERE a.buddy_name=? AND b.resource=? AND a.account_id=? AND c.cap=? AND c.account_id=?;"; + NSNumber* count = (NSNumber*) [self.db executeScalar:query andArguments:@[user, resource, accountNo, cap, accountNo]]; return (BOOL)([count integerValue]>0); }]; } @@ -559,70 +571,37 @@ -(void) setVer:(NSString*) ver forUser:(NSString*) user andResource:(NSString*) [self.db executeNonQuery:query andArguments:params]; //update timestamp for this ver string to make it not timeout (old ver strings and features are removed from feature cache after 28 days) - NSString* query2 = @"INSERT INTO ver_timestamp (ver, timestamp) VALUES (?, ?) ON CONFLICT(ver) DO UPDATE SET timestamp=?;"; - NSArray * params2 = @[ver, timestamp, timestamp]; + NSString* query2 = @"UPDATE ver_info SET timestamp=? WHERE ver=? AND account_id=?;"; + NSArray * params2 = @[timestamp, ver, accountNo]; [self.db executeNonQuery:query2 andArguments:params2]; }]; } --(NSSet*) getCapsforVer:(NSString*) ver +-(NSSet*) getCapsforVer:(NSString*) ver onAccountNo:(NSNumber*) accountNo { return [self.db idReadTransaction:^{ - NSString* query = @"select cap from ver_info where ver=?"; - NSArray * params = @[ver]; - NSArray* resultArray = [self.db executeReader:query andArguments:params]; + NSSet* result = [NSSet setWithArray:[self.db executeScalarReader:@"SELECT cap FROM ver_info WHERE ver=? AND account_id=?;" andArguments:@[ver, accountNo]]]; - if(resultArray != nil) - { - DDLogVerbose(@"caps count: %lu", (unsigned long)[resultArray count]); - if([resultArray count] == 0) - return (NSSet*)nil; - NSMutableSet* retval = [NSMutableSet new]; - for(NSDictionary* row in resultArray) - [retval addObject:row[@"cap"]]; - return (NSSet*)retval; - } - else - { - DDLogError(@"caps list is empty"); + DDLogVerbose(@"caps count: %lu", (unsigned long)[result count]); + if([result count] == 0) return (NSSet*)nil; - } + return result; }]; } --(void) setCaps:(NSSet*) caps forVer:(NSString*) ver +-(void) setCaps:(NSSet*) caps forVer:(NSString*) ver onAccountNo:(NSNumber*) accountNo { NSNumber* timestamp = [HelperTools currentTimestampInSeconds]; [self.db voidWriteTransaction:^{ //remove old caps for this ver - NSString* query0 = @"DELETE FROM ver_info WHERE ver=?;"; - NSArray * params0 = @[ver]; - [self.db executeNonQuery:query0 andArguments:params0]; + [self.db executeNonQuery:@"DELETE FROM ver_info WHERE ver=? AND account_id=?;" andArguments:@[ver, accountNo]]; //insert new caps for(NSString* feature in caps) - { - NSString* query1 = @"INSERT INTO ver_info (ver, cap) VALUES (?, ?);"; - NSArray * params1 = @[ver, feature]; - [self.db executeNonQuery:query1 andArguments:params1]; - } - - //update timestamp for this ver string - NSString* query2 = @"INSERT INTO ver_timestamp (ver, timestamp) VALUES (?, ?) ON CONFLICT(ver) DO UPDATE SET timestamp=?;"; - NSArray * params2 = @[ver, timestamp, timestamp]; - [self.db executeNonQuery:query2 andArguments:params2]; - - //cleanup old entries - NSString* query3 = @"SELECT ver FROM ver_timestamp WHERE timestamp" (e.g. lastInteraction > 0) - NSNumber* idle = [self.db executeScalar:@"SELECT R.lastInteraction FROM buddy_resources AS R INNER JOIN buddylist AS B ON R.buddy_id=B.buddy_id INNER JOIN ver_info AS V ON R.ver=V.ver WHERE B.account_id=? AND B.buddy_name=? AND cap='urn:xmpp:idle:1' AND R.lastInteraction!=0 ORDER BY R.lastInteraction DESC LIMIT 1;" andArguments:@[accountNo, jid]]; + NSNumber* idle = [self.db executeScalar:@"SELECT R.lastInteraction FROM buddy_resources AS R INNER JOIN buddylist AS B ON R.buddy_id=B.buddy_id INNER JOIN ver_info AS V ON R.ver=V.ver WHERE B.account_id=? AND B.buddy_name=? AND V.account_id=? AND V.cap='urn:xmpp:idle:1' AND R.lastInteraction!=0 ORDER BY R.lastInteraction DESC LIMIT 1;" andArguments:@[accountNo, jid, accountNo]]; //this will only return a value if the buddy has a last interaction not being NULL or 0 NSNumber* globalIdle = [self.db executeScalar:@"SELECT lastInteraction FROM buddylist WHERE account_id=? AND buddy_name=? AND NOT (lastInteraction IS NULL OR lastInteraction==0);" andArguments:@[accountNo, jid]]; @@ -2319,7 +2338,7 @@ -(NSDate* _Nullable) lastInteractionOfJid:(NSString* _Nonnull) jid andResource:( MLAssert(accountNo != nil, @"accountNo should not be null"); return [self.db idReadTransaction:^{ //this will only return resources supporting "urn:xmpp:idle:1" - NSNumber* lastInteraction = [self.db executeScalar:@"SELECT R.lastInteraction FROM buddy_resources AS R INNER JOIN buddylist AS B ON R.buddy_id=B.buddy_id WHERE B.account_id=? AND B.buddy_name=? AND R.resource=? AND EXISTS(SELECT * FROM ver_info AS V WHERE V.ver=R.ver AND V.cap='urn:xmpp:idle:1') LIMIT 1;" andArguments:@[accountNo, jid, resource]]; + NSNumber* lastInteraction = [self.db executeScalar:@"SELECT R.lastInteraction FROM buddy_resources AS R INNER JOIN buddylist AS B ON R.buddy_id=B.buddy_id WHERE B.account_id=? AND B.buddy_name=? AND R.resource=? AND EXISTS(SELECT * FROM ver_info AS V WHERE V.ver=R.ver AND V.account_id=B.account_id AND V.cap='urn:xmpp:idle:1') LIMIT 1;" andArguments:@[accountNo, jid, resource]]; DDLogDebug(@"LastInteraction of %@/%@ lastInteraction=%@", jid, resource, lastInteraction); if(lastInteraction == nil) return (NSDate*)nil; diff --git a/Monal/Classes/DataLayerMigrations.m b/Monal/Classes/DataLayerMigrations.m index bc4b42512f..80b379c3e9 100644 --- a/Monal/Classes/DataLayerMigrations.m +++ b/Monal/Classes/DataLayerMigrations.m @@ -1020,7 +1020,21 @@ FOREIGN KEY('account_id') REFERENCES 'account'('account_id') ON DELETE CASCADE\ [db executeNonQuery:@"UPDATE account SET username=TRIM(username);"]; [db executeNonQuery:@"UPDATE account SET domain=TRIM(domain);"]; }]; - + + [self updateDB:db withDataLayer:dataLayer toVersion:6.201 withBlock:^{ + [db executeNonQuery:@"DROP TABLE IF EXISTS ver_info;"]; + [db executeNonQuery:@"CREATE TABLE ver_info( \ + ver VARCHAR(32), \ + cap VARCHAR(255), \ + timestamp INTEGER NOT NULL DEFAULT 0, \ + account_id INTEGER NOT NULL, \ + PRIMARY KEY (ver, cap, account_id) \ + FOREIGN KEY('account_id') REFERENCES 'account'('account_id') ON DELETE CASCADE \ + );"]; + [db executeNonQuery:@"DROP TABLE IF EXISTS ver_timestamp;"]; + }]; + + //check if device id changed and invalidate state, if so //but do so only for non-sandbox (e.g. non-development) installs if(![[HelperTools defaultsDB] boolForKey:@"isSandboxAPNS"]) diff --git a/Monal/Classes/EditGroupName.swift b/Monal/Classes/EditGroupName.swift new file mode 100644 index 0000000000..abadd364d9 --- /dev/null +++ b/Monal/Classes/EditGroupName.swift @@ -0,0 +1,53 @@ +// +// EditGroupName.swift +// Monal +// +// Created by Friedrich Altheide on 24.02.24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +import SwiftUI + +struct EditGroupName: View { + @StateObject var contact: ObservableKVOWrapper + private let account: xmpp? + @State private var groupName: String + @State private var isEditingGroupName: Bool = false + + @Environment(\.presentationMode) var presentationMode + + init(contact: ObservableKVOWrapper) { + MLAssert(contact.isGroup, "contact must be a muc") + + _groupName = State(wrappedValue: contact.obj.contactDisplayName) + _contact = StateObject(wrappedValue: contact) + self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! as xmpp + } + + var body: some View { + + NavigationView { + Form { + Section(header: Text("Group name")) { + TextField(NSLocalizedString("Group Name (optional)", comment: "placeholder when editing a group name"), text: $groupName, onEditingChanged: { isEditingGroupName = $0 }) + .autocorrectionDisabled() + .autocapitalization(.none) + .addClearButton(isEditing: isEditingGroupName, text:$groupName) + } + } + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Abort") { + self.presentationMode.wrappedValue.dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + self.account!.mucProcessor.changeName(ofMuc: contact.contactJid, to: self.groupName) + self.presentationMode.wrappedValue.dismiss() + } + } + } + } + } +} diff --git a/Monal/Classes/EditGroupSubject.swift b/Monal/Classes/EditGroupSubject.swift new file mode 100644 index 0000000000..7dd8e816ec --- /dev/null +++ b/Monal/Classes/EditGroupSubject.swift @@ -0,0 +1,61 @@ +// +// EditGroupSubject.swift +// Monal +// +// Created by Friedrich Altheide on 27.02.24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +import SwiftUI + +struct EditGroupSubject: View { + @StateObject var contact: ObservableKVOWrapper + private let account: xmpp? + @State private var subject: String + @State private var isEditingSubject: Bool = false + + @Environment(\.presentationMode) var presentationMode + + init(contact: ObservableKVOWrapper) { + MLAssert(contact.isGroup, "contact must be a muc") + + _subject = State(wrappedValue: contact.obj.groupSubject) + _contact = StateObject(wrappedValue: contact) + self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! as xmpp + } + + var body: some View { + NavigationView { + VStack { + Form { + Section(header: Text("Group Description")) { + TextField(NSLocalizedString("Group Description (optional)", comment: "placeholder when editing a group description"), text: $subject, onEditingChanged: { isEditingSubject = $0 }) + .multilineTextAlignment(.leading) + .applyClosure { view in + if #available(iOS 16.0, *) { + view.lineLimit(10...50) + } else { + view + } + } + .addClearButton(isEditing: isEditingSubject, text:$subject) + } + } + } + .navigationTitle("Group description") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Abort") { + self.presentationMode.wrappedValue.dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + self.account!.mucProcessor.changeSubject(ofMuc: contact.contactJid, to: self.subject) + self.presentationMode.wrappedValue.dismiss() + } + } + } + } + } +} diff --git a/Monal/Classes/GroupDetailsEdit.swift b/Monal/Classes/GroupDetailsEdit.swift new file mode 100644 index 0000000000..36fb1b39b9 --- /dev/null +++ b/Monal/Classes/GroupDetailsEdit.swift @@ -0,0 +1,104 @@ +// +// GroupDetailsEdit.swift +// Monal +// +// Created by Friedrich Altheide on 23.02.24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +import SwiftUI +import _PhotosUI_SwiftUI + +struct GroupDetailsEdit: View { + @StateObject var contact: ObservableKVOWrapper + @State private var showingSheetEditName = false + @State private var showingSheetEditSubject = false + @State private var inputImage: UIImage? + @State private var showingImagePicker = false + @StateObject private var overlay = LoadingOverlayState() + private let account: xmpp + private let ownAffiliation: String? + + init(contact: ObservableKVOWrapper, ownAffiliation: String?) { + MLAssert(contact.isGroup) + + _contact = StateObject(wrappedValue: contact) + _inputImage = State(initialValue: contact.avatar) + self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: contact.accountId)! as xmpp + self.ownAffiliation = ownAffiliation + } + + var body: some View { + Form { + if ownAffiliation == "owner" { + Section { + HStack { + Spacer() + Image(uiImage: contact.avatar) + .resizable() + .scaledToFit() + .accessibilityLabel((contact.obj.mucType == "group") ? "Group Avatar" : "Channel Avatar") + .frame(width: 150, height: 150, alignment: .center) + .shadow(radius: 7) + .onTapGesture { + showingImagePicker = true + } + Spacer() + } + .sheet(isPresented:$showingImagePicker) { + ImagePicker(image:$inputImage) + } + } + } + + Section { + if ownAffiliation == "owner" { + Button(action: { + showingSheetEditName.toggle() + }) { + HStack { + Image(systemName: "person.2") + Text(contact.contactDisplayName as String) + Spacer() + } + } + .sheet(isPresented: $showingSheetEditName) { + LazyClosureView(EditGroupName(contact: contact)) + } + } + + Button(action: { + showingSheetEditSubject.toggle() + }) { + HStack { + Image(systemName: "pencil") + if contact.obj.mucType == "group" { + Text("Group description") + } else { + Text("Channel description") + } + Spacer() + } + } + .sheet(isPresented: $showingSheetEditSubject) { + LazyClosureView(EditGroupSubject(contact: contact)) + } + } + } + .addLoadingOverlay(overlay) + .navigationTitle((contact.obj.mucType == "group") ? "Edit group" : "Edit channel") + .onChange(of:inputImage) { _ in + showLoadingOverlay(overlay, headline: NSLocalizedString("Uploading image...", comment: "")) + self.account.mucProcessor.publishAvatar(inputImage, forMuc: contact.contactJid) + } + .onChange(of:contact.avatar as UIImage) { _ in + hideLoadingOverlay(overlay) + } + } +} + +struct GroupDetailsEdit_Previews: PreviewProvider { + static var previews: some View { + GroupDetailsEdit(contact:ObservableKVOWrapper(MLContact.makeDummyContact(0)), ownAffiliation:"owner") + } +} diff --git a/Monal/Classes/HelperTools.h b/Monal/Classes/HelperTools.h index af470c9894..d263b0f84f 100644 --- a/Monal/Classes/HelperTools.h +++ b/Monal/Classes/HelperTools.h @@ -87,6 +87,7 @@ void swizzle(Class c, SEL orig, SEL new); +(void) addUploadItemPreviewForItem:(NSURL* _Nullable) url provider:(NSItemProvider* _Nullable) provider andPayload:(NSMutableDictionary*) payload withCompletionHandler:(void(^)(NSMutableDictionary* _Nullable)) completion; +(void) handleUploadItemProvider:(NSItemProvider*) provider withCompletionHandler:(void (^)(NSMutableDictionary* _Nullable)) completion; +(UIView*) buttonWithNotificationBadgeForImage:(UIImage*) image hasNotification:(bool) hasNotification withTapHandler: (UITapGestureRecognizer*) handler; ++(UIImage*) imageWithNotificationBadgeForImage:(UIImage*) image; +(NSData*) resizeAvatarImage:(UIImage* _Nullable) image withCircularMask:(BOOL) circularMask toMaxBase64Size:(unsigned long) length; +(double) report_memory; +(UIColor*) generateColorFromJid:(NSString*) jid; diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index a5dda65679..5314446f9e 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -102,7 +102,7 @@ @interface KSCrash() int asyncSafeCopyFile(const char* from, const char* to) { int fd_to, fd_from; - char buf[4096]; + char buf[1024]; ssize_t nread; int saved_errno; @@ -201,9 +201,15 @@ void logException(NSException* exception) void uncaughtExceptionHandler(NSException* exception) { logException(exception); +//don't let kscrash handle the exception if we are in the simulator +//(this makes sure xcode will catch the exception and show proper backtraces etc.) +#if TARGET_OS_SIMULATOR + return; +#else //make sure this crash will be recorded by kscrash using the NSException rather than the c++ exception thrown by the objc runtime //this will make sure that the stacktrace matches the objc exception rather than being a top level c++ stacktrace KSCrash.sharedInstance.uncaughtExceptionHandler(exception); +#endif } //this function will only be in use under macos alpha builds to log every exception (even when catched with @try-@catch constructs) @@ -352,11 +358,13 @@ +(NSString*) extractXMPPError:(XMPPStanza*) stanza withDescription:(NSString*) d { if(description == nil || [description isEqualToString:@""]) description = @"XMPP Error"; + NSMutableString* message = [description mutableCopy]; NSString* errorReason = [stanza findFirst:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}!text$"]; + if(errorReason && ![errorReason isEqualToString:@""]) + [message appendString:[NSString stringWithFormat:@": %@", errorReason]]; NSString* errorText = [stanza findFirst:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}text#"]; - NSString* message = [NSString stringWithFormat:@"%@: %@", description, errorReason]; if(errorText && ![errorText isEqualToString:@""]) - message = [NSString stringWithFormat:@"%@: %@ (%@)", description, errorReason, errorText]; + [message appendString:[NSString stringWithFormat:@" (%@)", errorText]]; return message; } @@ -463,9 +471,10 @@ +(NSRunLoop*) getExtraRunloopWithIdentifier:(MLRunLoopIdentifier) identifier //every identifier has its own thread priority/qos class __block dispatch_queue_priority_t priority; + __block char* name; switch(identifier) { - case MLRunLoopIdentifierNetwork: priority = DISPATCH_QUEUE_PRIORITY_BACKGROUND; break; + case MLRunLoopIdentifierNetwork: priority = DISPATCH_QUEUE_PRIORITY_BACKGROUND; name = "im.monal.runloop.networking"; break; default: unreachable(@"unknown runloop identifier!"); } @@ -474,7 +483,7 @@ +(NSRunLoop*) getExtraRunloopWithIdentifier:(MLRunLoopIdentifier) identifier { NSCondition* condition = [NSCondition new]; [condition lock]; - dispatch_async(dispatch_get_global_queue(priority, 0), ^{ + dispatch_async(dispatch_queue_create_with_target(name, DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(priority, 0)), ^{ //we don't need an @synchronized block around this because the @synchronized block of the outer thread //waits until we signal our condition (e.g. no other thread can race with us) NSRunLoop* localLoop = runloops[@(identifier)] = [NSRunLoop currentRunLoop]; @@ -1550,7 +1559,7 @@ +(void) activityLog #endif if(log_activity) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + dispatch_async(dispatch_queue_create_with_target("im.monal.activityLog", DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)), ^{ unsigned long counter = 1; while(counter++) { @@ -1779,8 +1788,10 @@ +(void) installCrashHandler handler.monitoring = KSCrashMonitorTypeProductionSafe; //KSCrashMonitorTypeAll handler.onCrash = crash_callback; //this can trigger crashes on macos < 13 (e.g. mac catalyst < 16) (and possibly ios < 16) - if(@available(iOS 16.0, macCatalyst 16.0, *)) +#if !TARGET_OS_MACCATALYST + if(@available(iOS 16.0, *)) [handler enableSwapOfCxaThrow]; +#endif handler.searchQueueNames = NO; //this is not async safe and can crash :( handler.introspectMemory = YES; handler.addConsoleLogToReport = YES; diff --git a/Monal/Classes/ImageViewer.swift b/Monal/Classes/ImageViewer.swift index 4220243c51..f23d4135cf 100644 --- a/Monal/Classes/ImageViewer.swift +++ b/Monal/Classes/ImageViewer.swift @@ -42,10 +42,6 @@ struct ImageViewer: View { init(delegate: SheetDismisserProtocol, info:[String:AnyObject]) throws { self.delegate = delegate self.info = info - if #available(iOS 16, *) { - let mimeType = UTType(exportedAs:info["mimeType"] as! String, conformingTo:.data) - DDLogError("mimeType = \(String(describing:mimeType))") - } } // var body: some View { @@ -56,12 +52,12 @@ struct ImageViewer: View { // } var body: some View { - let image = UIImage(contentsOfFile:info["cacheFile"] as! String)! - ZStack(alignment: .top) { Color.background .edgesIgnoringSafeArea(.all) + let image = UIImage(contentsOfFile:info["cacheFile"] as! String)! + VStack { ZoomableContainer(maxScale:8.0, doubleTapScale:4.0) { if (info["mimeType"] as! String).hasPrefix("image/gif") { diff --git a/Monal/Classes/LoadingOverlay.swift b/Monal/Classes/LoadingOverlay.swift index eed244a60f..bb8f0d4f62 100644 --- a/Monal/Classes/LoadingOverlay.swift +++ b/Monal/Classes/LoadingOverlay.swift @@ -85,8 +85,8 @@ func hideLoadingOverlay(_ overlay: LoadingOverlayState) { } struct LoadingOverlay_Previews: PreviewProvider { - @ObservedObject static var overlay1 = LoadingOverlayState(enabled:true, headline:AnyView(Text("Loading")), description:AnyView(Text("More info?"))) - @ObservedObject static var overlay2 = LoadingOverlayState(enabled:true, headline:AnyView(Text("Loading")), description:AnyView(HStack { + @StateObject static var overlay1 = LoadingOverlayState(enabled:true, headline:AnyView(Text("Loading")), description:AnyView(Text("More info?"))) + @StateObject static var overlay2 = LoadingOverlayState(enabled:true, headline:AnyView(Text("Loading")), description:AnyView(HStack { Image(systemName: "checkmark") Text("Doing a lot of work...") })) diff --git a/Monal/Classes/MLBlockedUsersTableViewController.m b/Monal/Classes/MLBlockedUsersTableViewController.m index 8980b2829f..0a28598d78 100644 --- a/Monal/Classes/MLBlockedUsersTableViewController.m +++ b/Monal/Classes/MLBlockedUsersTableViewController.m @@ -120,7 +120,7 @@ - (IBAction)addBlockButton:(id)sender { passwordField.placeholder = NSLocalizedString(@"user@example.org/resource", @"BlockUserTable - blockJidForm"); }]; - UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Block", @"") style:UIAlertActionStyleDefault + [blockJidForm addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Block", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) { NSString* jidToBlock = [blockJidForm textFields][0].text; // try to split the jid @@ -143,9 +143,10 @@ - (IBAction)addBlockButton:(id)sender { [invalidJid addAction:defaultAction]; [self presentViewController:invalidJid animated:YES completion:nil]; } - }]; - - [blockJidForm addAction:defaultAction]; + }]]; + [blockJidForm addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { + [blockJidForm dismissViewControllerAnimated:YES completion:nil]; + }]]; [self presentViewController:blockJidForm animated:YES completion:nil]; } @end diff --git a/Monal/Classes/MLCall.m b/Monal/Classes/MLCall.m index f0d115406d..03705f57ea 100644 --- a/Monal/Classes/MLCall.m +++ b/Monal/Classes/MLCall.m @@ -496,6 +496,7 @@ -(void) migrateTo:(MLCall*) otherCall DDLogDebug(@"%@: Preparing this call for new webrtc connection...", [self short]); self.jmiid = otherCall.jmiid; self.fullRemoteJid = otherCall.fullRemoteJid; + self.callType = otherCall.callType; self.isConnected = NO; self.isReconnecting = YES; self.finishReason = MLCallFinishReasonUnknown; diff --git a/Monal/Classes/MLConstants.h b/Monal/Classes/MLConstants.h index 717cd64dce..4cbbf0b101 100644 --- a/Monal/Classes/MLConstants.h +++ b/Monal/Classes/MLConstants.h @@ -208,4 +208,4 @@ static inline NSString* _Nonnull LocalizationNotNeeded(NSString* _Nonnull s) //build MLXMLNode query statistics (will only optimize MLXMLNode queries if *not* defined) //#define QueryStatistics 1 -#define geoPattern @"^geo:(-?(?:90|[1-8][0-9]|[0-9])(?:\\.[0-9]{1,32})?),(-?(?:180|1[0-7][0-9]|[0-9]{1,2})(?:\\.[0-9]{1,32})?)$" +#define geoPattern @"^geo:(-?(?:90|[1-8][0-9]|[0-9])(?:\\.[0-9]{1,32})?),(-?(?:180|1[0-7][0-9]|[0-9]{1,2})(?:\\.[0-9]{1,32})?)(;.*)?$" diff --git a/Monal/Classes/MLContact.h b/Monal/Classes/MLContact.h index ee4bce8cc3..49aa660b8c 100644 --- a/Monal/Classes/MLContact.h +++ b/Monal/Classes/MLContact.h @@ -111,6 +111,8 @@ FOUNDATION_EXPORT NSString* const kAskSubscribe; -(void) clearHistory; -(void) removeShareInteractions; +-(NSUInteger) hash; + @end NS_ASSUME_NONNULL_END diff --git a/Monal/Classes/MLContact.m b/Monal/Classes/MLContact.m index afc77b9a22..ed8ccaedf7 100644 --- a/Monal/Classes/MLContact.m +++ b/Monal/Classes/MLContact.m @@ -214,7 +214,7 @@ +(MLContact*) createContactFromJid:(NSString*) jid andAccountNo:(NSNumber*) acco @"state": @"offline", @"count": @0, @"isActiveChat": @NO, - @"lastInteraction": [[NSDate date] initWithTimeIntervalSince1970:0], + @"lastInteraction": nilWrapper(nil), }]; } else @@ -412,6 +412,11 @@ -(BOOL) isSelfChat return [self.contactJid isEqualToString:account.connectionProperties.identity.jid]; } ++(NSSet*) keyPathsForValuesAffectingIsSelfChat +{ + return [NSSet setWithObjects:@"contactJid", @"accountId", nil]; +} + -(BOOL) isInRoster { // mucs have a subscription of both (ensured by the datalayer) @@ -420,33 +425,63 @@ -(BOOL) isInRoster || [self.ask isEqualToString:kAskSubscribe]; } ++(NSSet*) keyPathsForValuesAffectingIsInRoster +{ + return [NSSet setWithObjects:@"subscription", @"ask", nil]; +} + -(BOOL) isSubscribedTo { return [self.subscription isEqualToString:kSubBoth] || [self.subscription isEqualToString:kSubTo]; } ++(NSSet*) keyPathsForValuesAffectingIsSubscribedTo +{ + return [NSSet setWithObjects:@"subscription", nil]; +} + -(BOOL) isSubscribedFrom { return [self.subscription isEqualToString:kSubBoth] || [self.subscription isEqualToString:kSubFrom]; } ++(NSSet*) keyPathsForValuesAffectingIsSubscribedFrom +{ + return [NSSet setWithObjects:@"subscription", nil]; +} + -(BOOL) isSubscribedBoth { return [self.subscription isEqualToString:kSubBoth]; } ++(NSSet*) keyPathsForValuesAffectingIsSubscribedBoth +{ + return [NSSet setWithObjects:@"subscription", nil]; +} + -(BOOL) hasIncomingContactRequest { return self.isGroup == NO && [[DataLayer sharedInstance] hasContactRequestForContact:self]; } ++(NSSet*) keyPathsForValuesAffectingHasIncomingContactRequest +{ + return [NSSet setWithObjects:@"isGroup", nil]; +} + -(BOOL) hasOutgoingContactRequest { return self.isGroup == NO && [self.ask isEqualToString:kAskSubscribe]; } ++(NSSet*) keyPathsForValuesAffectingHasOutgoingContactRequest +{ + return [NSSet setWithObjects:@"isGroup", @"ask", nil]; +} + // this will cache the unread count on first access -(NSInteger) unreadCount { @@ -710,7 +745,7 @@ +(MLContact*) contactFromDictionary:(NSDictionary*) dic contact.isEncrypted = [[dic objectForKey:@"encrypt"] boolValue]; contact.isMuted = [[dic objectForKey:@"muted"] boolValue]; // initial value comes from db, all other values get updated by our kMonalLastInteractionUpdatedNotice handler - contact.lastInteractionTime = [dic objectForKey:@"lastInteraction"]; //no default needed, already done in DataLayer + contact.lastInteractionTime = nilExtractor([dic objectForKey:@"lastInteraction"]); //no default needed, already done in DataLayer contact->_avatar = nil; return contact; } diff --git a/Monal/Classes/MLCrashReporter.m b/Monal/Classes/MLCrashReporter.m index e6f888784f..f46ff1bec7 100644 --- a/Monal/Classes/MLCrashReporter.m +++ b/Monal/Classes/MLCrashReporter.m @@ -274,6 +274,9 @@ -(void) filterReports:(NSArray*) reports onCompletion:(KSCrashReportFilterComple { NSMutableString* auxData = [NSMutableString new]; + //add version of monal reporting this crash + [auxData appendString:[NSString stringWithFormat:@"reporterVersion: %@\n", [HelperTools appBuildVersionInfoFor:MLVersionTypeLog]]]; + //add user data to aux data for(NSString* userKey in report[@"user"]) [auxData appendString:[NSString stringWithFormat:@"%@: %@\n", userKey, report[@"user"][userKey]]]; diff --git a/Monal/Classes/MLFiletransfer.m b/Monal/Classes/MLFiletransfer.m index 9f49d07ba8..6527f8ee0e 100644 --- a/Monal/Classes/MLFiletransfer.m +++ b/Monal/Classes/MLFiletransfer.m @@ -845,7 +845,7 @@ +(void) setErrorType:(NSString*) errorType andErrorText:(NSString*) errorText fo { DDLogInfo(@"Encrypting file data before upload"); encryptedPayload = [AESGcm encrypt:fileData keySize:32]; - if(encryptedPayload) + if(encryptedPayload && encryptedPayload.body != nil) { NSMutableData* encryptedData = [encryptedPayload.body mutableCopy]; [encryptedData appendData:encryptedPayload.authTag]; @@ -866,6 +866,10 @@ +(void) setErrorType:(NSString*) errorType andErrorText:(NSString*) errorText fo if(encrypted) sendMimeType = @"application/octet-stream"; + MLAssert(fileData != nil, @"fileData should never be nil!"); + MLAssert(userFacingFilename != nil, @"userFacingFilename should never be nil!"); + MLAssert(sendMimeType != nil, @"sendMimeType should never be nil!"); + DDLogDebug(@"Requesting file upload slot for mimeType %@", sendMimeType); [account requestHTTPSlotWithParams:@{ @"data":fileData, diff --git a/Monal/Classes/MLHandler.h b/Monal/Classes/MLHandler.h index 1cf4b8c911..74aff082bf 100644 --- a/Monal/Classes/MLHandler.h +++ b/Monal/Classes/MLHandler.h @@ -131,8 +131,8 @@ MLHandler* h = $newHandlerWithInvalidation(ClassName, myHandlerName, myInvalidat #define $$ } //call handler/invalidation -#define $call(handler, ...) [handler callWithArguments:@{ __VA_ARGS__ }] -#define $invalidate(handler, ...) [handler invalidateWithArguments:@{ __VA_ARGS__ }] +#define $call(handler, ...) do { if(handler != nil) { [handler callWithArguments:@{ __VA_ARGS__ }]; } } while(0) +#define $invalidate(handler, ...) do { if(handler != nil) { [handler invalidateWithArguments:@{ __VA_ARGS__ }]; } } while(0) //internal stuff //$_*() and $$*() will add parentheses around its result to make sure all inner commas like those probably exposed by an inner STRIP_PARENTHESES() call get not diff --git a/Monal/Classes/MLIQProcessor.m b/Monal/Classes/MLIQProcessor.m index 6664e72e2c..e1a91e63ec 100644 --- a/Monal/Classes/MLIQProcessor.m +++ b/Monal/Classes/MLIQProcessor.m @@ -26,6 +26,11 @@ @implementation MLIQProcessor +(void) processUnboundIq:(XMPPIQ*) iqNode forAccount:(xmpp*) account { + //only handle these iqs if the remote user is on our roster + MLContact* contact = [MLContact createContactFromJid:iqNode.fromUser andAccountNo:account.accountNo]; + if(![account.connectionProperties.identity.jid isEqualToString:iqNode.fromUser] && !(contact.isSubscribedFrom && !contact.isGroup)) + DDLogWarn(@"Invalid sender for iq (!subscribedFrom || isGroup), ignoring: %@", iqNode); + if([iqNode check:@"/"]) [self processGetIq:iqNode forAccount:account]; else if([iqNode check:@"/"]) @@ -40,40 +45,33 @@ +(void) processUnboundIq:(XMPPIQ*) iqNode forAccount:(xmpp*) account +(void) processGetIq:(XMPPIQ*) iqNode forAccount:(xmpp*) account { - //only handle these iqs if the remote user is on our roster - MLContact* contact = [MLContact createContactFromJid:iqNode.fromUser andAccountNo:account.accountNo]; - if([account.connectionProperties.identity.jid isEqualToString:iqNode.fromUser] || (contact.isSubscribedFrom && !contact.isGroup)) + if([iqNode check:@"{urn:xmpp:ping}ping"]) { - if([iqNode check:@"{urn:xmpp:ping}ping"]) - { - XMPPIQ* pong = [[XMPPIQ alloc] initAsResponseTo:iqNode]; - [pong setiqTo:iqNode.from]; - [account send:pong]; - return; - } - - if([iqNode check:@"{jabber:iq:version}query"] && [[HelperTools defaultsDB] boolForKey: @"allowVersionIQ"]) - { - XMPPIQ* versioniq = [[XMPPIQ alloc] initAsResponseTo:iqNode]; - [versioniq setiqTo:iqNode.from]; - [versioniq setVersion]; - [account send:versioniq]; - return; - } - - if([iqNode check:@"{http://jabber.org/protocol/disco#info}query"]) - { - XMPPIQ* discoInfoResponse = [[XMPPIQ alloc] initAsResponseTo:iqNode]; - [discoInfoResponse setDiscoInfoWithFeatures:account.capsFeatures identity:account.capsIdentity andNode:[iqNode findFirst:@"{http://jabber.org/protocol/disco#info}query@node"]]; - [account send:discoInfoResponse]; - return; - } - - DDLogWarn(@"Got unhandled get IQ: %@", iqNode); - [self respondWithErrorTo:iqNode onAccount:account]; + XMPPIQ* pong = [[XMPPIQ alloc] initAsResponseTo:iqNode]; + [pong setiqTo:iqNode.from]; + [account send:pong]; + return; } - else - DDLogWarn(@"Invalid sender for get iq (!subscribedFrom || isGroup), ignoring iq: %@", iqNode); + + if([iqNode check:@"{jabber:iq:version}query"] && [[HelperTools defaultsDB] boolForKey: @"allowVersionIQ"]) + { + XMPPIQ* versioniq = [[XMPPIQ alloc] initAsResponseTo:iqNode]; + [versioniq setiqTo:iqNode.from]; + [versioniq setVersion]; + [account send:versioniq]; + return; + } + + if([iqNode check:@"{http://jabber.org/protocol/disco#info}query"]) + { + XMPPIQ* discoInfoResponse = [[XMPPIQ alloc] initAsResponseTo:iqNode]; + [discoInfoResponse setDiscoInfoWithFeatures:account.capsFeatures identity:account.capsIdentity andNode:[iqNode findFirst:@"{http://jabber.org/protocol/disco#info}query@node"]]; + [account send:discoInfoResponse]; + return; + } + + DDLogWarn(@"Got unhandled get IQ: %@", iqNode); + [self respondWithErrorTo:iqNode onAccount:account]; } +(void) processSetIq:(XMPPIQ*) iqNode forAccount:(xmpp*) account @@ -92,82 +90,70 @@ +(void) processSetIq:(XMPPIQ*) iqNode forAccount:(xmpp*) account return; } - MLContact* contact = [MLContact createContactFromJid:iqNode.fromUser andAccountNo:account.accountNo]; - if([account.connectionProperties.identity.jid isEqualToString:iqNode.fromUser] || (contact.isSubscribedFrom && !contact.isGroup)) + //its a roster push (sanity check will be done in processRosterWithAccount:andIqNode:) + if([iqNode check:@"{jabber:iq:roster}query"]) { - //its a roster push (sanity check will be done in processRosterWithAccount:andIqNode:) - if([iqNode check:@"{jabber:iq:roster}query"]) + //this will only return YES, if the roster push was allowed and processed successfully + if([self processRosterWithAccount:account andIqNode:iqNode]) { - //this will only return YES, if the roster push was allowed and processed successfully - if([self processRosterWithAccount:account andIqNode:iqNode]) - { - //send empty result iq as per RFC 6121 requirements - XMPPIQ* reply = [[XMPPIQ alloc] initAsResponseTo:iqNode]; - [reply setiqTo:iqNode.from]; - [account send:reply]; - } - return; + //send empty result iq as per RFC 6121 requirements + XMPPIQ* reply = [[XMPPIQ alloc] initAsResponseTo:iqNode]; + [reply setiqTo:iqNode.from]; + [account send:reply]; } + return; + } - if([iqNode check:@"{urn:xmpp:blocking}block"] || [iqNode check:@"{urn:xmpp:blocking}unblock"]) + if([iqNode check:@"{urn:xmpp:blocking}block"] || [iqNode check:@"{urn:xmpp:blocking}unblock"]) + { + //make sure we don't process blocking updates not coming from our own account + if(account.connectionProperties.supportsBlocking && (iqNode.from == nil || [iqNode.fromUser isEqualToString:account.connectionProperties.identity.jid])) { - //make sure we don't process blocking updates not coming from our own account - if(account.connectionProperties.supportsBlocking && (iqNode.from == nil || [iqNode.fromUser isEqualToString:account.connectionProperties.identity.jid])) + BOOL blockingUpdated = NO; + // mark jid as unblocked + if([iqNode check:@"{urn:xmpp:blocking}unblock"]) { - BOOL blockingUpdated = NO; - // mark jid as unblocked - if([iqNode check:@"{urn:xmpp:blocking}unblock"]) + NSArray* unBlockItems = [iqNode find:@"{urn:xmpp:blocking}unblock/item@@"]; + for(NSDictionary* item in unBlockItems) { - NSArray* unBlockItems = [iqNode find:@"{urn:xmpp:blocking}unblock/item@@"]; - for(NSDictionary* item in unBlockItems) - { - if(item && item[@"jid"]) - [[DataLayer sharedInstance] unBlockJid:item[@"jid"] withAccountNo:account.accountNo]; - } - if(unBlockItems && unBlockItems.count == 0) - { - // remove all blocks - [account updateLocalBlocklistCache:[[NSSet alloc] init]]; - } - blockingUpdated = YES; + if(item && item[@"jid"]) + [[DataLayer sharedInstance] unBlockJid:item[@"jid"] withAccountNo:account.accountNo]; } - // mark jid as blocked - if([iqNode check:@"{urn:xmpp:blocking}block"]) + if(unBlockItems && unBlockItems.count == 0) { - for(NSDictionary* item in [iqNode find:@"{urn:xmpp:blocking}block/item@@"]) - { - if(item && item[@"jid"]) - [[DataLayer sharedInstance] blockJid:item[@"jid"] withAccountNo:account.accountNo]; - } - blockingUpdated = YES; + // remove all blocks + [account updateLocalBlocklistCache:[[NSSet alloc] init]]; } - if(blockingUpdated) + blockingUpdated = YES; + } + // mark jid as blocked + if([iqNode check:@"{urn:xmpp:blocking}block"]) + { + for(NSDictionary* item in [iqNode find:@"{urn:xmpp:blocking}block/item@@"]) { - // notify the views - [[MLNotificationQueue currentQueue] postNotificationName:kMonalBlockListRefresh object:account userInfo:@{@"accountNo": account.accountNo}]; + if(item && item[@"jid"]) + [[DataLayer sharedInstance] blockJid:item[@"jid"] withAccountNo:account.accountNo]; } + blockingUpdated = YES; + } + if(blockingUpdated) + { + // notify the views + [[MLNotificationQueue currentQueue] postNotificationName:kMonalBlockListRefresh object:account userInfo:@{@"accountNo": account.accountNo}]; } - else - DDLogWarn(@"Invalid sender for blocklist, ignoring iq: %@", iqNode); - return; } - - DDLogWarn(@"Got unhandled set IQ: %@", iqNode); - [self respondWithErrorTo:iqNode onAccount:account]; + else + DDLogWarn(@"Invalid sender for blocklist, ignoring iq: %@", iqNode); + return; } - else - DDLogWarn(@"Invalid sender for set iq (!subscribedFrom || isGroup), ignoring iq: %@", iqNode); + + DDLogWarn(@"Got unhandled set IQ: %@", iqNode); + [self respondWithErrorTo:iqNode onAccount:account]; } +(void) processResultIq:(XMPPIQ*) iqNode forAccount:(xmpp*) account { - //this is the only iq result that does not need any state - //WARNING: be careful adding other stateless result handlers (those can impose security risks!) - if([iqNode check:@"{jabber:iq:version}query"]) - { - [self iqVersionResult:iqNode forAccount:account]; - return; - } + //WARNING: be careful adding stateless result handlers here (those can impose security risks!) DDLogWarn(@"Got unhandled result IQ: %@", iqNode); [self respondWithErrorTo:iqNode onAccount:account]; @@ -631,7 +617,7 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode NSSet* features = [NSSet setWithArray:[iqNode find:@"{http://jabber.org/protocol/disco#info}query/feature@var"]]; NSArray* forms = [iqNode find:@"{http://jabber.org/protocol/disco#info}query/{jabber:x:data}x"]; NSString* ver = [HelperTools getEntityCapsHashForIdentities:identities andFeatures:features andForms:forms]; - [[DataLayer sharedInstance] setCaps:features forVer:ver]; + [[DataLayer sharedInstance] setCaps:features forVer:ver onAccountNo:account.accountNo]; [account markCapsQueryCompleteFor:ver]; //send out kMonalContactRefresh notification @@ -714,12 +700,12 @@ +(BOOL) processRosterWithAccount:(xmpp*) account andIqNode:(XMPPIQ*) iqNode } $$ -+(void) iqVersionResult:(XMPPIQ*) iqNode forAccount:(xmpp*) account -{ +$$class_handler(handleVersionResponse, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode)) NSString* iqAppName = [iqNode findFirst:@"{jabber:iq:version}query/name#"]; NSString* iqAppVersion = [iqNode findFirst:@"{jabber:iq:version}query/version#"]; NSString* iqPlatformOS = [iqNode findFirst:@"{jabber:iq:version}query/os#"]; + //server version info is the only case where there will be no resource --> return here if([iqNode.fromUser isEqualToString:account.connectionProperties.identity.domain]) { account.connectionProperties.serverVersion = [[MLContactSoftwareVersionInfo alloc] initWithJid:iqNode.fromUser andRessource:iqNode.fromResource andAppName:iqAppName andAppVersion:iqAppVersion andPlatformOS:iqPlatformOS andLastInteraction:[NSDate date]]; @@ -738,7 +724,7 @@ +(void) iqVersionResult:(XMPPIQ*) iqNode forAccount:(xmpp*) account [[MLNotificationQueue currentQueue] postNotificationName:kMonalXmppUserSoftWareVersionRefresh object:account userInfo:@{@"versionInfo": newSoftwareVersionInfo}]; -} +$$ +(void) respondWithErrorTo:(XMPPIQ*) iqNode onAccount:(xmpp*) account { diff --git a/Monal/Classes/MLMessageProcessor.m b/Monal/Classes/MLMessageProcessor.m index b2a819ba08..a783006f83 100644 --- a/Monal/Classes/MLMessageProcessor.m +++ b/Monal/Classes/MLMessageProcessor.m @@ -67,7 +67,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag if(![messageNode check:@"/@id"]) { DDLogError(@"Ignoring error messages having an empty ID"); - return message; + return nil; } NSString* errorType = [messageNode findFirst:@"error@type"]; @@ -94,7 +94,17 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag @"errorReason": errorText }]; - return message; + return nil; + } + + NSString* buddyName = [messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid] ? messageNode.toUser : messageNode.fromUser; + MLContact* possiblyUnknownContact = [MLContact createContactFromJid:buddyName andAccountNo:account.accountNo]; + + //ignore unknown contacts if configured to do so + if(![[HelperTools defaultsDB] boolForKey: @"allowNonRosterContacts"] && !possiblyUnknownContact.isSubscribedFrom) + { + DDLogWarn(@"Ignoring incoming message stanza from unknown contact: %@", possiblyUnknownContact); + return nil; } //ignore prosody mod_muc_notifications muc push stanzas (they are only needed to trigger an apns push) @@ -104,18 +114,18 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag NSString* roomJid = [messageNode findFirst:@"{http://quobis.com/xmpp/muc#push}notification@jid"]; if([[[DataLayer sharedInstance] listMucsForAccount:account.accountNo] containsObject:roomJid]) [account.mucProcessor ping:roomJid]; - return message; + return nil; } if([messageNode check:@"//{http://jabber.org/protocol/pubsub#event}event"]) { [account.pubsub handleHeadlineMessage:messageNode]; - return message; + return nil; } - if(![messageNode check:@"/"] && [[messageNode from] isEqualToString:account.connectionProperties.identity.fullJid] && [[messageNode toUser] isEqualToString:account.connectionProperties.identity.jid]) { + //ignore messages from our own device, see this github issue: https://github.com/monal-im/Monal/issues/941 + if(![messageNode check:@"/"] && !isMLhistory && [messageNode.from isEqualToString:account.connectionProperties.identity.fullJid] && [messageNode.toUser isEqualToString:account.connectionProperties.identity.jid]) return nil; - } //handle incoming jmi calls (TODO: add entry to local history, once the UI for this is implemented) //only handle incoming propose messages if not older than 60 seconds @@ -123,7 +133,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag if([messageNode check:@"{urn:xmpp:jingle-message:0}*"] && ![HelperTools shouldProvideVoip]) { DDLogWarn(@"China locale detected, ignoring incoming JMI message!"); - return message; + return nil; } else if([messageNode check:@"{urn:xmpp:jingle-message:0}*"]) { @@ -138,7 +148,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag { //TODO: record this call in history db even if it was outgoing from another device on our account DDLogWarn(@"Ignoring incoming JMI propose coming from another device on our account"); - return message; + return nil; } //only handle jmi stanzas exchanged with contacts allowed to see us and ignore all others @@ -147,7 +157,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag if(!jmiContact.isSubscribedFrom) { DDLogWarn(@"Ignoring incoming JMI propose coming from a contact we are not subscribed from"); - return message; + return nil; } NSDate* delayStamp = [messageNode findFirst:@"{urn:xmpp:delay}delay@stamp|datetime"]; @@ -156,7 +166,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag if([[NSDate date] timeIntervalSinceDate:delayStamp] > 60.0) { DDLogWarn(@"Ignoring incoming JMI propose: too old"); - return message; + return nil; } //only allow audio calls for now @@ -173,7 +183,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag } else DDLogWarn(@"Ignoring incoming non-audio JMI call, not implemented yet"); - return message; + return nil; } //handle all other JMI events (TODO: add entry to local history, once the UI for this is implemented) //if the corresponding call is unknown these will just be ignored by MLVoipProcessor --> no presence leak @@ -192,20 +202,20 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag //in the monal compilation unit (the ui unit), the NSE resides in yet another compilation unit (the nse-appex unit) [[MLNotificationQueue currentQueue] postNotificationName:kMonalIncomingJMIStanza object:account userInfo:callData]; } - return message; + return nil; } } //ignore muc PMs (after discussion with holger we don't want to support that) if( - ![[messageNode findFirst:@"/@type"] isEqualToString:@"groupchat"] && [messageNode check:@"{http://jabber.org/protocol/muc#user}x"] && + ![messageNode check:@"/"] && [messageNode check:@"{http://jabber.org/protocol/muc#user}x"] && ![messageNode check:@"{http://jabber.org/protocol/muc#user}x/invite"] && [messageNode check:@"body#"] ) { DDLogWarn(@"Ignoring muc pm marked as such..."); //ignore muc pms without id attribute (we can't send out errors pointing to this message without an id) if([messageNode findFirst:@"/@id"] == nil) - return message; + return nil; XMPPMessage* errorReply = [XMPPMessage new]; [errorReply.attributes setObject:@"error" forKey:@"type"]; [errorReply.attributes setObject:messageNode.from forKey:@"to"]; //this has to be the full jid here @@ -216,7 +226,7 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag ] andData:nil]]; [errorReply setStoreHint]; [account send:errorReply]; - return message; + return nil; } //ignore carbon copied muc pms not marked as such NSString* carbonType = [outerMessageNode findFirst:@"{urn:xmpp:carbons:2}*$"]; @@ -227,27 +237,20 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag if(carbonTestContact.isGroup) { DDLogWarn(@"Ignoring carbon copied muc pm..."); - return message; + return nil; } else DDLogVerbose(@"Not a carbon copy of a muc pm for contact: %@", carbonTestContact); } - - NSString* possibleUnkownContact; - if([messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid]) - possibleUnkownContact = messageNode.toUser; - else - possibleUnkownContact = messageNode.fromUser; if(([messageNode check:@"/"] || [messageNode check:@"{http://jabber.org/protocol/muc#user}x"]) && ![messageNode check:@"{http://jabber.org/protocol/muc#user}x/invite"]) { - // Ignore all group chat msgs from unkown groups or 1:1 chats - MLContact* mucTestContact = [MLContact createContactFromJid:possibleUnkownContact andAccountNo:account.accountNo]; - if([[DataLayer sharedInstance] isContactInList:messageNode.fromUser forAccount:account.accountNo] == NO || !mucTestContact.isGroup) + // Ignore all group chat msgs from unkown groups + if(![[[DataLayer sharedInstance] listMucsForAccount:account.accountNo] containsObject:messageNode.fromUser]) { // ignore message DDLogWarn(@"Ignoring groupchat message from %@", messageNode.toUser); - return message; + return nil; } } else @@ -257,12 +260,12 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag { if(!isMLhistory) { - DDLogInfo(@"Handling KeyTransportElement without trying to add a 1:1 buddy %@", possibleUnkownContact); + DDLogInfo(@"Handling KeyTransportElement without trying to add a 1:1 buddy %@", possiblyUnknownContact); [account.omemo decryptMessage:messageNode withMucParticipantJid:nil]; } else - DDLogInfo(@"Ignoring MLhistory KeyTransportElement for buddy %@", possibleUnkownContact); - return message; + DDLogInfo(@"Ignoring MLhistory KeyTransportElement for buddy %@", possiblyUnknownContact); + return nil; } } @@ -290,14 +293,23 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag } //handle muc status changes or invites (this checks for the muc namespace itself) - if([account.mucProcessor processMessage:messageNode]) - return message; //the muc processor said we have stop processing + if(isMLhistory) + { + if([messageNode check:@"{http://jabber.org/protocol/muc#user}x/invite"] || ([messageNode check:@"{jabber:x:conference}x@jid"] && [[messageNode findFirst:@"{jabber:x:conference}x@jid"] length] > 0)) + return nil; //stop processing because this is a (mediated) muc invite received through backscrolling history + else + ; //continue processing for backscrolling history but don't call mucProcessor.processMessage to not process ancient status/memberlist updates + } + else if([account.mucProcessor processMessage:messageNode]) + { + DDLogVerbose(@"Muc processor said we have to stop message processing here..."); + return nil; //the muc processor said we have stop processing + } //add contact if possible (ignore groupchats or already existing contacts, or KeyTransportElements) - DDLogInfo(@"Adding possibly unknown contact for %@ to local contactlist (not updating remote roster!), doing nothing if contact is already known...", possibleUnkownContact); - [[DataLayer sharedInstance] addContact:possibleUnkownContact forAccount:account.accountNo nickname:nil]; + DDLogInfo(@"Adding possibly unknown contact for %@ to local contactlist (not updating remote roster!), doing nothing if contact is already known...", possiblyUnknownContact); + [[DataLayer sharedInstance] addContact:possiblyUnknownContact.contactJid forAccount:account.accountNo nickname:nil]; - NSString* buddyName = [messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid] ? messageNode.toUser : messageNode.fromUser; NSString* ownNick; NSString* actualFrom = messageNode.fromUser; NSString* participantJid = nil; @@ -344,7 +356,10 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag DDLogInfo(@"Got MUC subject for %@: %@", messageNode.fromUser, subject); if(subject == nil || [subject isEqualToString:currentSubject]) - return message; + { + DDLogVerbose(@"Ignoring subject, nothing changed..."); + return nil; + } DDLogVerbose(@"Updating subject in database: %@", subject); [[DataLayer sharedInstance] updateMucSubject:subject forAccount:account.accountNo andRoom:messageNode.fromUser]; @@ -354,12 +369,17 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag @"subject": subject, }]; } - return message; + else + DDLogVerbose(@"Ignoring muc subject: isMLhistory=YES..."); + return nil; } //ignore all other groupchat messages coming from bare jid (e.g. not being a "normal" muc message nor a subject update handled above) if([messageNode check:@"/"] && !messageNode.fromResource) - return message; + { + DDLogVerbose(@"Ignoring groupchat message without resource (should be already handled above)..."); + return nil; + } NSString* decrypted; if([messageNode check:@"{eu.siacs.conversations.axolotl}encrypted/header"]) @@ -379,10 +399,12 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag } else decrypted = [account.omemo decryptMessage:messageNode withMucParticipantJid:participantJid]; + + DDLogVerbose(@"Decrypted: %@", decrypted); } #ifdef IS_ALPHA - //thats the negation of our case from line 193 + //thats the negation of our case from line 375 //--> opportunistic omemo in alpha builds should use the fallback body instead of the EME error because the fallback body could be the cleartext message // (it could be a real omemo fallback, too, but there is no harm in using that instead of the EME message) if(!([messageNode check:@"{eu.siacs.conversations.axolotl}encrypted/header"] && isMLhistory && [messageNode check:@"body#"])) @@ -407,12 +429,19 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag } } + //ignore encrypted messages coming from our own device id (most probably a muc reflection) + BOOL sentByOwnOmemoDevice = NO; +#ifndef DISABLE_OMEMO + if([messageNode check:@"{eu.siacs.conversations.axolotl}encrypted/header@sid|uint"]) + sentByOwnOmemoDevice = ((NSNumber*)[messageNode findFirst:@"{eu.siacs.conversations.axolotl}encrypted/header@sid|uint"]).unsignedIntValue == [account.omemo getDeviceId].unsignedIntValue; +#endif + //handle message retraction (XEP-0424) if([messageNode check:@"{urn:xmpp:fasten:0}apply-to/{urn:xmpp:message-retract:0}retract"]) { NSString* originIdToRetract = [messageNode findFirst:@"{urn:xmpp:fasten:0}apply-to@id"]; //this checks if this message is from the same jid as the message it tries to retract for (e.g. inbound can only retract inbound and outbound only outbound) - NSNumber* historyIdToRetract = [[DataLayer sharedInstance] getHistoryIDForMessageId:originIdToRetract from:messageNode.fromUser andAccount:account.accountNo]; + NSNumber* historyIdToRetract = [[DataLayer sharedInstance] getHistoryIDForMessageId:originIdToRetract from:messageNode.fromUser actualFrom:actualFrom participantJid:participantJid andAccount:account.accountNo]; if(historyIdToRetract != nil) { @@ -464,7 +493,8 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag @"contact": [MLContact createContactFromJid:buddyName andAccountNo:account.accountNo], }]; } - else if([messageNode check:@"body#"] || decrypted) + //ignore encrypted body messages coming from our own device id (most probably a muc reflection) + else if(([messageNode check:@"body#"] || decrypted) && !sentByOwnOmemoDevice) { BOOL unread = YES; BOOL showAlert = YES; @@ -516,8 +546,10 @@ +(MLMessage* _Nullable) processMessage:(XMPPMessage*) messageNode andOuterMessag if([messageNode check:@"{urn:xmpp:message-correct:0}replace"]) { NSString* messageIdToReplace = [messageNode findFirst:@"{urn:xmpp:message-correct:0}replace@id"]; + DDLogVerbose(@"Message id to LMC-replace: %@", messageIdToReplace); //this checks if this message is from the same jid as the message it tries to do the LMC for (e.g. inbound can only correct inbound and outbound only outbound) - historyId = [[DataLayer sharedInstance] getHistoryIDForMessageId:messageIdToReplace from:messageNode.fromUser andAccount:account.accountNo]; + historyId = [[DataLayer sharedInstance] getHistoryIDForMessageId:messageIdToReplace from:messageNode.fromUser actualFrom:actualFrom participantJid:participantJid andAccount:account.accountNo]; + DDLogVerbose(@"History id to LMC-replace: %@", historyId); //now check if the LMC is allowed (we use historyIdToUse for MLhistory mam queries to only check LMC for the 3 messages coming before this ID in this converastion) //historyIdToUse will be nil, for messages going forward in time which means (check for the newest 3 messages in this conversation) if(historyId != nil && [[DataLayer sharedInstance] checkLMCEligible:historyId encrypted:encrypted historyBaseID:historyIdToUse]) diff --git a/Monal/Classes/MLMucProcessor.h b/Monal/Classes/MLMucProcessor.h index 5161c0da09..2e06ecf3d2 100644 --- a/Monal/Classes/MLMucProcessor.h +++ b/Monal/Classes/MLMucProcessor.h @@ -23,14 +23,15 @@ NS_ASSUME_NONNULL_BEGIN -(BOOL) processMessage:(XMPPMessage*) messageNode; -(void) join:(NSString*) room; --(void) leave:(NSString*) room withBookmarksUpdate:(BOOL) updateBookmarks; +-(void) leave:(NSString*) room withBookmarksUpdate:(BOOL) updateBookmarks keepBuddylistEntry:(BOOL) keepBuddylistEntry; //muc management methods --(NSString* _Nullable) createGroup:(NSString*) node; +-(NSString* _Nullable) createGroup:(NSString* _Nullable) node; -(void) changeNameOfMuc:(NSString*) room to:(NSString*) name; -(void) changeSubjectOfMuc:(NSString*) room to:(NSString*) subject; -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room; -(void) setAffiliation:(NSString*) affiliation ofUser:(NSString*) jid inMuc:(NSString*) roomJid; +-(void) inviteUser:(NSString*) jid inMuc:(NSString*) roomJid; -(void) pingAllMucs; -(void) ping:(NSString*) roomJid; diff --git a/Monal/Classes/MLMucProcessor.m b/Monal/Classes/MLMucProcessor.m index bf649ca3c0..404b8ddaf1 100644 --- a/Monal/Classes/MLMucProcessor.m +++ b/Monal/Classes/MLMucProcessor.m @@ -53,6 +53,7 @@ +(void) initialize @"muc#roomconfig_persistentroom": @"1", @"muc#roomconfig_membersonly": @"1", @"muc#roomconfig_whois": @"anyone", + //TODO: mark mam as mandatory }; _optionalGroupConfigOptions = @{ @"muc#roomconfig_enablelogging": @"0", @@ -155,8 +156,8 @@ -(void) handleResourceBound:(NSNotification*) notification } //join MUCs from (current) muc_favorites db, the pending bookmarks fetch will join the remaining currently unknown mucs - for(NSDictionary* entry in [[DataLayer sharedInstance] listMucsForAccount:_account.accountNo]) - [self join:entry[@"room"]]; + for(NSString* room in [[DataLayer sharedInstance] listMucsForAccount:_account.accountNo]) + [self join:room]; } } @@ -259,33 +260,30 @@ -(void) processPresence:(XMPPPresence*) presenceNode if(presenceNode.fromResource == nil) { DDLogVerbose(@"Got muc presence from bare jid: %@", presenceNode.from); - if([[DataLayer sharedInstance] isContactInList:presenceNode.fromUser forAccount:_account.accountNo]) + //check vcard hash + NSString* avatarHash = [presenceNode findFirst:@"{vcard-temp:x:update}x/photo#"]; + NSString* currentHash = [[DataLayer sharedInstance] getAvatarHashForContact:presenceNode.fromUser andAccount:_account.accountNo]; + DDLogVerbose(@"Checking if avatar hash in presence '%@' equals stored hash '%@'...", avatarHash, currentHash); + if(avatarHash != nil && !(currentHash && [avatarHash isEqualToString:currentHash])) { - //check vcard hash - NSString* avatarHash = [presenceNode findFirst:@"{vcard-temp:x:update}x/photo#"]; - NSString* currentHash = [[DataLayer sharedInstance] getAvatarHashForContact:presenceNode.fromUser andAccount:_account.accountNo]; - DDLogVerbose(@"Checking if avatar hash in presence '%@' equals stored hash '%@'...", avatarHash, currentHash); - if(avatarHash != nil && !(currentHash && [avatarHash isEqualToString:currentHash])) - { - DDLogInfo(@"Got new muc avatar hash '%@' for muc %@, fetching new image via vcard-temp...", avatarHash, presenceNode.fromUser); - [self fetchAvatarForRoom:presenceNode.fromUser]; - } - else if(avatarHash == nil && currentHash != nil && ![currentHash isEqualToString:@""]) - { - [[MLImageManager sharedInstance] setIconForContact:[MLContact createContactFromJid:presenceNode.fromUser andAccountNo:_account.accountNo] WithData:nil]; - [[DataLayer sharedInstance] setAvatarHash:@"" forContact:presenceNode.fromUser andAccount:_account.accountNo]; - //delete cache to make sure the image will be regenerated - [[MLImageManager sharedInstance] purgeCacheForContact:presenceNode.fromUser andAccount:_account.accountNo]; - [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ - @"contact": [MLContact createContactFromJid:presenceNode.fromUser andAccountNo:_account.accountNo] - }]; - DDLogInfo(@"Avatar of muc '%@' deleted successfully", presenceNode.fromUser); - } - else - DDLogInfo(@"Avatar hash '%@' of muc %@ did not change, not updating avatar...", avatarHash, presenceNode.fromUser); + DDLogInfo(@"Got new muc avatar hash '%@' for muc %@, fetching new image via vcard-temp...", avatarHash, presenceNode.fromUser); + [self fetchAvatarForRoom:presenceNode.fromUser]; + } + else if(avatarHash == nil && currentHash != nil && ![currentHash isEqualToString:@""]) + { + [[MLImageManager sharedInstance] setIconForContact:[MLContact createContactFromJid:presenceNode.fromUser andAccountNo:_account.accountNo] WithData:nil]; + [[DataLayer sharedInstance] setAvatarHash:@"" forContact:presenceNode.fromUser andAccount:_account.accountNo]; + //delete cache to make sure the image will be regenerated + [[MLImageManager sharedInstance] purgeCacheForContact:presenceNode.fromUser andAccount:_account.accountNo]; + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:presenceNode.fromUser andAccountNo:_account.accountNo] + }]; + DDLogInfo(@"Avatar of muc '%@' deleted successfully", presenceNode.fromUser); } else - DDLogInfo(@"Ignoring presence updates from %@, MUC not in buddylist", presenceNode.fromUser); + { + DDLogInfo(@"Avatar hash '%@' of muc %@ did not change, not updating avatar...", avatarHash, presenceNode.fromUser); + } } //handle reflected presences else @@ -302,22 +300,18 @@ -(void) processPresence:(XMPPPresence*) presenceNode item[@"jid"] = [HelperTools splitJid:item[@"jid"]][@"user"]; item[@"nick"] = presenceNode.fromResource; - //handle presences - if([presenceNode check:@"/"]) + //handle participant updates + if([presenceNode check:@"/"] || item[@"affiliation"] == nil) [[DataLayer sharedInstance] removeParticipant:item fromMuc:presenceNode.fromUser forAccountId:_account.accountNo]; else - { - if([[DataLayer sharedInstance] isContactInList:presenceNode.fromUser forAccount:_account.accountNo]) - { - if(item[@"jid"] != nil) - [self handleMembersListUpdate:presenceNode]; - [[DataLayer sharedInstance] addParticipant:item toMuc:presenceNode.fromUser forAccountId:_account.accountNo]; - } - else - DDLogInfo(@"Ignoring presence updates from %@, MUC not in buddylist", presenceNode.fromUser); - } + [[DataLayer sharedInstance] addParticipant:item toMuc:presenceNode.fromUser forAccountId:_account.accountNo]; + + //handle members updates + if(item[@"jid"] != nil) + [self handleMembersListUpdate:[presenceNode find:@"{http://jabber.org/protocol/muc#user}x/item@@"] forMuc:presenceNode.fromUser]; - //handle muc status codes in self-presences (after the above code, to make sure we are registered as participant in our db, too) + //handle muc status codes in reflected presences + //this MUST be done after the above code to make sure the db correctly reflects our membership/participant status if([presenceNode check:@"/{jabber:client}presence/{http://jabber.org/protocol/muc#user}x/status@code"]) [self handleStatusCodes:presenceNode]; } @@ -327,7 +321,7 @@ -(BOOL) processMessage:(XMPPMessage*) messageNode { //handle member list updates of offline members (useful for members-only mucs) if([messageNode check:@"{http://jabber.org/protocol/muc#user}x/item"]) - [self handleMembersListUpdate:messageNode]; + [self handleMembersListUpdate:[messageNode find:@"{http://jabber.org/protocol/muc#user}x/item@@"] forMuc:messageNode.fromUser]; //handle muc status codes [self handleStatusCodes:messageNode]; @@ -335,15 +329,43 @@ -(BOOL) processMessage:(XMPPMessage*) messageNode //handle mediated invites if([messageNode check:@"{http://jabber.org/protocol/muc#user}x/invite"]) { - DDLogInfo(@"Got mediated muc invite from %@ for %@ --> joining...", [messageNode findFirst:@"{http://jabber.org/protocol/muc#user}x/invite@from"], messageNode.fromUser); + //ignore outgoing carbon copies or mam results + if(![messageNode.toUser isEqualToString:_account.connectionProperties.identity.jid]) + return YES; //stop processing in MLMessageProcessor and ignore this invite + + NSString* invitedMucJid = [HelperTools splitJid:[messageNode findFirst:@"{http://jabber.org/protocol/muc#user}x/invite@from"]][@"user"]; + if(invitedMucJid == nil) + { + DDLogError(@"mediated inivite does not include a MUC jid, ignoring invite"); + return YES; + } + MLContact* inviteFrom = [MLContact createContactFromJid:invitedMucJid andAccountNo:_account.accountNo]; + DDLogInfo(@"Got mediated muc invite from %@ for %@...", inviteFrom, messageNode.fromUser); + if(!inviteFrom.isSubscribedFrom) + { + DDLogWarn(@"Ignoring invite from %@, this jid isn't at least marked as susbscribedFrom in our roster...", inviteFrom); + return YES; //don't process this further + } + DDLogInfo(@"--> joinging %@...", messageNode.fromUser); [self sendDiscoQueryFor:messageNode.fromUser withJoin:YES andBookmarksUpdate:YES]; return YES; //stop processing in MLMessageProcessor } - + //handle direct invites if([messageNode check:@"{jabber:x:conference}x@jid"] && [[messageNode findFirst:@"{jabber:x:conference}x@jid"] length] > 0) { - DDLogInfo(@"Got direct muc invite from %@ for %@ --> joining...", messageNode.fromUser, [messageNode findFirst:@"{jabber:x:conference}x@jid"]); + //ignore outgoing carbon copies or mam results + if(![messageNode.toUser isEqualToString:_account.connectionProperties.identity.jid]) + return YES; //stop processing in MLMessageProcessor and ignore this invite + + MLContact* inviteFrom = [MLContact createContactFromJid:messageNode.fromUser andAccountNo:_account.accountNo]; + DDLogInfo(@"Got direct muc invite from %@ for %@ --> joining...", inviteFrom, [messageNode findFirst:@"{jabber:x:conference}x@jid"]); + if(!inviteFrom.isSubscribedFrom) + { + DDLogWarn(@"Ignoring invite from %@, this jid isn't at least marked as susbscribedFrom in our roster...", inviteFrom); + return YES; //don't process this further + } + DDLogInfo(@"--> joinging %@...", [messageNode findFirst:@"{jabber:x:conference}x@jid"]); [self sendDiscoQueryFor:[messageNode findFirst:@"{jabber:x:conference}x@jid"] withJoin:YES andBookmarksUpdate:YES]; return YES; //stop processing in MLMessageProcessor } @@ -352,26 +374,32 @@ -(BOOL) processMessage:(XMPPMessage*) messageNode return NO; } --(void) handleMembersListUpdate:(XMPPStanza*) node +-(void) handleMembersListUpdate:(NSArray*) items forMuc:(NSString*) mucJid; { - if([[DataLayer sharedInstance] isContactInList:node.fromUser forAccount:_account.accountNo]) + //check if this is still a muc and ignore the members list update, if not + if([[DataLayer sharedInstance] isBuddyMuc:mucJid forAccount:_account.accountNo]) { - for(NSDictionary* entry in [node find:@"{http://jabber.org/protocol/muc#admin}query/item@@"]) + DDLogInfo(@"Handling members list update for %@: %@", mucJid, items); + for(NSDictionary* entry in items) { NSMutableDictionary* item = [entry mutableCopy]; if(!item || item[@"jid"] == nil) + { + DDLogDebug(@"Ignoring empty item/jid: %@", item); continue; //ignore empty items or items without a jid + } //update jid to be a bare jid item[@"jid"] = [HelperTools splitJid:item[@"jid"]][@"user"]; #ifndef DISABLE_OMEMO - BOOL isTypeGroup = [[[DataLayer sharedInstance] getMucTypeOfRoom:node.fromUser andAccount:_account.accountNo] isEqualToString:@"group"]; + BOOL isTypeGroup = [[[DataLayer sharedInstance] getMucTypeOfRoom:mucJid andAccount:_account.accountNo] isEqualToString:@"group"]; #endif - if([@"none" isEqualToString:item[@"affiliation"]]) + if(item[@"affiliation"] == nil || [@"none" isEqualToString:item[@"affiliation"]]) { - [[DataLayer sharedInstance] removeMember:item fromMuc:node.fromUser forAccountId:_account.accountNo]; + DDLogVerbose(@"Removing member '%@' from muc '%@'...", item[@"jid"], mucJid); + [[DataLayer sharedInstance] removeMember:item fromMuc:mucJid forAccountId:_account.accountNo]; #ifndef DISABLE_OMEMO if(isTypeGroup == YES) [_account.omemo checkIfSessionIsStillNeeded:item[@"jid"] isMuc:NO]; @@ -379,7 +407,8 @@ -(void) handleMembersListUpdate:(XMPPStanza*) node } else { - [[DataLayer sharedInstance] addMember:item toMuc:node.fromUser forAccountId:_account.accountNo]; + DDLogVerbose(@"Adding member '%@' to muc '%@'...", item[@"jid"], mucJid); + [[DataLayer sharedInstance] addMember:item toMuc:mucJid forAccountId:_account.accountNo]; #ifndef DISABLE_OMEMO if(isTypeGroup == YES) [_account.omemo subscribeAndFetchDevicelistIfNoSessionExistsForJid:item[@"jid"]]; @@ -388,15 +417,15 @@ -(void) handleMembersListUpdate:(XMPPStanza*) node } } else - DDLogInfo(@"Ignoring handleMembersListUpdate for %@, MUC not in buddylist", node.fromUser); + DDLogWarn(@"Ignoring handleMembersListUpdate for %@, MUC not in buddylist", mucJid); } --(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) mandatoryOptions andOptionalOptions:(NSDictionary*) optionalOptions deletingMucOnError:(BOOL) deleteOnError +-(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) mandatoryOptions andOptionalOptions:(NSDictionary*) optionalOptions deletingMucOnError:(BOOL) deleteOnError andJoiningMucOnSuccess:(BOOL) joinOnSuccess { DDLogInfo(@"Fetching room config form: %@", roomJid); XMPPIQ* configFetchNode = [[XMPPIQ alloc] initWithType:kiqGetType to:roomJid]; [configFetchNode setGetRoomConfig]; - [_account sendIq:configFetchNode withHandler:$newHandlerWithInvalidation(self, handleRoomConfigForm, handleRoomConfigFormInvalidation, $ID(roomJid), $ID(mandatoryOptions), $ID(optionalOptions), $BOOL(deleteOnError))]; + [_account sendIq:configFetchNode withHandler:$newHandlerWithInvalidation(self, handleRoomConfigForm, handleRoomConfigFormInvalidation, $ID(roomJid), $ID(mandatoryOptions), $ID(optionalOptions), $BOOL(deleteOnError), $BOOL(joinOnSuccess))]; } $$instance_handler(handleRoomConfigFormInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, roomJid), $$ID(NSDictionary*, mandatoryOptions), $$ID(NSDictionary*, optionalOptions), $$BOOL(deleteOnError)) @@ -411,7 +440,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Could fetch room config form for '%@': timeout", @""), roomJid] forMuc:roomJid withNode:nil andIsSevere:YES]; $$ -$$instance_handler(handleRoomConfigForm, account.mucProcessor, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, roomJid), $$ID(NSDictionary*, mandatoryOptions), $$ID(NSDictionary*, optionalOptions), $$BOOL(deleteOnError)) +$$instance_handler(handleRoomConfigForm, account.mucProcessor, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, roomJid), $$ID(NSDictionary*, mandatoryOptions), $$ID(NSDictionary*, optionalOptions), $$BOOL(deleteOnError), $$BOOL(joinOnSuccess)) MLAssert([iqNode.fromUser isEqualToString:roomJid], @"Room config form response jid not matching query jid!", (@{ @"iqNode.fromUser": [NSString stringWithFormat:@"%@", iqNode.fromUser], @"roomJid": [NSString stringWithFormat:@"%@", roomJid], @@ -440,11 +469,10 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Got empty room config form for '%@'", @""), roomJid] forMuc:roomJid withNode:nil andIsSevere:YES]; return; } - //these config options are mandatory and configure the room to be a group --> non anonymous, members only (and persistent) for(NSString* option in mandatoryOptions) { - if(!dataForm[option]) + if([dataForm getField:option] == nil) { DDLogError(@"Could not configure room '%@' to be a groupchat: config option '%@' not available!", roomJid, option); if(deleteOnError) @@ -470,9 +498,9 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma //reconfigure the room dataForm.type = @"submit"; - XMPPIQ* query = [[XMPPIQ alloc] initWithType:kiqSetType]; + XMPPIQ* query = [[XMPPIQ alloc] initWithType:kiqSetType to:roomJid]; [query setRoomConfig:dataForm]; - [_account sendIq:query withHandler:$newHandlerWithInvalidation(self, handleRoomConfigResult, handleRoomConfigResultInvalidation, $ID(roomJid), $ID(mandatoryOptions), $ID(optionalOptions), $BOOL(deleteOnError))]; + [_account sendIq:query withHandler:$newHandlerWithInvalidation(self, handleRoomConfigResult, handleRoomConfigResultInvalidation, $ID(roomJid), $ID(mandatoryOptions), $ID(optionalOptions), $BOOL(deleteOnError), $BOOL(joinOnSuccess))]; $$ $$instance_handler(handleRoomConfigResultInvalidation, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, roomJid), $$ID(NSDictionary*, mandatoryOptions), $$ID(NSDictionary*, optionalOptions), $$BOOL(deleteOnError)) @@ -487,11 +515,7 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Could not configure group '%@': timeout", @""), roomJid] forMuc:roomJid withNode:nil andIsSevere:YES]; $$ -$$instance_handler(handleRoomConfigResult, account.mucProcessor, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, roomJid), $$ID(NSDictionary*, mandatoryOptions), $$ID(NSDictionary*, optionalOptions), $$BOOL(deleteOnError)) - MLAssert([iqNode.fromUser isEqualToString:roomJid], @"Room config form response jid not matching query jid!", (@{ - @"iqNode.fromUser": [NSString stringWithFormat:@"%@", iqNode.fromUser], - @"roomJid": [NSString stringWithFormat:@"%@", roomJid], - })); +$$instance_handler(handleRoomConfigResult, account.mucProcessor, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, roomJid), $$ID(NSDictionary*, mandatoryOptions), $$ID(NSDictionary*, optionalOptions), $$BOOL(deleteOnError), $$BOOL(joinOnSuccess)) if([iqNode check:@"/"]) { DDLogError(@"Failed to submit room config form of '%@': %@", roomJid, [iqNode findFirst:@"error"]); @@ -503,47 +527,33 @@ -(void) configureMuc:(NSString*) roomJid withMandatoryOptions:(NSDictionary*) ma [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Could not configure group '%@'", @""), roomJid] forMuc:roomJid withNode:iqNode andIsSevere:YES]; return; } + MLAssert([iqNode.fromUser isEqualToString:roomJid], @"Room config form response jid not matching query jid!", (@{ + @"iqNode.fromUser": [NSString stringWithFormat:@"%@", iqNode.fromUser], + @"roomJid": [NSString stringWithFormat:@"%@", roomJid], + })); - //group is now properly configured and we are joined, but all the code handling a proper join was not run - //--> join again to make sure everything is sane - [self removeRoomFromCreating:roomJid]; - [self join:roomJid]; + if(joinOnSuccess) + { + //group is now properly configured and we are joined, but all the code handling a proper join was not run + //--> join again to make sure everything is sane + [self join:roomJid]; + } $$ -(void) handleStatusCodes:(XMPPStanza*) node { NSSet* presenceCodes = [[NSSet alloc] initWithArray:[node find:@"/{jabber:client}presence/{http://jabber.org/protocol/muc#user}x/status@code|int"]]; NSSet* messageCodes = [[NSSet alloc] initWithArray:[node find:@"/{jabber:client}message/{http://jabber.org/protocol/muc#user}x/status@code|int"]]; + NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:node.fromUser forAccount:_account.accountNo]; - //handle presence stanzas - if(presenceCodes && [presenceCodes count]) - { - NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:node.fromUser forAccount:_account.accountNo]; - for(NSNumber* code in presenceCodes) + //handle status codes allowed in presences AND messages + NSMutableSet* unhandledStatusCodes = [NSMutableSet new]; + NSMutableSet* jointCodes = [presenceCodes mutableCopy]; + [jointCodes unionSet:messageCodes]; + BOOL selfPrecenceHandled = NO; + for(NSNumber* code in jointCodes) switch([code intValue]) { - //room created and needs configuration now - case 201: - { - if(![presenceCodes containsObject:@110]) - { - DDLogError(@"Got 'muc needs configuration' status code (201) without self-presence, ignoring!"); - break; - } - if(![self isCreating:node.fromUser]) - { - DDLogError(@"Got 'muc needs configuration' status code (201) without this muc currently being created, ignoring!"); - break; - } - - //now configure newly created locked room - [self configureMuc:node.fromUser withMandatoryOptions:_mandatoryGroupConfigOptions andOptionalOptions:_optionalGroupConfigOptions deletingMucOnError:YES]; - - //stop processing here to not trigger the "successful join" code below - //we will trigger this code by a "second" join presence once the room was created and is not locked anymore - return; - break; - } //muc service changed our nick case 210: { @@ -553,25 +563,7 @@ -(void) handleStatusCodes:(XMPPStanza*) node //update nick in database DDLogInfo(@"Updating muc %@ nick in database to nick provided by server: '%@'...", node.fromUser, node.fromResource); [[DataLayer sharedInstance] updateOwnNickName:node.fromResource forMuc:node.fromUser forAccount:_account.accountNo]; - } - break; - } - //this is a self-presence (marking the end of the presence flood if we are in joining state) - case 110: - { - //check if we have joined already (we handle only non-joining self-presences here) - //joining self-presences are handled below - if(![self isJoining:node.fromUser]) - { - //muc got destroyed - if([node check:@"//{http://jabber.org/protocol/muc#user}x/destroy"]) - { - [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Group/Channel got destroyed: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; - [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; - } - else - ; //ignore other non-joining self-presences for now - break; + selfPrecenceHandled = YES; } break; } @@ -583,30 +575,57 @@ -(void) handleStatusCodes:(XMPPStanza*) node { DDLogDebug(@"got banned from room %@", node.fromUser); [self removeRoomFromJoining:node.fromUser]; - [self handleError:[NSString stringWithFormat:NSLocalizedString(@"You got banned from: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:NO]; + selfPrecenceHandled = YES; + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"You got banned from group/channel: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; } + break; } //kicked from room case 307: { - DDLogDebug(@"user '%@' got kicked from room %@", node.fromResource, node.fromUser); - if([nick isEqualToString:node.fromResource]) + /* + * To quote XEP-0045: + * Note: Some server implementations additionally include a 307 status code (signifying a 'kick', i.e. a forced ejection from the room). This is generally not advisable, as these types of disconnects may be frequent in the presence of poor network conditions and they are not linked to any user (e.g. moderator) action that the 307 code usually indicates. It is therefore recommended for the client to ignore the 307 code if a 333 status code is present. + */ + if(![jointCodes containsObject:@333]) { - DDLogDebug(@"got kicked from room %@", node.fromUser); - [self removeRoomFromJoining:node.fromUser]; - [self handleError:[NSString stringWithFormat:NSLocalizedString(@"You got kicked from: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + DDLogDebug(@"user '%@' got kicked from room %@", node.fromResource, node.fromUser); + if([nick isEqualToString:node.fromResource]) + { + DDLogDebug(@"got kicked from room %@", node.fromUser); + [self removeRoomFromJoining:node.fromUser]; + [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:NO]; + selfPrecenceHandled = YES; + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"You got kicked from group/channel: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + } } + else + DDLogWarn(@"Ignoring 307 status code because code 333 is present, too..."); + break; } - //removed because of affiliation change --> reenter room + //removed because of affiliation change case 321: { - DDLogDebug(@"user '%@' got affiliation changed for room %@", node.fromResource, node.fromUser); - if([nick isEqualToString:node.fromResource]) + //only handle this and rejoin, if we did not get removed from a members-only room + if(![jointCodes containsObject:@322]) { - DDLogDebug(@"got affiliation change for room %@", node.fromUser); - [self removeRoomFromJoining:node.fromUser]; - [self sendDiscoQueryFor:node.fromUser withJoin:YES andBookmarksUpdate:YES]; + DDLogDebug(@"user '%@' got affiliation changed for room %@", node.fromResource, node.fromUser); + if([nick isEqualToString:node.fromResource]) + { + DDLogDebug(@"got own affiliation change for room %@", node.fromUser); + //check if we are still in the room (e.g. loss of membership status in public channel or admin to member degradation) + if([[DataLayer sharedInstance] getParticipantForNick:node.fromResource inRoom:node.fromUser forAccountId:_account.accountNo] == nil) + { + DDLogInfo(@"Got removed from room..."); + [self removeRoomFromJoining:node.fromUser]; + [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; + selfPrecenceHandled = YES; + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"You got removed from group/channel: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + } + } } + break; } //removed because room is now members only (an we are not a member) case 322: @@ -616,9 +635,11 @@ -(void) handleStatusCodes:(XMPPStanza*) node { DDLogDebug(@"got removed from members-only room %@", node.fromUser); [self removeRoomFromJoining:node.fromUser]; - [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Kicked, because muc is now members-only: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Kicked, because group/channel is now members-only: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; + selfPrecenceHandled = YES; } + break; } //removed because of system shutdown case 332: @@ -628,96 +649,164 @@ -(void) handleStatusCodes:(XMPPStanza*) node { DDLogDebug(@"got removed from room %@ because of system shutdown", node.fromUser); [self removeRoomFromJoining:node.fromUser]; - [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Kicked, because of system shutdown: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + selfPrecenceHandled = YES; + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Kicked from group/channel, because of system shutdown: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; } + break; } default: - DDLogWarn(@"Got unhandled muc status code in presence from %@: %@", node.from, code); - } - - //this is a self-presence marking the end of the presence flood, handle this code last because it resets _joining - if([presenceCodes containsObject:@110] && [self isJoining:node.fromUser]) - { - DDLogInfo(@"Successfully joined muc %@...", node.fromUser); - - //we are joined now, remove from joining list - [self removeRoomFromJoining:node.fromUser]; - - //we joined successfully --> add muc to our favorites (this will use the already up to date nick from buddylist db table) - //and update bookmarks if this was the first time we joined this muc - [[DataLayer sharedInstance] addMucFavorite:node.fromUser forAccountId:_account.accountNo andMucNick:nil]; - @synchronized(_stateLockObject) { - DDLogVerbose(@"_firstJoin set: %@\n_noUpdateBookmarks set: %@", _firstJoin, _noUpdateBookmarks); - //only update bookmarks on first join AND if not requested otherwise (batch join etc.) - if([_firstJoin containsObject:node.fromUser] && ![_noUpdateBookmarks containsObject:node.fromUser]) - [self updateBookmarks]; - [_firstJoin removeObject:node.fromUser]; - [_noUpdateBookmarks removeObject:node.fromUser]; + [unhandledStatusCodes addObject:code]; } - - [self logMembersOfMuc:node.fromUser]; - - //load members/admins/owners list (this has to be done *after* joining the muc to not get auth errors) - DDLogInfo(@"Querying member/admin/owner lists for muc %@...", node.fromUser); - for(NSString* type in @[@"member", @"admin", @"owner"]) + + //handle presence stanzas + if(presenceCodes && [presenceCodes count]) + { + for(NSNumber* code in presenceCodes) + switch([code intValue]) { - XMPPIQ* discoInfo = [[XMPPIQ alloc] initWithType:kiqGetType to:node.fromUser]; - [discoInfo setMucListQueryFor:type]; - [_account sendIq:discoInfo withHandler:$newHandler(self, handleMembersList, $ID(type))]; + //room created and needs configuration now + case 100: + DDLogVerbose(@"This room is non-anonymous: everybody can see all jids..."); + break; + case 110: + break; //ignore self-presence status handled below + case 201: + { + if(![presenceCodes containsObject:@110]) + { + DDLogError(@"Got 'muc needs configuration' status code (201) without self-presence, ignoring!"); + break; + } + if(![self isCreating:node.fromUser]) + { + DDLogError(@"Got 'muc needs configuration' status code (201) without this muc currently being created, ignoring: %@", node.fromUser); + break; + } + + //now configure newly created locked room + [self configureMuc:node.fromUser withMandatoryOptions:_mandatoryGroupConfigOptions andOptionalOptions:_optionalGroupConfigOptions deletingMucOnError:YES andJoiningMucOnSuccess:YES]; + + //stop processing here to not trigger the "successful join" code below + //we will trigger this code by a "second" join presence once the room was created and is not locked anymore + return; + break; + } + default: + //only log errors for status codes not already handled by our joint handling above + if([unhandledStatusCodes containsObject:code]) + DDLogWarn(@"Got unhandled muc status code in presence from %@: %@", node.from, code); + break; } - - monal_id_block_t uiHandler = [self getUIHandlerForMuc:node.fromUser]; - if(uiHandler) + + //this is a self-presence (marking the end of the presence flood if we are in joining state) + //handle this code last because it may reset _joining + if([presenceCodes containsObject:@110] && !selfPrecenceHandled) + { + //check if we have joined already (we handle only non-joining self-presences here) + //joining self-presences are handled below + if(![self isJoining:node.fromUser]) { - DDLogInfo(@"Calling UI handler for muc %@...", node.fromUser); - dispatch_async(dispatch_get_main_queue(), ^{ - uiHandler(@{ - @"success": @YES, - @"muc": node.fromUser, - @"account": self->_account - }); - }); - } - - //MAYBE TODO: send out notification indicating we joined that room - - //query muc-mam for new messages - BOOL supportsMam = NO; - @synchronized(_stateLockObject) { - if(_roomFeatures[node.fromUser] && [_roomFeatures[node.fromUser] containsObject:@"urn:xmpp:mam:2"]) - supportsMam = YES; + DDLogInfo(@"Got non-joining muc presence for %@...", node.fromUser); + + //handle muc destroys, but ignore other non-joining self-presences for now + //(normally these have an additional status code that was already handled in the switch statement above + if([node check:@"//{http://jabber.org/protocol/muc#user}x/destroy"]) + { + [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Group/Channel got destroyed: %@", @""), node.fromUser] forMuc:node.fromUser withNode:node andIsSevere:YES]; + [self deleteMuc:node.fromUser withBookmarksUpdate:YES keepBuddylistEntry:YES]; + } } - if(supportsMam) + else { - DDLogInfo(@"Muc %@ supports mam:2...", node.fromUser); + DDLogInfo(@"Successfully joined muc %@...", node.fromUser); + + //we are joined now, remove from joining list + [self removeRoomFromJoining:node.fromUser]; + + //we joined successfully --> add muc to our favorites (this will use the already up to date nick from buddylist db table) + //and update bookmarks if this was the first time we joined this muc + [[DataLayer sharedInstance] addMucFavorite:node.fromUser forAccountId:_account.accountNo andMucNick:nil]; + @synchronized(_stateLockObject) { + DDLogVerbose(@"_firstJoin set: %@\n_noUpdateBookmarks set: %@", _firstJoin, _noUpdateBookmarks); + //only update bookmarks on first join AND if not requested otherwise (batch join etc.) + if([_firstJoin containsObject:node.fromUser] && ![_noUpdateBookmarks containsObject:node.fromUser]) + [self updateBookmarks]; + [_firstJoin removeObject:node.fromUser]; + [_noUpdateBookmarks removeObject:node.fromUser]; + } + + DDLogDebug(@"Updating muc contact..."); + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": [MLContact createContactFromJid:node.fromUser andAccountNo:_account.accountNo] + }]; + + [self logMembersOfMuc:node.fromUser]; + + //load members/admins/owners list (this has to be done *after* joining the muc to not get auth errors) + DDLogInfo(@"Querying member/admin/owner lists for muc %@...", node.fromUser); + for(NSString* type in @[@"member", @"admin", @"owner"]) + { + XMPPIQ* discoInfo = [[XMPPIQ alloc] initWithType:kiqGetType to:node.fromUser]; + [discoInfo setMucListQueryFor:type]; + [_account sendIq:discoInfo withHandler:$newHandler(self, handleMembersList, $ID(type))]; + } - //query mam since last received stanza ID because we could not resume the smacks session - //(we would not have landed here if we were able to resume the smacks session) - //this will do a catchup of everything we might have missed since our last connection - //we possibly receive sent messages, too (this will update the stanzaid in database and gets deduplicate by messageid, - //which is guaranteed to be unique (because monal uses uuids for outgoing messages) - NSString* lastStanzaId = [[DataLayer sharedInstance] lastStanzaIdForMuc:node.fromUser andAccount:_account.accountNo]; - [_account delayIncomingMessageStanzasForArchiveJid:node.fromUser]; - XMPPIQ* mamQuery = [[XMPPIQ alloc] initWithType:kiqSetType to:node.fromUser]; - if(lastStanzaId) + monal_id_block_t uiHandler = [self getUIHandlerForMuc:node.fromUser]; + if(uiHandler) { - DDLogInfo(@"Querying muc mam:2 archive after stanzaid '%@' for catchup", lastStanzaId); - [mamQuery setMAMQueryAfter:lastStanzaId]; - [_account sendIq:mamQuery withHandler:$newHandler(self, handleCatchup, $BOOL(secondTry, NO))]; + //remove handler (it will only be called once) + [self removeUIHandlerForMuc:node.fromUser]; + + DDLogInfo(@"Calling UI handler for muc %@...", node.fromUser); + dispatch_async(dispatch_get_main_queue(), ^{ + uiHandler(@{ + @"success": @YES, + @"muc": node.fromUser, + @"account": self->_account + }); + }); + } + + //MAYBE TODO: send out notification indicating we joined that room + + //query muc-mam for new messages + BOOL supportsMam = NO; + @synchronized(_stateLockObject) { + if(_roomFeatures[node.fromUser] && [_roomFeatures[node.fromUser] containsObject:@"urn:xmpp:mam:2"]) + supportsMam = YES; } - else + if(supportsMam) { - DDLogInfo(@"Querying muc mam:2 archive for latest stanzaid to prime database"); - [mamQuery setMAMQueryForLatestId]; - [_account sendIq:mamQuery withHandler:$newHandler(self, handleMamResponseWithLatestId)]; + DDLogInfo(@"Muc %@ supports mam:2...", node.fromUser); + + //query mam since last received stanza ID because we could not resume the smacks session + //(we would not have landed here if we were able to resume the smacks session) + //this will do a catchup of everything we might have missed since our last connection + //we possibly receive sent messages, too (this will update the stanzaid in database and gets deduplicate by messageid, + //which is guaranteed to be unique (because monal uses uuids for outgoing messages) + NSString* lastStanzaId = [[DataLayer sharedInstance] lastStanzaIdForMuc:node.fromUser andAccount:_account.accountNo]; + [_account delayIncomingMessageStanzasForArchiveJid:node.fromUser]; + XMPPIQ* mamQuery = [[XMPPIQ alloc] initWithType:kiqSetType to:node.fromUser]; + if(lastStanzaId) + { + DDLogInfo(@"Querying muc mam:2 archive after stanzaid '%@' for catchup", lastStanzaId); + [mamQuery setMAMQueryAfter:lastStanzaId]; + [_account sendIq:mamQuery withHandler:$newHandler(self, handleCatchup, $BOOL(secondTry, NO))]; + } + else + { + DDLogInfo(@"Querying muc mam:2 archive for latest stanzaid to prime database"); + [mamQuery setMAMQueryForLatestId]; + [_account sendIq:mamQuery withHandler:$newHandler(self, handleMamResponseWithLatestId)]; + } } + + //we don't need to force saving of our new state because once this incoming presence gets counted by smacks the whole state will be saved } - - //we don't need to force saving of our new state because once this incoming presence gets counted by smacks the whole state will be saved } } //handle message stanzas - else if([[node findFirst:@"/@type"] isEqualToString:@"groupchat"] && messageCodes && [messageCodes count]) + else if(messageCodes && [messageCodes count]) { for(NSNumber* code in messageCodes) switch([code intValue]) @@ -742,20 +831,30 @@ -(void) handleStatusCodes:(XMPPStanza*) node break; } default: - DDLogWarn(@"Got unhandled muc status code in message from %@: %@", node.from, code); + //only log errors for status codes not already handled by our joint handling above + if([unhandledStatusCodes containsObject:code]) + DDLogWarn(@"Got unhandled muc status code in message from %@: %@", node.from, code); + break; } } } $$instance_handler(handleCreateTimeout, account.mucProcessor, $$ID(xmpp*, account), $$ID(NSString*, room)) + if(![self isCreating:room]) + { + DDLogError(@"Got room create idle timeout but not creating group, ignoring: %@", room); + return; + } [self removeRoomFromCreating:room]; [self deleteMuc:room withBookmarksUpdate:NO keepBuddylistEntry:NO]; - [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Could fetch room config form from '%@': timeout", @""), room] forMuc:room withNode:nil andIsSevere:YES]; [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Could not create group '%@': timeout", @""), room] forMuc:room withNode:nil andIsSevere:YES]; $$ --(NSString*) createGroup:(NSString*) node +-(NSString* _Nullable) createGroup:(NSString* _Nullable) node { + if(node == nil) + node = [self generateSpeakableGroupNode]; + node = [node stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet].lowercaseString; NSString* room = [[NSString stringWithFormat:@"%@@%@", node, _account.connectionProperties.conferenceServer] lowercaseString]; if([[DataLayer sharedInstance] isBuddyMuc:room forAccount:_account.accountNo]) { @@ -799,7 +898,7 @@ -(void) join:(NSString*) room [self sendDiscoQueryFor:room withJoin:YES andBookmarksUpdate:YES]; } --(void) leave:(NSString*) room withBookmarksUpdate:(BOOL) updateBookmarks +-(void) leave:(NSString*) room withBookmarksUpdate:(BOOL) updateBookmarks keepBuddylistEntry:(BOOL) keepBuddylistEntry { room = [room lowercaseString]; NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:_account.accountNo]; @@ -822,7 +921,7 @@ -(void) leave:(NSString*) room withBookmarksUpdate:(BOOL) updateBookmarks [_account send:presence]; //delete muc from favorites table and update bookmarks if requested - [self deleteMuc:room withBookmarksUpdate:updateBookmarks keepBuddylistEntry:NO]; + [self deleteMuc:room withBookmarksUpdate:updateBookmarks keepBuddylistEntry:keepBuddylistEntry]; } -(void) sendDiscoQueryFor:(NSString*) roomJid withJoin:(BOOL) join andBookmarksUpdate:(BOOL) updateBookmarks @@ -859,8 +958,8 @@ -(void) pingAllMucs DDLogInfo(@"Not pinging all mucs, last ping was less than %d seconds ago: %@", MUC_PING, _lastPing); return; } - for(NSDictionary* entry in [[DataLayer sharedInstance] listMucsForAccount:_account.accountNo]) - [self ping:entry[@"room"] withLastPing:_lastPing]; + for(NSString* room in [[DataLayer sharedInstance] listMucsForAccount:_account.accountNo]) + [self ping:room withLastPing:_lastPing]; _lastPing = [NSDate date]; } @@ -944,6 +1043,25 @@ -(void) ping:(NSString*) roomJid withLastPing:(NSDate* _Nullable) lastPing }]; } +-(void) inviteUser:(NSString*) jid inMuc:(NSString*) roomJid +{ + DDLogInfo(@"Inviting user '%@' to '%@' directly & indirectly", jid, roomJid); + + XMPPMessage* indirectInviteMsg = [[XMPPMessage alloc] initWithType:kMessageNormalType to:roomJid]; + [indirectInviteMsg addChildNode:[[MLXMLNode alloc] initWithElement:@"x" andNamespace:@"http://jabber.org/protocol/muc#user" withAttributes:@{} andChildren:@[ + [[MLXMLNode alloc] initWithElement:@"invite" withAttributes:@{ + @"to": jid + } andChildren:@[] andData:nil] + ] andData:nil]]; + [_account send:indirectInviteMsg]; + + XMPPMessage* directInviteMsg = [[XMPPMessage alloc] initWithType:kMessageNormalType to:jid]; + [directInviteMsg addChildNode:[[MLXMLNode alloc] initWithElement:@"x" andNamespace:@"jabber:x:conference" withAttributes:@{ + @"jid": roomJid + } andChildren:@[] andData:nil]]; + [_account send:directInviteMsg]; +} + -(void) setAffiliation:(NSString*) affiliation ofUser:(NSString*) jid inMuc:(NSString*) roomJid { DDLogInfo(@"Changing affiliation of '%@' in '%@' to '%@'", jid, roomJid, affiliation); @@ -964,14 +1082,14 @@ -(void) setAffiliation:(NSString*) affiliation ofUser:(NSString*) jid inMuc:(NSS [self handleError:[NSString stringWithFormat:NSLocalizedString(@"Failed to change affiliation of '%@' in '%@' to '%@'", @""), jid, roomJid, affiliation] forMuc:roomJid withNode:iqNode andIsSevere:YES]; return; } - DDLogError(@"Successfully changed affiliation of '%@' in '%@' to '%@'", jid, roomJid, affiliation); + DDLogInfo(@"Successfully changed affiliation of '%@' in '%@' to '%@'", jid, roomJid, affiliation); $$ -(void) changeNameOfMuc:(NSString*) room to:(NSString*) name { [self configureMuc:room withMandatoryOptions:@{ @"muc#roomconfig_roomname": name, - } andOptionalOptions:@{} deletingMucOnError:NO]; + } andOptionalOptions:@{} deletingMucOnError:NO andJoiningMucOnSuccess:NO]; } -(void) changeSubjectOfMuc:(NSString*) room to:(NSString*) subject @@ -1027,6 +1145,13 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room @"roomJid": [NSString stringWithFormat:@"%@", roomJid], })); + //no matter what the disco response is: we are not creating this muc anymore + //either because we successfully created it and called join afterwards, + //or because the user tried to simultaneously create and join this muc (the join has precendence in this case) + BOOL wasCreating = [self isCreating:roomJid]; + [self removeRoomFromCreating:roomJid]; + + if([iqNode check:@"//error/{urn:ietf:params:xml:ns:xmpp-stanzas}gone"]) { DDLogError(@"Querying muc info returned this muc isn't available anymore: %@", [iqNode findFirst:@"error"]); @@ -1108,15 +1233,19 @@ -(void) publishAvatar:(UIImage* _Nullable) image forMuc:(NSString*) room mucType = @"group"; //update db with new infos - if(![[DataLayer sharedInstance] isBuddyMuc:iqNode.fromUser forAccount:_account.accountNo]) + BOOL isBuddyMuc = [[DataLayer sharedInstance] isBuddyMuc:iqNode.fromUser forAccount:_account.accountNo]; + if(!isBuddyMuc || wasCreating) { - //remove old non-muc contact from contactlist (we don't want mucs as normal contacts on our (server) roster and shadowed in monal by the real muc contact) - NSDictionary* existingContactDict = [[DataLayer sharedInstance] contactDictionaryForUsername:iqNode.fromUser forAccount:_account.accountNo]; - if(existingContactDict != nil) + if(!isBuddyMuc) { - MLContact* existingContact = [MLContact createContactFromJid:iqNode.fromUser andAccountNo:_account.accountNo]; - DDLogVerbose(@"Removing already existing contact (%@) having raw db dict: %@", existingContact, existingContactDict); - [_account removeFromRoster:existingContact]; + //remove old non-muc contact from contactlist (we don't want mucs as normal contacts on our (server) roster and shadowed in monal by the real muc contact) + NSDictionary* existingContactDict = [[DataLayer sharedInstance] contactDictionaryForUsername:iqNode.fromUser forAccount:_account.accountNo]; + if(existingContactDict != nil) + { + MLContact* existingContact = [MLContact createContactFromJid:iqNode.fromUser andAccountNo:_account.accountNo]; + DDLogVerbose(@"Removing already existing contact (%@) having raw db dict: %@", existingContact, existingContactDict); + [_account removeFromRoster:existingContact]; + } } //add new muc buddy (potentially deleting a non-muc buddy having the same jid) NSString* nick = [self calculateNickForMuc:iqNode.fromUser]; @@ -1195,7 +1324,7 @@ -(void) sendJoinPresenceFor:(NSString*) room $$instance_handler(handleMembersList, account.mucProcessor, $$ID(xmpp*, account), $$ID(XMPPIQ*, iqNode), $$ID(NSString*, type)) DDLogInfo(@"Got %@s list from %@...", type, iqNode.fromUser); - [self handleMembersListUpdate:iqNode]; + [self handleMembersListUpdate:[iqNode find:@"{http://jabber.org/protocol/muc#admin}query/item@@"] forMuc:iqNode.fromUser]; [self logMembersOfMuc:iqNode.fromUser]; $$ @@ -1379,8 +1508,8 @@ -(void) updateBookmarks -(BOOL) checkIfStillBookmarked:(NSString*) room { room = [room lowercaseString]; - for(NSDictionary* entry in [[DataLayer sharedInstance] listMucsForAccount:_account.accountNo]) - if([room isEqualToString:[entry[@"room"] lowercaseString]]) + for(NSString* entry in [[DataLayer sharedInstance] listMucsForAccount:_account.accountNo]) + if([room isEqualToString:entry]) return YES; return NO; } @@ -1394,22 +1523,23 @@ -(void) deleteMuc:(NSString*) room withBookmarksUpdate:(BOOL) updateBookmarks ke { DDLogInfo(@"Deleting muc %@ on account %@...", room, _account); - //create contact object before deleting it to retain the muc specific settings - //(after deleting it from our db, the contact for this jid will look like an ordinary 1:1 contact) - MLContact* contact = [MLContact createContactFromJid:room andAccountNo:_account.accountNo]; - //delete muc from favorites table and update bookmarks if requested [[DataLayer sharedInstance] deleteMuc:room forAccountId:_account.accountNo]; if(updateBookmarks) [self updateBookmarks]; //update buddylist (e.g. contact list) if requested + MLContact* contact = [MLContact createContactFromJid:room andAccountNo:_account.accountNo]; + [contact removeShareInteractions]; if(keepBuddylistEntry) - ; //TODO: mark entry as destroyed to be displayed in the ui + { + [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:_account userInfo:@{ + @"contact": contact + }]; + } else { [[DataLayer sharedInstance] removeBuddy:room forAccount:_account.accountNo]; - [contact removeShareInteractions]; [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRemoved object:_account userInfo:@{ @"contact": contact }]; @@ -1459,4 +1589,20 @@ -(void) logMembersOfMuc:(NSString*) jid } } +-(NSString*) generateSpeakableGroupNode +{ + NSArray* charLists = @[ + @"bcdfghjklmnpqrstvwxyz", + @"aeiou", + ]; + NSMutableString* retval = [NSMutableString new]; + int charTypeBegin = arc4random() % charLists.count; + for(int i=0; i<10; i++) + { + NSString* selectedCharList = charLists[(i + charTypeBegin) % charLists.count]; + [retval appendString:[selectedCharList substringWithRange:NSMakeRange(arc4random() % selectedCharList.length, 1)]]; + } + return retval; +} + @end diff --git a/Monal/Classes/MLOMEMO.m b/Monal/Classes/MLOMEMO.m index c0c8de1604..d64005b3cf 100644 --- a/Monal/Classes/MLOMEMO.m +++ b/Monal/Classes/MLOMEMO.m @@ -195,51 +195,72 @@ -(void) handleCatchupDone:(NSNotification*) notification self.state.catchupDone = YES; //if we did not see our own devicelist until now that means the server does not have any devicelist stored - //(e.g. we are the first omemo capable client) - //--> publish devicelist by faking an empty server-sent devicelist - //self.state.hasSeenDeviceList will be set to YES once the published devicelist gets returned to us by a pubsub headline echo - //(e.g. once the devicelist was safely stored on our server) + //OR: our own devicelist could have been delayed by the server having to do a disco query to us to discover our +notify + //for the devicelist (e.g. either we are the first omemo capable client, or the devicelist has just been delayed) + //--> forcefully fetch devicelist to be sure (but don't subscribe, we are +notify and have a presence subscription to our own account) + //If our device is not listed in this devicelist node, that fetch and the headline push eventually coming in + //may both trigger a devicelist publish, but that should not do any harm if(self.state.hasSeenDeviceList == NO) { - DDLogInfo(@"We did not see any devicelist during catchup since last non-smacks-resume reconnect, adding our device to an otherwise empty devicelist and publishing this list..."); - [self processOMEMODevices:[NSSet new] from:self.account.connectionProperties.identity.jid]; + DDLogInfo(@"We did not see any devicelist during catchup since last non-smacks-resume reconnect, forcefully fetching own devicelist..."); + [self queryOMEMODevices:self.account.connectionProperties.identity.jid withSubscribe:NO]; } else { - //generate new prekeys if needed and publish them - [self generateNewKeysIfNeeded]; + [self generateNewKeysIfNeeded]; //generate new prekeys if needed and publish them + [self repairQueuedSessions]; } - - //send all needed key transport elements now (added by incoming catchup messages or bundle fetches) - //the queue is needed to make sure we won't send multiple key transport messages to a single contact/device - //only because we received multiple messages from this user in the catchup or fetched multiple bundles - //queuedKeyTransportElements will survive any smacks or non-smacks resumptions and eventually trigger key transport elements - //once the catchup could be finished (could take several smacks resumptions to finish the whole (mam) catchup) - //has to be synchronized because [xmpp sendMessage:] could be called from main thread - @synchronized(self.state.queuedKeyTransportElements) { - DDLogDebug(@"Replaying queuedKeyTransportElements for all jids: %@", self.state.queuedKeyTransportElements); - for(NSString* jid in [self.state.queuedKeyTransportElements allKeys]) - [self retriggerKeyTransportElementsForJid:jid]; - } - - //handle all broken sessions now (e.g. reestablish them by fetching their bundles and sending key transport elements afterwards) - //the code handling the fetched bundle will check for an entry in queuedSessionRepairs and send - //a key transport element if such an entry can be found - //it removes the entry in queuedSessionRepairs afterwards, so no need to remove it here - //queuedSessionRepairs will survive a non-smacks relogin and trigger these dropped bundle fetches again to complete them - //has to be synchronized because [xmpp sendMessage:] could be called from main thread - @synchronized(self.state.queuedSessionRepairs) { - DDLogDebug(@"Replaying queuedSessionRepairs: %@", self.state.queuedSessionRepairs); - for(NSString* jid in self.state.queuedSessionRepairs) - for(NSNumber* rid in self.state.queuedSessionRepairs[jid]) - [self queryOMEMOBundleFrom:jid andDevice:rid]; - } - - DDLogVerbose(@"New state: %@", self.state); } #endif } +-(void) handleOwnDevicelistFetchError +{ + //devicelist could neither be fetched explicitly nor by using +notify --> publish own devicelist by faking an empty server-sent devicelist + //self.state.hasSeenDeviceList will be set to YES once the published devicelist gets returned to us by a pubsub headline echo + //(e.g. once the devicelist was safely stored on our server) + DDLogInfo(@"Could not fetch own devicelist, faking empty devicelist to publish our own deviceid..."); + [self processOMEMODevices:[NSSet new] from:self.account.connectionProperties.identity.jid]; + + [self repairQueuedSessions]; +} + +-(void) repairQueuedSessions +{ + DDLogInfo(@"Own devicelist was handled, now trying to repair queued sessions..."); + + //send all needed key transport elements now (added by incoming catchup messages or bundle fetches) + //the queue is needed to make sure we won't send multiple key transport messages to a single contact/device + //only because we received multiple messages from this user in the catchup or fetched multiple bundles + //queuedKeyTransportElements will survive any smacks or non-smacks resumptions and eventually trigger key transport elements + //once the catchup could be finished (could take several smacks resumptions to finish the whole (mam) catchup) + //has to be synchronized because [xmpp sendMessage:] could be called from main thread + @synchronized(self.state.queuedKeyTransportElements) { + DDLogDebug(@"Replaying queuedKeyTransportElements for all jids: %@", self.state.queuedKeyTransportElements); + for(NSString* jid in [self.state.queuedKeyTransportElements allKeys]) + [self retriggerKeyTransportElementsForJid:jid]; + } + + //handle all broken sessions now (e.g. reestablish them by fetching their bundles and sending key transport elements afterwards) + //the code handling the fetched bundle will check for an entry in queuedSessionRepairs and send + //a key transport element if such an entry can be found + //it removes the entry in queuedSessionRepairs afterwards, so no need to remove it here + //queuedSessionRepairs will survive a non-smacks relogin and trigger these dropped bundle fetches again to complete them + //has to be synchronized because [xmpp sendMessage:] could be called from main thread + @synchronized(self.state.queuedSessionRepairs) { + DDLogDebug(@"Replaying queuedSessionRepairs: %@", self.state.queuedSessionRepairs); + for(NSString* jid in self.state.queuedSessionRepairs) + for(NSNumber* rid in self.state.queuedSessionRepairs[jid]) + [self queryOMEMOBundleFrom:jid andDevice:rid]; + } + + //check bundle fetch status and inform ui if we are now catchupDone *and* all bundles are fetched + //(this method is only called by the catchupDone handler above or by the devicelist fetch triggered by the catchupDone handler) + [self checkBundleFetchCount]; + + DDLogVerbose(@"New state: %@", self.state); +} + -(void) retriggerKeyTransportElementsForJid:(NSString*) jid { //send all needed key transport elements now (added by incoming catchup messages or bundle fetches) @@ -294,7 +315,7 @@ -(void) retriggerKeyTransportElementsForJid:(NSString*) jid } $$ --(void) queryOMEMODevices:(NSString*) jid +-(void) queryOMEMODevices:(NSString*) jid withSubscribe:(BOOL) subscribe { //don't fetch devicelist twice (could be triggered by multiple useractions in a row) if([self.state.openDevicelistFetches containsObject:jid]) @@ -302,7 +323,7 @@ -(void) queryOMEMODevices:(NSString*) jid else { //fetch newest devicelist (this is needed even after a subscribe on at least prosody) - [self.account.pubsub fetchNode:@"eu.siacs.conversations.axolotl.devicelist" from:jid withItemsList:nil andHandler:$newHandlerWithInvalidation(self, handleDevicelistFetch, handleDevicelistFetchInvalidation, $BOOL(subscribe, YES))]; + [self.account.pubsub fetchNode:@"eu.siacs.conversations.axolotl.devicelist" from:jid withItemsList:nil andHandler:$newHandlerWithInvalidation(self, handleDevicelistFetch, handleDevicelistFetchInvalidation, $BOOL(subscribe))]; } } @@ -328,11 +349,14 @@ -(void) queryOMEMODevices:(NSString*) jid //mark devicelist fetch as done [self.state.openDevicelistFetches removeObject:jid]; + [self handleOwnDevicelistFetchError]; + //retrigger queued key transport elements for this jid (if any) [self retriggerKeyTransportElementsForJid:jid]; $$ $$instance_handler(handleDevicelistFetch, account.omemo, $$ID(xmpp*, account), $$BOOL(subscribe), $$ID(NSString*, jid), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $_ID((NSDictionary*), data)) + //mark devicelist fetch as done [self.state.openDevicelistFetches removeObject:jid]; if(success == NO) @@ -341,11 +365,16 @@ -(void) queryOMEMODevices:(NSString*) jid DDLogError(@"Error while fetching omemo devices: jid: %@ - %@", jid, errorIq); else DDLogError(@"Error while fetching omemo devices: jid: %@ - %@", jid, errorReason); - // TODO: improve error handling + if([self.account.connectionProperties.identity.jid isEqualToString:jid]) + [self handleOwnDevicelistFetchError]; + else + { + // TODO: improve error handling + } } else { - if(subscribe) + if(subscribe && ![self.account.connectionProperties.identity.jid isEqualToString:jid]) { DDLogInfo(@"Successfully fetched devicelist, now subscribing to this node for updates..."); //don't subscribe devicelist twice (could be triggered by multiple useractions in a row) @@ -367,23 +396,23 @@ -(void) queryOMEMODevices:(NSString*) jid if(publishedDevices) { - NSArray* deviceIds = [publishedDevices find:@"{eu.siacs.conversations.axolotl}list/device@id|uint"]; - NSSet* deviceSet = [[NSSet alloc] initWithArray:deviceIds]; - + NSSet* deviceSet = [[NSSet alloc] initWithArray:[publishedDevices find:@"{eu.siacs.conversations.axolotl}list/device@id|uint"]]; [self processOMEMODevices:deviceSet from:jid]; } } - //retrigger queued key transport elements for this jid (if any) - [self retriggerKeyTransportElementsForJid:jid]; + if([self.account.connectionProperties.identity.jid isEqualToString:jid]) + [self repairQueuedSessions]; //now try to repair all broken sessions (our catchup is now really done) + else + [self retriggerKeyTransportElementsForJid:jid]; //retrigger queued key transport elements for this jid (if any) $$ -(void) processOMEMODevices:(NSSet*) receivedDevices from:(NSString*) source { DDLogVerbose(@"Processing omemo devices from %@: %@", source, receivedDevices); - NSMutableSet* existingDevices = [NSMutableSet setWithArray:[self.monalSignalStore knownDevicesForAddressName:source]]; + NSMutableSet* existingDevices = [[self knownDevicesForAddressName:source] mutableCopy]; // ensure that we refetch bundles of devices with broken bundles again after some time NSSet* existingDevicesReqPendingFetch = [NSSet setWithArray:[self.monalSignalStore knownDevicesWithPendingBrokenSessionHandling:source]]; [existingDevices minusSet:existingDevicesReqPendingFetch]; @@ -437,23 +466,27 @@ -(void) processOMEMODevices:(NSSet*) receivedDevices from:(NSString*) -(void) handleOwnDevicelistUpdate:(NSSet*) receivedDevices { //check for new deviceids not previously known, but only if the devicelist is not empty - NSMutableSet* newDevices = [receivedDevices mutableCopy]; - [newDevices minusSet:self.ownDeviceList]; - for(NSNumber* device in newDevices) - if([device unsignedIntValue] != self.monalSignalStore.deviceid) - { - DDLogWarn(@"Got new deviceid %@ for own account %@", device, self.account.connectionProperties.identity.jid); - UNMutableNotificationContent* content = [UNMutableNotificationContent new]; - content.title = NSLocalizedString(@"New omemo device", @"");; - content.subtitle = self.account.connectionProperties.identity.jid; - content.body = [NSString stringWithFormat:NSLocalizedString(@"Detected a new omemo device on your account: %@", @""), device]; - content.sound = [UNNotificationSound defaultSound]; - content.categoryIdentifier = @"simple"; - UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:[NSString stringWithFormat:@"newOwnOmemoDevice::%@::%@", self.account.connectionProperties.identity.jid, device] content:content trigger:nil]; - NSError* error = [HelperTools postUserNotificationRequest:request]; - if(error) - DDLogError(@"Error posting new deviceid notification: %@", error); - } + if([self.ownDeviceList count] > 0) + { + NSMutableSet* newDevices = [receivedDevices mutableCopy]; + [newDevices minusSet:self.ownDeviceList]; + //alert for all devices now still listed in newDevices + for(NSNumber* device in newDevices) + if([device unsignedIntValue] != self.monalSignalStore.deviceid) + { + DDLogWarn(@"Got new deviceid %@ for own account %@", device, self.account.connectionProperties.identity.jid); + UNMutableNotificationContent* content = [UNMutableNotificationContent new]; + content.title = NSLocalizedString(@"New omemo device", @"");; + content.subtitle = self.account.connectionProperties.identity.jid; + content.body = [NSString stringWithFormat:NSLocalizedString(@"Detected a new omemo device on your account: %@", @""), device]; + content.sound = [UNNotificationSound defaultSound]; + content.categoryIdentifier = @"simple"; + UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:[NSString stringWithFormat:@"newOwnOmemoDevice::%@::%@", self.account.connectionProperties.identity.jid, device] content:content trigger:nil]; + NSError* error = [HelperTools postUserNotificationRequest:request]; + if(error) + DDLogError(@"Error posting new deviceid notification: %@", error); + } + } //update own devicelist (this can be an empty list, if the list on our server is empty) self.ownDeviceList = [receivedDevices mutableCopy]; @@ -499,25 +532,21 @@ -(void) queryOMEMOBundleFrom:(NSString*) jid andDevice:(NSNumber*) deviceid return; } - NSString* bundleNode = [NSString stringWithFormat:@"eu.siacs.conversations.axolotl.bundles:%@", deviceid]; + //update bundle fetch status + self.openBundleFetchCnt++; [[MLNotificationQueue currentQueue] postNotificationName:kMonalUpdateBundleFetchStatus object:self userInfo:@{ @"accountNo": self.account.accountNo, @"completed": @(self.closedBundleFetchCnt), @"all": @(self.openBundleFetchCnt + self.closedBundleFetchCnt) }]; + + NSString* bundleNode = [NSString stringWithFormat:@"eu.siacs.conversations.axolotl.bundles:%@", deviceid]; [self.account.pubsub fetchNode:bundleNode from:jid withItemsList:nil andHandler:$newHandlerWithInvalidation(self, handleBundleFetchResult, handleBundleFetchInvalidation, $ID(jid), $ID(rid, deviceid))]; if(self.state.openBundleFetches[jid] == nil) self.state.openBundleFetches[jid] = [NSMutableSet new]; [self.state.openBundleFetches[jid] addObject:deviceid]; - //update bundle fetch status - self.openBundleFetchCnt++; - [[MLNotificationQueue currentQueue] postNotificationName:kMonalUpdateBundleFetchStatus object:self userInfo:@{ - @"accountNo": self.account.accountNo, - @"completed": @(self.closedBundleFetchCnt), - @"all": @(self.openBundleFetchCnt + self.closedBundleFetchCnt) - }]; } //don't mark any devices as deleted in this invalidation handler (like we do for an error in the normal handler below), @@ -604,21 +633,9 @@ -(void) handleBundleWithInvalidEntryForJid:(NSString*) jid andRid:(NSNumber*) ri } } --(void) decrementBundleFetchCount +-(BOOL) checkBundleFetchCount { - //use catchupDone for better UX on first login - if(self.openBundleFetchCnt > 1 || !self.state.catchupDone) - { - //update bundle fetch status (e.g. pending) - self.openBundleFetchCnt--; - self.closedBundleFetchCnt++; - [[MLNotificationQueue currentQueue] postNotificationName:kMonalUpdateBundleFetchStatus object:self userInfo:@{ - @"accountNo": self.account.accountNo, - @"completed": @(self.closedBundleFetchCnt), - @"all": @(self.openBundleFetchCnt + self.closedBundleFetchCnt), - }]; - } - else + if(self.openBundleFetchCnt == 0 && self.state.catchupDone) { //update bundle fetch status (e.g. complete) self.openBundleFetchCnt = 0; @@ -626,6 +643,25 @@ -(void) decrementBundleFetchCount [[MLNotificationQueue currentQueue] postNotificationName:kMonalFinishedOmemoBundleFetch object:self userInfo:@{ @"accountNo": self.account.accountNo, }]; + return YES; + } + return NO; +} + +-(void) decrementBundleFetchCount +{ + //update bundle fetch status (e.g. pending) + self.openBundleFetchCnt--; + self.closedBundleFetchCnt++; + + //check if we should send a bundle fetch status update or if checkBundleFetchCount already sent the final finished notification for us + if(![self checkBundleFetchCount]) + { + [[MLNotificationQueue currentQueue] postNotificationName:kMonalUpdateBundleFetchStatus object:self userInfo:@{ + @"accountNo": self.account.accountNo, + @"completed": @(self.closedBundleFetchCnt), + @"all": @(self.openBundleFetchCnt + self.closedBundleFetchCnt), + }]; } } @@ -959,7 +995,7 @@ -(void) encryptMessage:(XMPPMessage*) messageNode withMessage:(NSString* _Nullab } //check if we found omemo keys of at least one of the recipients or more than 1 own device, otherwise don't encrypt anything - NSSet* myDevices = [NSSet setWithArray:[self.monalSignalStore knownDevicesForAddressName:self.account.connectionProperties.identity.jid]]; + NSSet* myDevices = [self knownDevicesForAddressName:self.account.connectionProperties.identity.jid]; if(contactDeviceMap.count > 0 || myDevices.count > 1) { //add encryption for all of our own devices to contactDeviceMap @@ -1244,7 +1280,7 @@ -(void) subscribeAndFetchDevicelistIfNoSessionExistsForJid:(NSString*) buddyJid MLContact* contact = [MLContact createContactFromJid:buddyJid andAccountNo:self.account.accountNo]; //only do so if we don't receive automatic headline pushes of the devicelist if(!contact.isSubscribedTo) - [self queryOMEMODevices:buddyJid]; + [self queryOMEMODevices:buddyJid withSubscribe:YES]; } } diff --git a/Monal/Classes/MLPipe.m b/Monal/Classes/MLPipe.m index 3d158b74bd..394ec57d15 100755 --- a/Monal/Classes/MLPipe.m +++ b/Monal/Classes/MLPipe.m @@ -10,10 +10,11 @@ #import "HelperTools.h" #define kPipeBufferSize 4096 -static uint8_t _staticOutputBuffer[kPipeBufferSize+1]; //+1 for '\0' needed for logging the received raw bytes @interface MLPipe() { + uint8_t _staticOutputBuffer[kPipeBufferSize+1]; //+1 for '\0' needed for logging the received raw bytes + //buffer for writes to the output stream that can not be completed uint8_t* _outputBuffer; size_t _outputBufferByteCount; @@ -172,7 +173,8 @@ -(void) process //try to send remaining buffered data first if(_outputBufferByteCount > 0) { - DDLogDebug(@"trying to send buffered data: %lu bytes", (unsigned long)_outputBufferByteCount); + _outputBuffer[_outputBufferByteCount] = '\0'; //null termination for log output of raw string + DDLogDebug(@"trying to send buffered data(%lu): %s", (unsigned long)_outputBufferByteCount, _outputBuffer); NSInteger writtenLen = [_output write:_outputBuffer maxLength:_outputBufferByteCount]; if(writtenLen > 0) { @@ -233,7 +235,7 @@ -(void) process else DDLogDebug(@"pipe read %ld <= 0 bytes", (long)readLen); } while(readLen > 0 && [_input hasBytesAvailable] && [_output hasSpaceAvailable]); - //DDLogVerbose(@"pipe processing done"); + DDLogVerbose(@"pipe processing done"); } } diff --git a/Monal/Classes/MLPrivacySettingsViewController.m b/Monal/Classes/MLPrivacySettingsViewController.m index 0364417b9d..2be7104b14 100644 --- a/Monal/Classes/MLPrivacySettingsViewController.m +++ b/Monal/Classes/MLPrivacySettingsViewController.m @@ -59,31 +59,32 @@ - (void)viewWillDisappear:(BOOL)animated #pragma mark tableview datasource delegate --(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +-(NSInteger) numberOfSectionsInTableView:(UITableView*) tableView { return [self.sectionArray count]; } -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section +-(NSString*) tableView:(UITableView*) tableView titleForHeaderInSection:(NSInteger) section { return [self.sectionArray objectAtIndex:section]; } --(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section +-(UIView*) tableView:(UITableView*) tableView viewForHeaderInSection:(NSInteger) section { NSString* sectionTitle = [self tableView:tableView titleForHeaderInSection:section]; return [HelperTools MLCustomViewHeaderWithTitle:sectionTitle]; } -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +-(NSInteger) tableView:(UITableView*) tableView numberOfRowsInSection:(NSInteger) section { - switch (section) { + switch (section) + { case 0: { #ifdef DISABLE_OMEMO - return 12 + (self.isNotificationPrivacyOpened ? NotificationPrivacyOptionCnt : 0); -#else// DISABLE_OMEMO return 13 + (self.isNotificationPrivacyOpened ? NotificationPrivacyOptionCnt : 0); +#else// DISABLE_OMEMO + return 14 + (self.isNotificationPrivacyOpened ? NotificationPrivacyOptionCnt : 0); #endif// DISABLE_OMEMO } default: @@ -94,13 +95,14 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger } } -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +-(UITableViewCell*) tableView:(UITableView*) tableView cellForRowAtIndexPath:(NSIndexPath*) indexPath { - MLSwitchCell* cell = (MLSwitchCell *)[tableView dequeueReusableCellWithIdentifier:@"AccountCell"]; + MLSwitchCell* cell = (MLSwitchCell*)[tableView dequeueReusableCellWithIdentifier:@"AccountCell"]; [cell clear]; - switch (indexPath.section) { + switch(indexPath.section) + { case 0: { long row = indexPath.row; @@ -197,6 +199,11 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N break; #endif// DISABLE_OMEMO } + case 16: + { + [cell initCell:NSLocalizedString(@"Allow contacts not in my Contact list to contact me", @"") withToggleDefaultsKey:@"allowNonRosterContacts"]; + break; + } default: unreachable(); break; @@ -210,9 +217,10 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N return cell; } -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +-(void) tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) indexPath { - switch (indexPath.section) { + switch (indexPath.section) + { case 0: { long row = indexPath.row; @@ -252,6 +260,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath case 13: case 14: case 15: + case 16: break; } break; @@ -268,25 +277,18 @@ -(IBAction)close:(id)sender [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; } --(void)openNotificationPrivacyFolder +-(void) openNotificationPrivacyFolder { - if (self.isNotificationPrivacyOpened) - { - self.isNotificationPrivacyOpened = NO; - } - else - { - self.isNotificationPrivacyOpened = YES; - } + self.isNotificationPrivacyOpened = !self.isNotificationPrivacyOpened; [self refershTable]; } --(void)refershTable +-(void) refershTable { [_settingsTable reloadData]; } --(void)checkStatusForCell:(MLSwitchCell*) cell atIndexPath:(NSIndexPath*) idxPath +-(void) checkStatusForCell:(MLSwitchCell*) cell atIndexPath:(NSIndexPath*) idxPath { NotificationPrivacySettingOption privacySettionOption = (NotificationPrivacySettingOption)[[HelperTools defaultsDB] integerForKey:@"NotificationPrivacySetting"]; // default: remove checkmark @@ -318,8 +320,9 @@ -(void)checkStatusForCell:(MLSwitchCell*) cell atIndexPath:(NSIndexPath*) idxPat -(NSString*)getNsNotificationPrivacyOption:(NotificationPrivacySettingOption) option { - NSString *optionStr = @""; - switch (option) { + NSString* optionStr = @""; + switch (option) + { case DisplayNameAndMessage: optionStr = NSLocalizedString(@"Display Name And Message", @""); break; @@ -336,7 +339,7 @@ -(NSString*)getNsNotificationPrivacyOption:(NotificationPrivacySettingOption) op return optionStr; } --(void)setNotificationPrivacyOption:(NSIndexPath*) idxPath +-(void) setNotificationPrivacyOption:(NSIndexPath*) idxPath { switch (idxPath.row) { case DisplayNameAndMessageRow: diff --git a/Monal/Classes/MLPubSubProcessor.m b/Monal/Classes/MLPubSubProcessor.m index 900a37042f..3cbd5e7091 100644 --- a/Monal/Classes/MLPubSubProcessor.m +++ b/Monal/Classes/MLPubSubProcessor.m @@ -204,9 +204,7 @@ @implementation MLPubSubProcessor return; } - NSMutableDictionary* ownFavorites = [NSMutableDictionary new]; - for(NSDictionary* entry in [[DataLayer sharedInstance] listMucsForAccount:account.accountNo]) - ownFavorites[entry[@"room"]] = entry; + NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountNo]; //new/updated bookmarks if([type isEqualToString:@"publish"]) @@ -226,7 +224,7 @@ @implementation MLPubSubProcessor autojoin = @NO; //default value specified in xep //check if this is a new entry with autojoin=true - if(ownFavorites[room] == nil && [autojoin boolValue]) + if(![ownFavorites containsObject:room] && [autojoin boolValue]) { DDLogInfo(@"Entering muc '%@' on account %@ because it got added to bookmarks...", room, account.accountNo); //make sure we update our favorites table right away, to counter any race conditions when joining multiple mucs with one bookmarks update @@ -238,15 +236,14 @@ @implementation MLPubSubProcessor [account.mucProcessor sendDiscoQueryFor:room withJoin:YES andBookmarksUpdate:NO]; } //check if it is a known entry that changed autojoin to false - else if(ownFavorites[room] != nil && ![autojoin boolValue]) + else if([ownFavorites containsObject:room] && ![autojoin boolValue]) { DDLogInfo(@"Leaving muc '%@' on account %@ because not listed as autojoin=true in bookmarks...", room, account.accountNo); - //delete local favorites entry and leave room afterwards - [[DataLayer sharedInstance] deleteMuc:room forAccountId:account.accountNo]; - [account.mucProcessor leave:room withBookmarksUpdate:NO]; + //delete local favorites entry and leave room afterwards, but keep buddylist entry because only the autojoin flag changed + [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:YES]; } //check for nickname changes - else if(ownFavorites[room] != nil && nick != nil) + else if([ownFavorites containsObject:room] && nick != nil) { NSString* oldNick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountNo]; if(![nick isEqualToString:oldNick]) @@ -270,10 +267,14 @@ @implementation MLPubSubProcessor for(NSString* itemId in data) { NSString* room = [itemId lowercaseString]; - DDLogInfo(@"Leaving muc '%@' on account %@ because not listed in bookmarks anymore...", room, account.accountNo); - //delete local favorites entry and leave room afterwards - [[DataLayer sharedInstance] deleteMuc:room forAccountId:account.accountNo]; - [account.mucProcessor leave:room withBookmarksUpdate:NO]; + if([ownFavorites containsObject:room]) + { + DDLogInfo(@"Leaving muc '%@' on account %@ because not listed in bookmarks anymore...", room, account.accountNo); + //delete local favorites entry and leave room afterwards + [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO]; + } + else + DDLogVerbose(@"Ignoring retracted bookmark because not listed in muc_favorites already..."); } } else @@ -284,8 +285,7 @@ @implementation MLPubSubProcessor { DDLogInfo(@"Leaving muc '%@' on account %@ because all bookmarks got deleted...", room, account.accountNo); //delete local favorites entry and leave room afterwards - [[DataLayer sharedInstance] deleteMuc:room forAccountId:account.accountNo]; - [account.mucProcessor leave:room withBookmarksUpdate:NO]; + [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO]; } } $$ @@ -315,10 +315,8 @@ @implementation MLPubSubProcessor max_items = @"max"; NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary]; - NSMutableDictionary* ownFavorites = [NSMutableDictionary new]; - for(NSDictionary* entry in [[DataLayer sharedInstance] listMucsForAccount:account.accountNo]) - ownFavorites[entry[@"room"]] = entry; - DDLogVerbose(@"Own favorites: %@", [ownFavorites allKeys]); + NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountNo]; + DDLogVerbose(@"Own favorites: %@", ownFavorites); //filter passwort protected mucs and make sure jids (the item ids) are always lowercase NSMutableDictionary* _data = [NSMutableDictionary new]; @@ -346,7 +344,7 @@ @implementation MLPubSubProcessor autojoin = @NO; //default value specified in xep //check if the bookmark exists with autojoin==false and only update the autojoin and nick values, if true - if(ownFavorites[room] && ![autojoin boolValue]) + if([ownFavorites containsObject:room] && ![autojoin boolValue]) { DDLogInfo(@"Updating autojoin of bookmarked muc '%@' on account %@ to 'true'...", room, account.accountNo); @@ -372,7 +370,7 @@ @implementation MLPubSubProcessor } //add all mucs not yet listed in bookmarks - NSMutableSet* toAdd = [NSMutableSet setWithArray:[ownFavorites allKeys]]; + NSMutableSet* toAdd = [ownFavorites mutableCopy]; [toAdd minusSet:[NSSet setWithArray:[_data allKeys]]]; for(NSString* room in toAdd) { @@ -385,7 +383,7 @@ @implementation MLPubSubProcessor } andChildren:@[ nilWrapper(nick != nil ? [[MLXMLNode alloc] initWithElement:@"nick" withAttributes:@{} andChildren:@[] andData:nick] : nil), [[MLXMLNode alloc] initWithElement:@"extensions" withAttributes:@{} andChildren:@[ - [[MLXMLNode alloc] initWithElement:@"added-by" andNamespace:@"urn:xmpp:monal.im:bookmarks:info" withAttributes:@{ + [[MLXMLNode alloc] initWithElement:@"added-by" andNamespace:@"urn:monal.im:bookmarks:info" withAttributes:@{ @"name": @"Monal", @"version": infoDict[@"CFBundleShortVersionString"], @"build": infoDict[@"CFBundleVersion"], @@ -402,7 +400,7 @@ @implementation MLPubSubProcessor //remove all mucs not listed in local favorites table NSMutableSet* toRemove = [NSMutableSet setWithArray:[_data allKeys]]; - [toRemove minusSet:[NSSet setWithArray:[ownFavorites allKeys]]]; + [toRemove minusSet:ownFavorites]; for(NSString* room in toRemove) { DDLogInfo(@"Removing muc '%@' on account %@ from bookmarks...", room, account.accountNo); @@ -455,9 +453,7 @@ @implementation MLPubSubProcessor return; } - NSMutableDictionary* ownFavorites = [NSMutableDictionary new]; - for(NSDictionary* entry in [[DataLayer sharedInstance] listMucsForAccount:account.accountNo]) - ownFavorites[entry[@"room"]] = entry; + NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountNo]; //new/updated bookmarks if([type isEqualToString:@"publish"]) @@ -489,7 +485,7 @@ @implementation MLPubSubProcessor autojoin = @NO; //default value specified in xep //check if this is a new entry with autojoin=true - if(ownFavorites[room] == nil && [autojoin boolValue]) + if(![ownFavorites containsObject:room] && [autojoin boolValue]) { DDLogInfo(@"Entering muc '%@' on account %@ because it got added to bookmarks...", room, account.accountNo); //make sure we update our favorites table right away, to counter any race conditions when joining multiple mucs with one bookmarks update @@ -501,15 +497,14 @@ @implementation MLPubSubProcessor [account.mucProcessor sendDiscoQueryFor:room withJoin:YES andBookmarksUpdate:NO]; } //check if it is a known entry that changed autojoin to false - else if(ownFavorites[room] != nil && ![autojoin boolValue]) + else if([ownFavorites containsObject:room] && ![autojoin boolValue]) { DDLogInfo(@"Leaving muc '%@' on account %@ because not listed as autojoin=true in bookmarks...", room, account.accountNo); - //delete local favorites entry and leave room afterwards - [[DataLayer sharedInstance] deleteMuc:room forAccountId:account.accountNo]; - [account.mucProcessor leave:room withBookmarksUpdate:NO]; + //delete local favorites entry and leave room afterwards, but keep buddylist entry because only the autojoin flag changed + [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:YES]; } //check for nickname changes - else if(ownFavorites[room] != nil && nick != nil) + else if([ownFavorites containsObject:room] && nick != nil) { NSString* oldNick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountNo]; if(![nick isEqualToString:oldNick]) @@ -529,14 +524,13 @@ @implementation MLPubSubProcessor } //remove and leave all mucs removed from bookmarks - NSMutableSet* toLeave = [NSMutableSet setWithArray:[ownFavorites allKeys]]; + NSMutableSet* toLeave = [ownFavorites mutableCopy]; [toLeave minusSet:bookmarkedMucs]; for(NSString* room in toLeave) { DDLogInfo(@"Leaving muc '%@' on account %@ because not listed in bookmarks anymore...", room, account.accountNo); //delete local favorites entry and leave room afterwards - [[DataLayer sharedInstance] deleteMuc:room forAccountId:account.accountNo]; - [account.mucProcessor leave:room withBookmarksUpdate:NO]; + [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO]; } return; //we only need the first pep item (there should be only one item in the first place) @@ -549,8 +543,7 @@ @implementation MLPubSubProcessor { DDLogInfo(@"Leaving muc '%@' on account %@ because all bookmarks got deleted...", room, account.accountNo); //delete local favorites entry and leave room afterwards - [[DataLayer sharedInstance] deleteMuc:room forAccountId:account.accountNo]; - [account.mucProcessor leave:room withBookmarksUpdate:NO]; + [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO]; } $$ @@ -575,9 +568,7 @@ @implementation MLPubSubProcessor } BOOL changed = NO; - NSMutableDictionary* ownFavorites = [NSMutableDictionary new]; - for(NSDictionary* entry in [[DataLayer sharedInstance] listMucsForAccount:account.accountNo]) - ownFavorites[entry[@"room"]] = entry; + NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountNo]; for(NSString* itemId in data) { @@ -606,7 +597,7 @@ @implementation MLPubSubProcessor autojoin = @NO; //default value specified in xep //check if the bookmark exists with autojoin==false and only update the autojoin and nick values, if true - if(ownFavorites[room] && ![autojoin boolValue]) + if([ownFavorites containsObject:room] && ![autojoin boolValue]) { DDLogInfo(@"Updating autojoin of bookmarked muc '%@' on account %@ to 'true'...", room, account.accountNo); @@ -626,7 +617,7 @@ @implementation MLPubSubProcessor } //add all mucs not yet listed in bookmarks - NSMutableSet* toAdd = [NSMutableSet setWithArray:[ownFavorites allKeys]]; + NSMutableSet* toAdd = [ownFavorites mutableCopy]; [toAdd minusSet:bookmarkedMucs]; for(NSString* room in toAdd) { @@ -642,7 +633,7 @@ @implementation MLPubSubProcessor //remove all mucs not listed in local favorites table NSMutableSet* toRemove = [bookmarkedMucs mutableCopy]; - [toRemove minusSet:[NSMutableSet setWithArray:[ownFavorites allKeys]]]; + [toRemove minusSet:ownFavorites]; for(NSString* room in toRemove) { DDLogInfo(@"Removing muc '%@' on account %@ from bookmarks...", room, account.accountNo); diff --git a/Monal/Classes/MLSQLite.m b/Monal/Classes/MLSQLite.m index 1ae83ef847..88b60fde22 100644 --- a/Monal/Classes/MLSQLite.m +++ b/Monal/Classes/MLSQLite.m @@ -381,12 +381,16 @@ -(BOOL) boolWriteTransaction:(monal_sqlite_bool_operations_t) operations -(id) idWriteTransaction:(monal_sqlite_operations_t) operations { [self beginWriteTransaction]; +#if !TARGET_OS_SIMULATOR NSDate* startTime = [NSDate date]; +#endif id retval = operations(); +#if !TARGET_OS_SIMULATOR NSDate* endTime = [NSDate date]; + if([endTime timeIntervalSinceDate:startTime] > 2.0) + showErrorOnAlpha(nil, @"Write transaction blocking took %fs (longer than 2.0s): %@", (double)[endTime timeIntervalSinceDate:startTime], [NSThread callStackSymbols]); +#endif [self endWriteTransaction]; - if([endTime timeIntervalSinceDate:startTime] > 0.5) - showErrorOnAlpha(nil, @"Write transaction took %fs (longer than 0.5s): %@", (double)[endTime timeIntervalSinceDate:startTime], [NSThread callStackSymbols]); return retval; } diff --git a/Monal/Classes/MLStream.h b/Monal/Classes/MLStream.h index 906fe9ef2d..60e07b59d4 100644 --- a/Monal/Classes/MLStream.h +++ b/Monal/Classes/MLStream.h @@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nullable, readonly, copy) NSError* streamError; -+(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host connectPort:(NSNumber*) port tls:(BOOL) tls inputStream:(NSInputStream* _Nullable * _Nonnull) inputStream outputStream:(NSOutputStream* _Nullable * _Nonnull) outputStream; ++(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host connectPort:(NSNumber*) port tls:(BOOL) tls inputStream:(NSInputStream* _Nullable * _Nonnull) inputStream outputStream:(NSOutputStream* _Nullable * _Nonnull) outputStream logtag:(id _Nullable) logtag; -(void) startTLS; @property(readonly) BOOL hasTLS; @property(readonly) BOOL isTLS13; diff --git a/Monal/Classes/MLStream.m b/Monal/Classes/MLStream.m index 07b974ddf5..e65af8e0d6 100644 --- a/Monal/Classes/MLStream.m +++ b/Monal/Classes/MLStream.m @@ -414,7 +414,7 @@ -(void) generateEvent:(NSStreamEvent) event @implementation MLStream -+(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host connectPort:(NSNumber*) port tls:(BOOL) tls inputStream:(NSInputStream* _Nullable * _Nonnull) inputStream outputStream:(NSOutputStream* _Nullable * _Nonnull) outputStream ++(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host connectPort:(NSNumber*) port tls:(BOOL) tls inputStream:(NSInputStream* _Nullable * _Nonnull) inputStream outputStream:(NSOutputStream* _Nullable * _Nonnull) outputStream logtag:(id _Nullable) logtag { //create state volatile __block BOOL wasOpenOnce = NO; @@ -463,9 +463,9 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host nw_protocol_definition_t starttls_framer_definition = nw_framer_create_definition([[[NSUUID UUID] UUIDString] UTF8String], NW_FRAMER_CREATE_FLAGS_DEFAULT, ^(nw_framer_t framer) { //we don't need any locking for our counter because all framers will be started in the same internal network queue int framerId = startupCounter++; - DDLogInfo(@"Framer(%d) %@ start called with wasOpenOnce=%@...", framerId, framer, bool2str(wasOpenOnce)); + DDLogInfo(@"%@: Framer(%d) %@ start called with wasOpenOnce=%@...", logtag, framerId, framer, bool2str(wasOpenOnce)); nw_framer_set_stop_handler(framer, (nw_framer_stop_handler_t)^(nw_framer_t _Nullable framer) { - DDLogInfo(@"Framer(%d) stop called: %@", framerId, framer); + DDLogInfo(@"%@, Framer(%d) stop called: %@", logtag, framerId, framer); return YES; }); @@ -477,6 +477,7 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host nw_framer_set_input_handler(framer, ^size_t(nw_framer_t framer) { nw_framer_parse_input(framer, 1, BUFFER_SIZE, nil, ^size_t(uint8_t* buffer, size_t buffer_length, bool is_complete) { MLAssert(NO, @"Unexpected incoming bytes in first framer!", (@{ + @"logtag": nilWrapper(logtag), @"framer": framer, @"buffer": [NSData dataWithBytes:buffer length:buffer_length], @"buffer_length": @(buffer_length), @@ -488,6 +489,7 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host }); nw_framer_set_output_handler(framer, ^(nw_framer_t framer, nw_framer_message_t message, size_t message_length, bool is_complete) { MLAssert(NO, @"Unexpected outgoing bytes in first framer!", (@{ + @"logtag": nilWrapper(logtag), @"framer": framer, @"message": message, @"message_length": @(message_length), @@ -522,7 +524,7 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host shared_state.framer = framer; return nw_framer_start_result_will_mark_ready; }); - DDLogInfo(@"Not doing direct TLS: appending framer to protocol stack..."); + DDLogInfo(@"%@: Not doing direct TLS: appending framer to protocol stack...", logtag); nw_protocol_stack_prepend_application_protocol(nw_parameters_copy_default_protocol_stack(parameters), nw_framer_create_options(starttls_framer_definition)); } //needed to activate tcp fast open with apple's internal tls framer @@ -531,7 +533,7 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host //create and configure connection object nw_endpoint_t endpoint = nw_endpoint_create_host([host cStringUsingEncoding:NSUTF8StringEncoding], [[port stringValue] cStringUsingEncoding:NSUTF8StringEncoding]); nw_connection_t connection = nw_connection_create(endpoint, parameters); - nw_connection_set_queue(connection, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)); + nw_connection_set_queue(connection, dispatch_queue_create_with_target([NSString stringWithFormat:@"im.monal.networking:%@", logtag].UTF8String, DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0))); //configure shared state shared_state.connection = connection; @@ -543,7 +545,7 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host //connection was opened once (e.g. opening=YES) and closed later on (e.g. open=NO) if(wasOpenOnce && !shared_state.open) { - DDLogVerbose(@"ignoring call to nw_connection state_changed_handler, connection already closed: %@ --> %du, %@", self, state, error); + DDLogVerbose(@"%@: ignoring call to nw_connection state_changed_handler, connection already closed: %@ --> %du, %@", logtag, self, state, error); return; } } @@ -553,11 +555,11 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host //which seems to mean: if the network path changed (for example connectivity regained) //if this happens inside the connection timeout all is ok //if not, the connection will be cancelled already and everything will be ok, too - DDLogVerbose(@"got nw_connection_state_waiting and ignoring it, see comments in code..."); + DDLogVerbose(@"%@: got nw_connection_state_waiting and ignoring it, see comments in code...", logtag); } else if(state == nw_connection_state_failed) { - DDLogError(@"Connection failed"); + DDLogError(@"%@: Connection failed", logtag); NSError* st_error = (NSError*)CFBridgingRelease(nw_error_copy_cf_error(error)); @synchronized(shared_state) { shared_state.error = st_error; @@ -567,7 +569,7 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host } else if(state == nw_connection_state_ready) { - DDLogInfo(@"Connection established, wasOpenOnce: %@", bool2str(wasOpenOnce)); + DDLogInfo(@"%@: Connection established, wasOpenOnce: %@", bool2str(wasOpenOnce), logtag); if(!wasOpenOnce) { wasOpenOnce = YES; @@ -601,17 +603,17 @@ +(void) connectWithSNIDomain:(NSString*) SNIDomain connectHost:(NSString*) host else if(state == nw_connection_state_cancelled) { //ignore this (we use reference counting) - DDLogVerbose(@"ignoring call to nw_connection state_changed_handler with state nw_connection_state_cancelled: %@ (%@)", self, error); + DDLogVerbose(@"%@: ignoring call to nw_connection state_changed_handler with state nw_connection_state_cancelled: %@ (%@)", logtag, self, error); } else if(state == nw_connection_state_invalid) { //ignore all other states (preparing, invalid) - DDLogVerbose(@"ignoring call to nw_connection state_changed_handler with state nw_connection_state_invalid: %@ (%@)", self, error); + DDLogVerbose(@"%@: ignoring call to nw_connection state_changed_handler with state nw_connection_state_invalid: %@ (%@)", logtag, self, error); } else if(state == nw_connection_state_preparing) { //ignore all other states (preparing, invalid) - DDLogVerbose(@"ignoring call to nw_connection state_changed_handler with state nw_connection_state_preparing: %@ (%@)", self, error); + DDLogVerbose(@"%@: ignoring call to nw_connection state_changed_handler with state nw_connection_state_preparing: %@ (%@)", logtag, self, error); } else unreachable(); diff --git a/Monal/Classes/MLVoIPProcessor.m b/Monal/Classes/MLVoIPProcessor.m index bcdf8e7a63..a3bcb37fb8 100644 --- a/Monal/Classes/MLVoIPProcessor.m +++ b/Monal/Classes/MLVoIPProcessor.m @@ -264,13 +264,21 @@ -(void) handleIncomingVoipCall:(NSNotification*) notification //handle tie breaking: one party migrates the call to other device else if(existingCall.state < MLCallStateFinished) //call already running { - DDLogInfo(@"Migrating from new call to existing call: %@", existingCall); - [existingCall migrateTo:newCall]; - - //drop new call after migration to make sure it does not interfere with our existing call - DDLogInfo(@"Dropping newCall '%@' in favor of migrated existingCall '%@' ...", [newCall short], [existingCall short]); - newCall = nil; - + if(newCall.callType == existingCall.callType) + { + DDLogInfo(@"Migrating from new call to existing call: %@", existingCall); + [existingCall migrateTo:newCall]; + + //drop new call after migration to make sure it does not interfere with our existing call + DDLogInfo(@"Dropping newCall '%@' in favor of migrated existingCall '%@' ...", [newCall short], [existingCall short]); + newCall = nil; + } + else + { + existingCall.tieBreak = YES; //will be ignored if call was connected, but it doesn't hurt either + [existingCall end]; + [self processIncomingCall:notification.userInfo withCompletion:nil]; + } return; } unreachable(); diff --git a/Monal/Classes/MLXMPPManager.m b/Monal/Classes/MLXMPPManager.m index 14bbbd17ab..440c02d9db 100644 --- a/Monal/Classes/MLXMPPManager.m +++ b/Monal/Classes/MLXMPPManager.m @@ -139,6 +139,9 @@ -(void) defaultSettings //default value for sanbox is no (e.g. production) [self upgradeBoolUserSettingsIfUnset:@"isSandboxAPNS" toDefault:NO]; + + //anti spam/privacy setting, but default to yes (current behavior, conversations behavior etc.) + [self upgradeBoolUserSettingsIfUnset:@"allowNonRosterContacts" toDefault:YES]; } -(void) upgradeBoolUserSettingsIfUnset:(NSString*) settingsName toDefault:(BOOL) defaultVal @@ -316,7 +319,7 @@ -(id) init //trigger iq invalidations from a background thread because timeouts aren't time critical //we use this to decrement the timeout value of an iq handler every second until it reaches zero - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + dispatch_async(dispatch_queue_create_with_target("im.monal.iqtimeouts", DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)), ^{ while(YES) { for(xmpp* account in [MLXMPPManager sharedInstance].connectedXMPP) [account updateIqHandlerTimeouts]; diff --git a/Monal/Classes/MemberList.swift b/Monal/Classes/MemberList.swift index 88fca399eb..052efb1d53 100644 --- a/Monal/Classes/MemberList.swift +++ b/Monal/Classes/MemberList.swift @@ -11,96 +11,227 @@ import monalxmpp import OrderedCollections struct MemberList: View { - @Environment(\.editMode) private var editMode - - private let memberList: [ObservableKVOWrapper] - private let groupName: String - private let account: xmpp? - private let isAlpha: Bool - + private let account: xmpp + private let ownAffiliation: String; + @StateObject var group: ObservableKVOWrapper + @State private var memberList: OrderedSet> + @State private var affiliation: Dictionary @State private var openAccountSelection : Bool = false - @State private var contactsToAdd : OrderedSet = [] - @State private var showAlert = false @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) - func setAndShowAlert(title: String, description: String) { + @State private var selectedMember: MLContact? + + init(mucContact: ObservableKVOWrapper) { + self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.accountId)! as xmpp + _group = StateObject(wrappedValue: mucContact) + _memberList = State(wrappedValue: getContactList(viewContact: mucContact)) + self.ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:mucContact.obj) ?? "none" + var affiliationTmp = Dictionary() + for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: mucContact.contactJid, forAccountId: self.account.accountNo)) { + guard let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String else { + continue + } + affiliationTmp.updateValue((memberInfo["affiliation"] as? String) ?? "none", forKey: jid) + } + _affiliation = State(wrappedValue: affiliationTmp) + } + + func showAlert(title: String, description: String) { self.alertPrompt.title = Text(title) self.alertPrompt.message = Text(description) self.showAlert = true } + func ownUserHasAffiliationToRemove(contact: ObservableKVOWrapper) -> Bool { + if contact.obj.contactJid == self.account.connectionProperties.identity.jid { + return false + } + if let contactAffiliation = self.affiliation[contact.contactJid] { + if self.ownAffiliation == "owner" { + return true + } else if self.ownAffiliation == "admin" && contactAffiliation == "member" { + return true + } + } + return false + } + var body: some View { - // This is the invisible NavigationLink hack again... - NavigationLink(destination:LazyClosureView(ContactPicker(selectedContacts: $contactsToAdd)), isActive: $openAccountSelection){}.hidden().disabled(true) // navigation happens as soon as our button sets navigateToQRCodeView to true... List { - Section(header: Text(self.groupName)) { + Section(header: Text(self.group.obj.contactDisplayName)) { + if self.ownAffiliation == "owner" || self.ownAffiliation == "admin" { + NavigationLink(destination: LazyClosureView(ContactPicker(account: self.account, selectedContacts: $memberList, existingMembers: self.memberList)), label: { + Text("Add Group Members") + }) + } ForEach(self.memberList, id: \.self.obj) { contact in - if contact.obj.contactJid != self.account?.connectionProperties.identity.jid { - NavigationLink(destination: LazyClosureView(ContactDetails(delegate: SheetDismisserProtocol(), contact: contact)), label: { - ZStack(alignment: .topLeading) { - HStack(alignment: .center) { - Image(uiImage: contact.obj.avatar) - .resizable() - .frame(width: 40, height: 40, alignment: .center) - Text(contact.contactDisplayName as String) - if(editMode?.wrappedValue.isEditing == true) { - Spacer() - Button(action: {}, label: { - Image(systemName: "slider.horizontal.3") - }) - } - } + HStack(alignment: .center) { + Image(uiImage: contact.avatar) + .resizable() + .frame(width: 40, height: 40, alignment: .center) + Text(contact.contactDisplayName as String) + Spacer() + if let contactAffiliation = self.affiliation[contact.contactJid] { + if contactAffiliation == "owner" { + Text(NSLocalizedString("Owner", comment: "muc affiliation")) + } else if contactAffiliation == "admin" { + Text(NSLocalizedString("Admin", comment: "muc affiliation")) + } else if contactAffiliation == "member" { + Text(NSLocalizedString("Member", comment: "muc affiliation")) + } else if contactAffiliation == "outcast" { + Text(NSLocalizedString("Outcast", comment: "muc affiliation")) + } else { + Text(NSLocalizedString("", comment: "muc affiliation")) } - }) + } } + .onTapGesture(perform: { + if contact.obj.contactJid != self.account.connectionProperties.identity.jid { + self.selectedMember = contact.obj + } + }) + .deleteDisabled( + !ownUserHasAffiliationToRemove(contact: contact) + ) } .onDelete(perform: { memberIdx in - // TODO maybe alert before deletion - if(memberIdx.count == 1) { - self.setAndShowAlert(title: "Member deleted", description: self.memberList[memberIdx.first!].contactJid) - } + let member = self.memberList[memberIdx.first!] + self.account.mucProcessor.setAffiliation("none", ofUser: member.contactJid, inMuc: self.group.contactJid) + + self.showAlert(title: "Member deleted", description: self.memberList[memberIdx.first!].contactJid) + self.memberList.remove(at: memberIdx.first!) }) - .deleteDisabled(self.isAlpha) - }.alert(isPresented: $showAlert, content: { + } + .onChange(of: self.memberList) { [previousMemberList = self.memberList] newMemberList in + // only handle new members (added via the contact picker) + for member in newMemberList { + if !previousMemberList.contains(member) { + // add selected group member with affiliation member + affiliationChangeAction(member, affiliation: "member") + } + } + } + .alert(isPresented: $showAlert, content: { Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(alertPrompt.dismissLabel)) }) + .sheet(item: self.$selectedMember, content: { selectedMemberUnobserved in + let selectedMember = ObservableKVOWrapper(selectedMemberUnobserved) + VStack { + Form { + Section { + HStack { + Spacer() + Image(uiImage: selectedMember.avatar) + .resizable() + .frame(width: 150, height: 150, alignment: .center) + Spacer() + } + HStack { + Spacer() + Text(selectedMember.contactDisplayName as String) + Spacer() + } + } + Section(header: Text("Configure Membership")) { + if self.ownAffiliation == "owner" && self.affiliation[selectedMember.contactJid] == "owner" { + makeAdmin(selectedMember) + makeMember(selectedMember) + removeUserButton(selectedMember) + block(selectedMember) + } + if self.ownAffiliation == "owner" && self.affiliation[selectedMember.contactJid] == "admin" { + makeOwner(selectedMember) + makeMember(selectedMember) + removeUserButton(selectedMember) + block(selectedMember) + } + if self.ownAffiliation == "owner" && self.affiliation[selectedMember.contactJid] == "member" { + makeOwner(selectedMember) + makeAdmin(selectedMember) + removeUserButton(selectedMember) + block(selectedMember) + } + if self.ownAffiliation == "admin" && self.affiliation[selectedMember.contactJid] == "member" { + removeUserButton(selectedMember) + block(selectedMember) + } + if (self.ownAffiliation == "admin" || self.ownAffiliation == "owner") && self.affiliation[selectedMember.contactJid] == "outcast" { + makeMember(selectedMember) + } + } + } + } + }) } - .toolbar { - if(isAlpha && editMode?.wrappedValue.isEditing == true) { - Button(action: { - openAccountSelection = true - }, label: { - Image(systemName: "plus") - .foregroundColor(.blue) - }) + .navigationBarTitle("Group Members", displayMode: .inline) + } + + func removeUserButton(_ selectedMember: ObservableKVOWrapper) -> some View { + if #available(iOS 15, *) { + return Button(role: .destructive, action: { + self.account.mucProcessor.setAffiliation("none", ofUser: selectedMember.contactJid, inMuc: self.group.contactJid) + self.showAlert(title: "Member deleted", description: selectedMember.contactJid) + if let index = self.memberList.firstIndex(of: selectedMember) { + self.memberList.remove(at: index) + } + self.selectedMember = nil + }) { + Text("Remove from group") } - EditButton() + } else { + return AnyView(EmptyView()) } - .navigationBarTitle("Group Members", displayMode: .inline) } - init(mucContact: ObservableKVOWrapper?) { - if let mucContact = mucContact { - self.account = MLXMPPManager.sharedInstance().getConnectedAccount(forID: mucContact.obj.accountId)! as xmpp - self.groupName = mucContact.contactDisplayName - self.memberList = getContactList(viewContact: mucContact) + func affiliationChangeAction(_ selectedMember: ObservableKVOWrapper, affiliation: String) { + self.account.mucProcessor.setAffiliation(affiliation, ofUser: selectedMember.contactJid, inMuc: self.group.contactJid) + self.affiliation[selectedMember.contactJid] = affiliation + } + + func affiliationButton(_ selectedMember: ObservableKVOWrapper, affiliation: String, @ViewBuilder label: () -> Label) -> some View { + return Button(action: { + affiliationChangeAction(selectedMember, affiliation: affiliation) + // dismiss sheet + self.selectedMember = nil + }) { + label() + } + } + + func makeOwner(_ selectedMember: ObservableKVOWrapper) -> some View { + return affiliationButton(selectedMember, affiliation: "owner", label: { + Text("Make owner") + }) + } + + func makeAdmin(_ selectedMember: ObservableKVOWrapper) -> some View { + return affiliationButton(selectedMember, affiliation: "admin", label: { + Text("Make admin") + }) + } + + func makeMember(_ selectedMember: ObservableKVOWrapper) -> some View { + return affiliationButton(selectedMember, affiliation: "member", label: { + Text("Make member") + }) + } + + func block(_ selectedMember: ObservableKVOWrapper) -> AnyView { + if self.group.mucType != "group" { + return AnyView( + affiliationButton(selectedMember, affiliation: "outcast", label: { + Text("Block grom group") + }) + ) } else { - self.account = nil - self.groupName = "Invalid Group" - self.memberList = [] + return AnyView(EmptyView()) } -#if IS_ALPA - self.isAlpha = true -#else - self.isAlpha = false -#endif } } struct MemberList_Previews: PreviewProvider { static var previews: some View { - // TODO some dummy views, requires a dummy xmpp obj - MemberList(mucContact: nil); + MemberList(mucContact:ObservableKVOWrapper(MLContact.makeDummyContact(2))); } } diff --git a/Monal/Classes/MonalAppDelegate.m b/Monal/Classes/MonalAppDelegate.m index 8bbef63934..51d1a6bf2a 100644 --- a/Monal/Classes/MonalAppDelegate.m +++ b/Monal/Classes/MonalAppDelegate.m @@ -73,7 +73,7 @@ -(void) runParserTests \n\ SCRAM-SHA-1PLAIN\n\ http://jabber.org/protocol/muc#roominfo200testchat gruppe\n\ - \n\ + \n\ \n\ \n\ \n\ @@ -124,7 +124,7 @@ -(void) runParserTests DDLogDebug(@"Query: '%@', result: '%@'", query, result); } NSString* specialQuery1 = @"//{http://jabber.org/protocol/pubsub}pubsub/subscription"; - id result = [parsedStanza find:specialQuery1, @"result", @"eu.siacs.conversations.axolotl.devicelist", "subscribed", @"test1@xmpp.eightysoft.de"]; + id result = [parsedStanza find:specialQuery1, @"result", @"eu.siacs.conversations.axolotl.devicelist", "subscribed", @"user@example.com"]; DDLogDebug(@"Query: '%@', result: '%@'", specialQuery1, result); //handle gajim disco hash testcase @@ -1617,7 +1617,10 @@ -(void) handleBackgroundProcessingTask:(BGTask*) task [refreshingTask setTaskCompletedWithSuccess:YES]; } - MLAssert([[MLXMPPManager sharedInstance] hasConnectivity], @"BGTASK has *no* connectivity? That's strange!"); + if(![[MLXMPPManager sharedInstance] hasConnectivity]) + { + DDLogError(@"BGTASK has *no* connectivity? That's strange!"); + } [self startBackgroundTimer:BGPROCESS_GRACEFUL_TIMEOUT]; @synchronized(self) { @@ -1715,7 +1718,10 @@ -(void) handleBackgroundRefreshingTask:(BGTask*) task // [[UIApplication sharedApplication] endBackgroundTask:task]; // } - MLAssert([[MLXMPPManager sharedInstance] hasConnectivity], @"BGTASK has *no* connectivity? That's strange!"); + if(![[MLXMPPManager sharedInstance] hasConnectivity]) + { + DDLogError(@"BGTASK has *no* connectivity? That's strange!"); + } [self startBackgroundTimer:GRACEFUL_TIMEOUT]; @synchronized(self) { diff --git a/Monal/Classes/OmemoKeys.swift b/Monal/Classes/OmemoKeys.swift index 575144adcf..8621abc8dd 100644 --- a/Monal/Classes/OmemoKeys.swift +++ b/Monal/Classes/OmemoKeys.swift @@ -267,7 +267,7 @@ struct OmemoKeys: View { @State private var scannedJid : String = "" @State private var scannedFingerprints : Dictionary = [:] - @State private var contacts: [ObservableKVOWrapper] // contact list may change/be reloaded -> state + @State private var contacts: OrderedSet> // contact list may change/be reloaded -> state @State var selectedContact : ObservableKVOWrapper? // for reason why see start of body @State private var navigateToQRCodeView = false diff --git a/Monal/Classes/PasswordMigration.swift b/Monal/Classes/PasswordMigration.swift index f090ca344c..83f7819225 100644 --- a/Monal/Classes/PasswordMigration.swift +++ b/Monal/Classes/PasswordMigration.swift @@ -78,7 +78,7 @@ struct PasswordMigration: View { } } )) - .addClearButton(text:Binding( + .addClearButton(isEditing: true, text:Binding( get: { self.needingMigration[id]?["password"] as? String ?? "" }, set: { self.needingMigration[id]?["password"] = $0 as NSString } )) diff --git a/Monal/Classes/RegisterAccount.swift b/Monal/Classes/RegisterAccount.swift index dda5c9885a..f3bc791d4b 100644 --- a/Monal/Classes/RegisterAccount.swift +++ b/Monal/Classes/RegisterAccount.swift @@ -55,7 +55,7 @@ struct RegisterAccount: View { @State private var captchaText: String = "" @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) - @ObservedObject private var overlay = LoadingOverlayState() + @StateObject private var overlay = LoadingOverlayState() @State private var currentTimeout : DispatchTime? = nil @State private var showWebView = false diff --git a/Monal/Classes/RichAlert.swift b/Monal/Classes/RichAlert.swift index eacc0b4f9a..4b2a12a329 100644 --- a/Monal/Classes/RichAlert.swift +++ b/Monal/Classes/RichAlert.swift @@ -59,17 +59,18 @@ struct RichAlertView: ViewModifier DispatchQueue.main.async { scrollViewContentSize = geo.size } - return Color.white + return Color.background } ) } .frame(maxHeight: scrollViewContentSize.height) } + .foregroundColor(.primary) .padding([.top, .bottom], 13) .frame(width: 320) - .background(Color.white) + .background(Color.background) .cornerRadius(16) - .shadow(color: Color.black.opacity(0.4), radius: 16, x: 0, y: 0) + .shadow(color: Color.primary.opacity(0.4), radius: 16, x: 0, y: 0) .padding([.top, .bottom], 24) } } diff --git a/Monal/Classes/SwiftHelpers.swift b/Monal/Classes/SwiftHelpers.swift index acc6ab408e..e002a1919a 100644 --- a/Monal/Classes/SwiftHelpers.swift +++ b/Monal/Classes/SwiftHelpers.swift @@ -69,7 +69,7 @@ class KVOObserver: NSObject { } @dynamicMemberLookup -public class ObservableKVOWrapper: ObservableObject { +public class ObservableKVOWrapper: ObservableObject, Hashable, Equatable { public var obj: ObjType private var observedMembers: NSMutableSet = NSMutableSet() private var observers: [KVOObserver] = Array() @@ -113,7 +113,7 @@ public class ObservableKVOWrapper: ObservableObject { self.setWrapper(for:member, value:newValue as AnyObject?) } } - + public subscript(dynamicMember member: String) -> T { get { return self.getWrapper(for:member) as! T @@ -122,6 +122,22 @@ public class ObservableKVOWrapper: ObservableObject { self.setWrapper(for:member, value:newValue as AnyObject?) } } + + @inlinable + public static func ==(lhs: ObservableKVOWrapper, rhs: ObservableKVOWrapper) -> Bool { + return lhs.obj.isEqual(rhs.obj) + } + + // see https://stackoverflow.com/a/33320737 + @inlinable + public static func ===(lhs: ObservableKVOWrapper, rhs: ObservableKVOWrapper) -> Bool { + return lhs.obj === rhs.obj + } + + @inlinable + public func hash(into hasher: inout Hasher) { + hasher.combine(self.obj.hashValue) + } } @objcMembers diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index bdf9eaa922..4f25c63345 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -16,6 +16,7 @@ import PhotosUI import Combine import FLAnimatedImage +import OrderedCollections extension MLContact : Identifiable {} //make MLContact be usable in swiftui ForEach clauses @@ -158,26 +159,33 @@ extension DocumentPickerViewController: UIDocumentPickerDelegate { // clear button for text fields, see https://stackoverflow.com/a/58896723/3528174 struct ClearButton: ViewModifier { + let isEditing: Bool @Binding var text: String + public func body(content: Content) -> some View { - ZStack(alignment: .trailing) { + HStack { content - if(!text.isEmpty) { - Button(action: { + .accessibilitySortPriority(2) + + if isEditing, !text.isEmpty { + Button { self.text = "" - }) { - Image(systemName: "delete.left") - .foregroundColor(Color(UIColor.opaqueSeparator)) + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundColor(Color(UIColor.tertiaryLabel)) + .accessibilityLabel("Clear text") } .padding(.trailing, 8) + .accessibilitySortPriority(1) } } } } //this extension contains the easy-access view modifier extension View { - func addClearButton(text: Binding) -> some View { - modifier(ClearButton(text:text)) + /// Puts the view in an HStack and adds a clear button to the right when the text is not empty. + func addClearButton(isEditing: Bool, text: Binding) -> some View { + modifier(ClearButton(isEditing: isEditing, text:text)) } } @@ -319,42 +327,16 @@ struct AlertPrompt { var dismissLabel: Text = Text("Close") } -//see https://www.avanderlee.com/swiftui/conditional-view-modifier/ extension View { - /// Applies the given transform if the given condition evaluates to `true`. + /// Applies the given transform. + /// + /// Useful for availability branching on view modifiers. Do not branch with any properties that may change during runtime as this will cause errors. /// - Parameters: - /// - condition: The condition to evaluate. /// - transform: The transform to apply to the source `View`. - /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. - @ViewBuilder func `if`(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View { - if condition() { - transform(self) - } else { - self - } - } - - @ViewBuilder func `if`(closure condition: () -> Bool, transform: (Self) -> Content) -> some View { - if condition() { - transform(self) - } else { - self - } - } -} - -func iOS15() -> Bool { - guard #available(iOS 15, *) else { - return true - } - return false -} - -func iOS16() -> Bool { - guard #available(iOS 16, *) else { - return true + /// - Returns: The view transformed by the transform. + func applyClosure(@ViewBuilder _ transform: (Self) -> Content) -> some View { + transform(self) } - return false } // Interfaces between ObjectiveC/Storyboards and SwiftUI @@ -473,9 +455,7 @@ class SwiftuiInterface : NSObject { case "ContactRequests": host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: ContactRequestsMenu(delegate: delegate))) case "CreateGroup": - host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: CreateGroupMenu(delegate: delegate, dismissWithNewGroup: { contact in - // FIXME - }))) + host.rootView = AnyView(AddTopLevelNavigation(withDelegate: delegate, to: CreateGroupMenu(delegate: delegate))) case "ChatPlaceholder": host.rootView = AnyView(ChatPlaceholder()) default: @@ -485,12 +465,12 @@ class SwiftuiInterface : NSObject { } } -func getContactList(viewContact: (ObservableKVOWrapper?)) -> [ObservableKVOWrapper] { +func getContactList(viewContact: (ObservableKVOWrapper?)) -> OrderedSet> { if let contact = viewContact { if(contact.isGroup && contact.mucType == "group") { //this uses the account the muc belongs to and treats every other account to be remote, even when multiple accounts of the same monal instance are in the same group let jidList = Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc: contact.contactJid, forAccountId: contact.accountId)) - var contactList : [ObservableKVOWrapper] = [] + var contactList : OrderedSet> = OrderedSet() for jidDict in jidList { //jid can be participant_jid (if currently joined to muc) or member_jid (if not joined but member of muc) var jid : String? = jidDict["participant_jid"] as? String diff --git a/Monal/Classes/WebRTCClient.swift b/Monal/Classes/WebRTCClient.swift index ab161c98a4..481ed73ad7 100644 --- a/Monal/Classes/WebRTCClient.swift +++ b/Monal/Classes/WebRTCClient.swift @@ -32,7 +32,7 @@ final class WebRTCClient: NSObject { @objc public let peerConnection: RTCPeerConnection private let rtcAudioSession = RTCAudioSession.sharedInstance() private let audioQueue = DispatchQueue(label: "audio") - private let streamId = "stream" + private let streamId = "-" private var mediaConstrains: [String:String] = [:] private var videoCapturer: RTCVideoCapturer? private var localVideoTrack: RTCVideoTrack? @@ -250,7 +250,7 @@ final class WebRTCClient: NSObject { private func createAudioTrack() -> RTCAudioTrack { let audioConstrains = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) let audioSource = WebRTCClient.factory.audioSource(with: audioConstrains) - let audioTrack = WebRTCClient.factory.audioTrack(with: audioSource, trackId: "audio0") + let audioTrack = WebRTCClient.factory.audioTrack(with: audioSource, trackId: "audio-"+UUID().uuidString) return audioTrack } @@ -274,7 +274,7 @@ final class WebRTCClient: NSObject { method_setImplementation(swizzledMethod!, replacementImplementation) } - let videoTrack = WebRTCClient.factory.videoTrack(with: videoSource, trackId: "video0") + let videoTrack = WebRTCClient.factory.videoTrack(with: videoSource, trackId: "video-"+UUID().uuidString) return videoTrack } @@ -362,6 +362,7 @@ extension WebRTCClient { setTrackEnabled(RTCVideoTrack.self, isEnabled: isEnabled) } } + // MARK:- Audio control extension WebRTCClient { @objc diff --git a/Monal/Classes/WelcomeLogIn.swift b/Monal/Classes/WelcomeLogIn.swift index e1d68f2585..765852a813 100644 --- a/Monal/Classes/WelcomeLogIn.swift +++ b/Monal/Classes/WelcomeLogIn.swift @@ -14,7 +14,9 @@ struct WelcomeLogIn: View { var delegate: SheetDismisserProtocol + @State private var isEditingJid: Bool = false @State private var jid: String = "" + @State private var isEditingPassword: Bool = false @State private var password: String = "" @State private var showAlert = false @@ -25,9 +27,10 @@ struct WelcomeLogIn: View { @State private var errorObserverEnabled = false @State private var newAccountNo: NSNumber? = nil @State private var loginComplete = false + @State private var isLoadingOmemoBundles = false @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) - @ObservedObject private var overlay = LoadingOverlayState() + @StateObject private var overlay = LoadingOverlayState() #if IS_ALPHA let appLogoId = "AlphaAppLogo" @@ -56,9 +59,10 @@ struct WelcomeLogIn: View { } private func showTimeoutAlert() { + DDLogVerbose("Showing timeout alert...") hideLoadingOverlay(overlay) alertPrompt.title = Text("Timeout Error") - alertPrompt.message = Text("We were not able to connect your account. Please check your credentials and make sure you are connected to the internet.") + alertPrompt.message = Text("We were not able to connect your account. Please check your username and password and make sure you are connected to the internet.") showAlert = true } @@ -72,7 +76,7 @@ struct WelcomeLogIn: View { private func showLoginErrorAlert(errorMessage: String) { hideLoadingOverlay(overlay) alertPrompt.title = Text("Error") - alertPrompt.message = Text(String(format: NSLocalizedString("We were not able to connect your account. Please check your credentials and make sure you are connected to the internet.\n\nTechnical error message: %@", comment: ""), errorMessage)) + alertPrompt.message = Text(String(format: NSLocalizedString("We were not able to connect your account. Please check your username and password and make sure you are connected to the internet.\n\nTechnical error message: %@", comment: ""), errorMessage)) showAlert = true } @@ -98,7 +102,9 @@ struct WelcomeLogIn: View { self.currentTimeout = newTimeout DispatchQueue.main.asyncAfter(deadline: newTimeout) { if(newTimeout == self.currentTimeout) { + DDLogWarn("First login timeout triggered...") if(self.newAccountNo != nil) { + DDLogVerbose("Removing account...") MLXMPPManager.sharedInstance().removeAccount(forAccountNo: self.newAccountNo!) self.newAccountNo = nil } @@ -133,14 +139,17 @@ struct WelcomeLogIn: View { TextField(NSLocalizedString("user@domain.tld", comment: "placeholder when adding account"), text: Binding( get: { self.jid }, - set: { string in self.jid = string.lowercased().replacingOccurrences(of: " ", with: "") }) + set: { string in self.jid = string.lowercased().replacingOccurrences(of: " ", with: "") }), onEditingChanged: { isEditingJid = $0 } ) //ios15: .textInputAutocapitalization(.never) .autocapitalization(.none) .autocorrectionDisabled() .keyboardType(.emailAddress) + .addClearButton(isEditing: isEditingJid, text: $jid) SecureField(NSLocalizedString("Password", comment: "placeholder when adding account"), text: $password) + .addClearButton(isEditing: password.count > 0 + , text: $password) HStack() { Button(action: { @@ -257,6 +266,7 @@ struct WelcomeLogIn: View { .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalUpdateBundleFetchStatus")).receive(on: RunLoop.main)) { notification in if let notificationAccountNo = notification.userInfo?["accountNo"] as? NSNumber, let completed = notification.userInfo?["completed"] as? NSNumber, let all = notification.userInfo?["all"] as? NSNumber, let newAccountNo : NSNumber = self.newAccountNo { if(notificationAccountNo.intValue == newAccountNo.intValue) { + isLoadingOmemoBundles = true DispatchQueue.main.async { showLoadingOverlay( overlay, @@ -267,10 +277,9 @@ struct WelcomeLogIn: View { } } } - /* .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalFinishedOmemoBundleFetch")).receive(on: RunLoop.main)) { notification in if let notificationAccountNo = notification.userInfo?["accountNo"] as? NSNumber, let newAccountNo : NSNumber = self.newAccountNo { - if(notificationAccountNo.intValue == newAccountNo.intValue) { + if(notificationAccountNo.intValue == newAccountNo.intValue && isLoadingOmemoBundles) { DispatchQueue.main.async { self.loginComplete = true showSuccessAlert() @@ -278,10 +287,9 @@ struct WelcomeLogIn: View { } } } - */ .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalFinishedCatchup")).receive(on: RunLoop.main)) { notification in if let xmppAccount = notification.object as? xmpp, let newAccountNo : NSNumber = self.newAccountNo { - if(xmppAccount.accountNo.intValue == newAccountNo.intValue) { + if(xmppAccount.accountNo.intValue == newAccountNo.intValue && !isLoadingOmemoBundles) { DispatchQueue.main.async { self.loginComplete = true showSuccessAlert() diff --git a/Monal/Classes/XMPPEdit.m b/Monal/Classes/XMPPEdit.m index 0e3373ac14..f0847cd9eb 100644 --- a/Monal/Classes/XMPPEdit.m +++ b/Monal/Classes/XMPPEdit.m @@ -482,7 +482,12 @@ - (IBAction) removeAccountClicked: (id) sender }]; [questionAlert addAction:noAction]; [questionAlert addAction:yesAction]; - questionAlert.popoverPresentationController.sourceView = sender; + + UIPopoverPresentationController* popPresenter = [questionAlert popoverPresentationController]; + if(@available(iOS 16.0, macCatalyst 16.0, *)) + popPresenter.sourceItem = sender; + else + popPresenter.barButtonItem = sender; [self presentViewController:questionAlert animated:YES completion:nil]; } @@ -538,7 +543,12 @@ -(IBAction) deleteAccountClicked:(id) sender }]; [questionAlert addAction:noAction]; [questionAlert addAction:yesAction]; - questionAlert.popoverPresentationController.sourceView = sender; + + UIPopoverPresentationController* popPresenter = [questionAlert popoverPresentationController]; + if(@available(iOS 16.0, macCatalyst 16.0, *)) + popPresenter.sourceItem = sender; + else + popPresenter.barButtonItem = sender; [self presentViewController:questionAlert animated:YES completion:nil]; } @@ -568,7 +578,12 @@ - (IBAction) clearHistoryClicked: (id) sender [questionAlert addAction:noAction]; [questionAlert addAction:yesAction]; - questionAlert.popoverPresentationController.sourceView = sender; + + UIPopoverPresentationController* popPresenter = [questionAlert popoverPresentationController]; + if(@available(iOS 16.0, macCatalyst 16.0, *)) + popPresenter.sourceItem = sender; + else + popPresenter.barButtonItem = sender; [self presentViewController:questionAlert animated:YES completion:nil]; diff --git a/Monal/Classes/XMPPIQ.h b/Monal/Classes/XMPPIQ.h index 84e6f49f7f..3b7141b0c0 100644 --- a/Monal/Classes/XMPPIQ.h +++ b/Monal/Classes/XMPPIQ.h @@ -87,7 +87,7 @@ FOUNDATION_EXPORT NSString* const kiqErrorType; /** gets Entity SoftWare Version */ --(void) getEntitySoftWareVersionTo:(NSString*) to; +-(void) getEntitySoftwareVersionInfo; /** removes a contact from the roster diff --git a/Monal/Classes/XMPPIQ.m b/Monal/Classes/XMPPIQ.m index 49b0436885..9643747a7a 100644 --- a/Monal/Classes/XMPPIQ.m +++ b/Monal/Classes/XMPPIQ.m @@ -340,13 +340,9 @@ -(void) httpUploadforFile:(NSString *) file ofSize:(NSNumber *) filesize andCont #pragma mark iq get --(void) getEntitySoftWareVersionTo:(NSString*) to +-(void) getEntitySoftwareVersionInfo { - [self setiqTo:to]; - - MLXMLNode* queryNode = [[MLXMLNode alloc] initWithElement:@"query" andNamespace:@"jabber:iq:version"]; - - [self addChildNode:queryNode]; + [self addChildNode:[[MLXMLNode alloc] initWithElement:@"query" andNamespace:@"jabber:iq:version"]]; } #pragma mark MUC diff --git a/Monal/Classes/XMPPMessage.m b/Monal/Classes/XMPPMessage.m index 3cfa2150f6..d93fb7ed8a 100644 --- a/Monal/Classes/XMPPMessage.m +++ b/Monal/Classes/XMPPMessage.m @@ -35,18 +35,14 @@ -(XMPPMessage*) init -(XMPPMessage*) initWithType:(NSString*) type to:(NSString*) to { - self = [self init]; - self.attributes[@"type"] = type; + self = [self initWithType:type]; self.attributes[@"to"] = to; - return self; } -(XMPPMessage*) initToContact:(MLContact*) toContact { - self = [self init]; - self.attributes[@"to"] = toContact.contactJid; - + self = [self initWithType:(toContact.isGroup ? kMessageGroupChatType : kMessageChatType) to:toContact.contactJid]; return self; } @@ -54,7 +50,6 @@ -(XMPPMessage*) initWithType:(NSString*) type { self = [self init]; self.attributes[@"type"] = type; - return self; } diff --git a/Monal/Classes/XMPPPresence.m b/Monal/Classes/XMPPPresence.m index 5659399084..7ad0c9b7b7 100644 --- a/Monal/Classes/XMPPPresence.m +++ b/Monal/Classes/XMPPPresence.m @@ -31,7 +31,7 @@ -(id) initWithHash:(NSString*) version { self = [self init]; [self addChildNode:[[MLXMLNode alloc] initWithElement:@"c" andNamespace:@"http://jabber.org/protocol/caps" withAttributes:@{ - @"node": @"http://monal-im.org/", + @"node": @"https://monal-im.org/", @"hash": @"sha-1", @"ver": version } andChildren:@[] andData:nil]]; diff --git a/Monal/Classes/chatViewController.h b/Monal/Classes/chatViewController.h index 418a80c6a8..1fcf7b497e 100644 --- a/Monal/Classes/chatViewController.h +++ b/Monal/Classes/chatViewController.h @@ -41,11 +41,12 @@ @property (weak, nonatomic) IBOutlet UICollectionView* uploadMenuView; @property (nonatomic, weak) IBOutlet UIView* inputContainerView; -@property (nonatomic, strong) IBOutlet UIView* navBarContainerView; @property (nonatomic, weak) IBOutlet NSLayoutConstraint* tableviewBottom; -@property (nonatomic, strong) IBOutlet UILabel* navBarContactJid; -@property (nonatomic, strong) IBOutlet UILabel* navBarLastInteraction; +@property (nonatomic, strong) UILabel* navBarContactJid; +@property (nonatomic, strong) UILabel* navBarLastInteraction; @property (nonatomic, strong) IBOutlet UIImageView* navBarIcon; +@property (nonatomic, strong) UIBarButtonItem* customHeader; + @property (weak, nonatomic) IBOutlet UIBarButtonItem* navBarEncryptToggleButton; @property (nonatomic, weak) IBOutlet UIImageView* backgroundImage; diff --git a/Monal/Classes/chatViewController.m b/Monal/Classes/chatViewController.m index 47ad312f29..8fb9211f6f 100644 --- a/Monal/Classes/chatViewController.m +++ b/Monal/Classes/chatViewController.m @@ -293,15 +293,27 @@ -(void) initNavigationBarItems self.navBarContactJid = [[UILabel alloc] initWithFrame:CGRectMake(38, 7, 200, 18)]; self.navBarLastInteraction = [[UILabel alloc] initWithFrame:CGRectMake(38, 26, 200, 12)]; - [self.navBarContactJid setFont:[UIFont systemFontOfSize:15.0]]; - [self.navBarLastInteraction setFont:[UIFont systemFontOfSize:10.0]]; + self.navBarContactJid.font = [UIFont systemFontOfSize:15.0]; + self.navBarLastInteraction.font = [UIFont systemFontOfSize:10.0]; [cusView addSubview:self.navBarIcon]; [cusView addSubview:self.navBarContactJid]; [cusView addSubview:self.navBarLastInteraction]; - UITapGestureRecognizer* customViewTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(commandIPressed:)]; - [cusView addGestureRecognizer:customViewTapRecognizer]; - self.navigationItem.leftBarButtonItems = @[[[UIBarButtonItem alloc] initWithCustomView:cusView]]; + + UITapGestureRecognizer* openContactDetailsTapAction = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(commandIPressed:)]; + [cusView addGestureRecognizer:openContactDetailsTapAction]; + + UIBarButtonItem* customViewButtonWithMultipleItems = [[UIBarButtonItem alloc] initWithCustomView:cusView]; + [customViewButtonWithMultipleItems setAction:@selector(commandIPressed:)]; + + // allow opening of contact details via voice over + [customViewButtonWithMultipleItems setIsAccessibilityElement:YES]; + [customViewButtonWithMultipleItems setAccessibilityTraits:UIAccessibilityTraitAllowsDirectInteraction]; + [customViewButtonWithMultipleItems setAccessibilityLabel:self.navBarContactJid.text]; + + self.customHeader = customViewButtonWithMultipleItems; + + self.navigationItem.leftBarButtonItems = @[customViewButtonWithMultipleItems]; self.navigationItem.leftItemsSupplementBackButton = YES; } @@ -485,11 +497,13 @@ -(void) setChatInputHeightConstraints:(BOOL) hwKeyboardPresent -(void) handleForeGround { - @synchronized(_localMLContactCache) { - [_localMLContactCache removeAllObjects]; - } - [self refreshData]; - [self reloadTable]; + dispatch_async(dispatch_get_main_queue(), ^{ + @synchronized(self->_localMLContactCache) { + [self->_localMLContactCache removeAllObjects]; + } + [self refreshData]; + [self reloadTable]; + }); } -(void) openCallScreen:(id) sender @@ -508,7 +522,7 @@ -(void) openCallScreen:(id) sender //now initiate call MonalAppDelegate* appDelegate = (MonalAppDelegate*)[[UIApplication sharedApplication] delegate]; - [appDelegate.activeChats callContact:self.contact]; + [appDelegate.activeChats callContact:self.contact withUIKitSender:sender]; }]]; [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { [self dismissViewControllerAnimated:YES completion:nil]; @@ -523,22 +537,18 @@ -(void) openCallScreen:(id) sender else { MonalAppDelegate* appDelegate = (MonalAppDelegate*)[[UIApplication sharedApplication] delegate]; - [appDelegate.activeChats callContact:self.contact]; + [appDelegate.activeChats callContact:self.contact withUIKitSender:sender]; } } --(IBAction) toggleEncryption:(id)sender +-(IBAction) toggleEncryption:(id) sender { if([HelperTools isContactBlacklistedForEncryption:self.contact]) return; #ifndef DISABLE_OMEMO if(self.contact.isEncrypted) { - NSInteger style = UIAlertControllerStyleActionSheet; -#if TARGET_OS_MACCATALYST - style = UIAlertControllerStyleAlert; -#endif - UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Disable encryption?", @"") message:NSLocalizedString(@"Do you really want to disable encryption for this contact?", @"") preferredStyle:style]; + UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Disable encryption?", @"") message:NSLocalizedString(@"Do you really want to disable encryption for this contact?", @"") preferredStyle:UIAlertControllerStyleActionSheet]; [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Yes, deactivate encryption", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [MLChatViewHelper toggleEncryptionForContact:self.contact withSelf:self afterToggle:^() { [self displayEncryptionStateInUI]; @@ -548,7 +558,11 @@ -(IBAction) toggleEncryption:(id)sender [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"No, keep encryption activated", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { [self dismissViewControllerAnimated:YES completion:nil]; }]]; - //alert.popoverPresentationController.sourceView = sender; + UIPopoverPresentationController* popPresenter = [alert popoverPresentationController]; + if(@available(iOS 16.0, macCatalyst 16.0, *)) + popPresenter.sourceItem = sender; + else + popPresenter.barButtonItem = sender; [self presentViewController:alert animated:YES completion:nil]; } else @@ -628,9 +642,10 @@ -(void) updateUIElements // change text values dispatch_async(dispatch_get_main_queue(), ^{ self.navBarContactJid.text = jidLabelText; + [self.customHeader setAccessibilityLabel:jidLabelText]; self.sendButton.enabled = sendButtonEnabled; [[MLImageManager sharedInstance] getIconForContact:self.contact withCompletion:^(UIImage *image) { - self.navBarIcon.image=image; + self.navBarIcon.image = image; }]; [self updateCallButtonImage]; @@ -903,13 +918,15 @@ -(void) dealloc -(void) handleBackgroundChanged { - DDLogVerbose(@"Loading background image for %@", self.contact); - self.backgroundImage.image = [[MLImageManager sharedInstance] getBackgroundFor:self.contact]; - //use default background if this contact does not have its own - if(self.backgroundImage.image == nil) - self.backgroundImage.image = [[MLImageManager sharedInstance] getBackgroundFor:nil]; - self.backgroundImage.hidden = self.backgroundImage.image == nil; - DDLogVerbose(@"Background is now: %@", self.backgroundImage.image); + dispatch_async(dispatch_get_main_queue(), ^{ + DDLogVerbose(@"Loading background image for %@", self.contact); + self.backgroundImage.image = [[MLImageManager sharedInstance] getBackgroundFor:self.contact]; + //use default background if this contact does not have its own + if(self.backgroundImage.image == nil) + self.backgroundImage.image = [[MLImageManager sharedInstance] getBackgroundFor:nil]; + self.backgroundImage.hidden = self.backgroundImage.image == nil; + DDLogVerbose(@"Background is now: %@", self.backgroundImage.image); + }); } #pragma mark rotation @@ -2538,7 +2555,9 @@ -(UISwipeActionsConfiguration*) tableView:(UITableView*) tableView trailingSwipe -(MLBaseCell*) fileTransferCellCheckerWithInfo:(NSDictionary*)info direction:(BOOL)inDirection tableView:(UITableView*)tableView andMsg:(MLMessage*)row{ MLBaseCell *cell = nil; - if([info[@"mimeType"] hasPrefix:@"image/"]) + //don't crash on svg images not supported by UIImage + //TODO: use webview to display SVG + if([info[@"mimeType"] hasPrefix:@"image/"] && ![info[@"mimeType"] hasPrefix:@"image/svg"]) { MLChatImageCell* imageCell = (MLChatImageCell *)[self messageTableCellWithIdentifier:@"image" andInbound:inDirection fromTable:tableView]; [imageCell initCellWithMLMessage:row]; @@ -2706,7 +2725,7 @@ -(void) loadOldMsgHistory:(id) sender { NSString* errorText = error; if(!error) - errorText = NSLocalizedString(@"All messages already present in local history!", @""); + errorText = NSLocalizedString(@"Unknown error!", @""); DDLogError(@"Got backscrolling mam error: %@", errorText); UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Could not fetch messages", @"") message:[NSString stringWithFormat:NSLocalizedString(@"Could not fetch (all) old messages for this chat from your server archive. Please try again later. %@", @""), errorText] preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { @@ -2716,11 +2735,19 @@ -(void) loadOldMsgHistory:(id) sender } else { - if([messages count] == 0) { + DDLogVerbose(@"Got backscrolling mam response: %lu", (unsigned long)[messages count]); + if([messages count] == 0) + { self.moreMessagesAvailable = NO; + + UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Finished fetching messages", @"") message:NSLocalizedString(@"All messages fetched successfully, there are no more left on the server!", @"") preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + [alert dismissViewControllerAnimated:YES completion:nil]; + }]]; + [self presentViewController:alert animated:YES completion:nil]; } - DDLogVerbose(@"Got backscrolling mam response: %lu", (unsigned long)[messages count]); - [self insertOldMessages:[[messages reverseObjectEnumerator] allObjects]]; + else + [self insertOldMessages:[[messages reverseObjectEnumerator] allObjects]]; } //allow next mam fetch self.isLoadingMam = NO; diff --git a/Monal/Classes/xmpp.h b/Monal/Classes/xmpp.h index 5aae41bad2..158380c27e 100644 --- a/Monal/Classes/xmpp.h +++ b/Monal/Classes/xmpp.h @@ -22,6 +22,7 @@ typedef NS_ENUM (NSInteger, xmppState) { kStateLoggedOut = -1, kStateDisconnected, // has connected once kStateReconnecting, + kStateConnected, kStateHasStream, kStateLoggedIn, kStateBinding, diff --git a/Monal/Classes/xmpp.m b/Monal/Classes/xmpp.m index f52a35550c..59c7ae6f46 100644 --- a/Monal/Classes/xmpp.m +++ b/Monal/Classes/xmpp.m @@ -49,7 +49,7 @@ @import AVFoundation; @import WebRTC; -#define STATE_VERSION 10 +#define STATE_VERSION 12 #define CONNECT_TIMEOUT 7.0 #define IQ_TIMEOUT 60.0 NSString* const kQueueID = @"queueID"; @@ -114,6 +114,7 @@ @interface xmpp() BOOL _lastIdleState; NSMutableDictionary* _mamPageArrays; NSString* _internalID; + NSString* _logtag; NSMutableDictionary* _inCatchup; //registration related stuff @@ -180,7 +181,8 @@ -(id) initWithServer:(nonnull MLXMPPServer*) server andIdentity:(nonnull MLXMPPI self = [super init]; u_int32_t i = arc4random(); _internalID = [HelperTools hexadecimalString:[NSData dataWithBytes: &i length: sizeof(i)]]; - DDLogVerbose(@"Created account %@ with id %@", accountNo, _internalID); + _logtag = [NSString stringWithFormat:@"[%@:%@]", accountNo, _internalID]; + DDLogVerbose(@"Creating account %@ with id %@", accountNo, _internalID); self.accountNo = accountNo; self.connectionProperties = [[MLXMPPConnection alloc] initWithServer:server andIdentity:identity]; @@ -557,12 +559,12 @@ -(void) createStreams if(self.connectionProperties.server.isDirectTLS == YES) { DDLogInfo(@"creating directTLS streams"); - [MLStream connectWithSNIDomain:self.connectionProperties.identity.domain connectHost:self.connectionProperties.server.connectServer connectPort:self.connectionProperties.server.connectPort tls:YES inputStream:&localIStream outputStream:&localOStream]; + [MLStream connectWithSNIDomain:self.connectionProperties.identity.domain connectHost:self.connectionProperties.server.connectServer connectPort:self.connectionProperties.server.connectPort tls:YES inputStream:&localIStream outputStream:&localOStream logtag:self->_logtag]; } else { DDLogInfo(@"creating plaintext streams"); - [MLStream connectWithSNIDomain:self.connectionProperties.identity.domain connectHost:self.connectionProperties.server.connectServer connectPort:self.connectionProperties.server.connectPort tls:NO inputStream:&localIStream outputStream:&localOStream]; + [MLStream connectWithSNIDomain:self.connectionProperties.identity.domain connectHost:self.connectionProperties.server.connectServer connectPort:self.connectionProperties.server.connectPort tls:NO inputStream:&localIStream outputStream:&localOStream logtag:self->_logtag]; } if(localOStream) @@ -718,10 +720,11 @@ -(void) freezeParseQueue //this has to be synchronous because we want to be sure no further stanzas are leaking from the parse queue //into the receive queue once we leave this method _parseQueue.suspended = YES; - MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES!"); [self dispatchOnReceiveQueue: ^{ + MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES (in receive queue)!"); DDLogWarn(@"Parse queue is frozen now!"); }]; + MLAssert([self parseQueueFrozen] == YES, @"Parse queue not frozen after setting suspended to YES!"); } -(void) unfreezeParseQueue @@ -830,6 +833,12 @@ -(void) connect if([[HelperTools defaultsDB] boolForKey:@"AutodeleteAllMessagesAfter3Days"]) [[DataLayer sharedInstance] autodeleteAllMessagesAfter3Days]; + if(_parseQueue.suspended) + { + DDLogWarn(@"Not trying to connect: parse queue frozen!"); + return; + } + [self dispatchAsyncOnReceiveQueue: ^{ [self->_parseQueue cancelAllOperations]; //throw away all parsed but not processed stanzas from old connections [self unfreezeParseQueue]; //make sure the parse queue is operational again @@ -1158,7 +1167,6 @@ -(void) closeSocket self->_accountState = kStateDisconnected; [self->_parseQueue cancelAllOperations]; //throw away all parsed but not processed stanzas (we should have closed sockets then!) - [self unfreezeParseQueue]; //make sure the cancelled operations get handled and our next connect can use the parse queue again //we don't throw away operations in the receive queue because they could be more than just stanzas //(for example outgoing messages that should be written to the smacks queue instead of just vanishing in a void) //all incoming stanzas in the receive queue will honor the _accountState being lower than kStateReconnecting and be dropped @@ -1219,8 +1227,11 @@ -(void) reconnectWithStreamError:(MLXMLNode* _Nullable) streamError andWaitingTi self->_reconnectInProgress = NO; }]; }), (^{ - DDLogInfo(@"Reconnect got aborted..."); - self->_reconnectInProgress = NO; + DDLogInfo(@"Reconnect got aborted: %@", self); + self->_cancelReconnectTimer = nil; + [self dispatchAsyncOnReceiveQueue: ^{ + self->_reconnectInProgress = NO; + }]; })); DDLogInfo(@"reconnect exits"); }]; @@ -1233,58 +1244,41 @@ -(void) prepareXMPPParser BOOL appex = [HelperTools isAppExtension]; if(_xmlParser!=nil) { - DDLogInfo(@"resetting old xml parser"); + DDLogInfo(@"%@: resetting old xml parser", self->_logtag); [_xmlParser setDelegate:nil]; [_xmlParser abortParsing]; [_parseQueue cancelAllOperations]; //throw away all parsed but not processed stanzas (we aborted the parser right now) } if(!_baseParserDelegate) { - DDLogInfo(@"creating parser delegate"); + DDLogInfo(@"%@: creating parser delegate", self->_logtag); _baseParserDelegate = [[MLBasePaser alloc] initWithCompletion:^(MLXMLNode* _Nullable parsedStanza) { - DDLogVerbose(@"Parse finished for new <%@> stanza...", parsedStanza.element); - - if(!appex) - { - //don't parse any more if we reached > 50 stanzas already parsed and waiting in parse queue - //this makes ure we don't need to much memory while parsing a flood of stanzas and, in theory, - //should create a backpressure ino the tcp stream, too - //the calculated sleep time gives every stanza in the queue ~10ms to be handled - BOOL wasSleeping = NO; - while([self->_parseQueue operationCount] > 50 && self.accountState >= kStateReconnecting) - { - double waittime = (double)[self->_parseQueue operationCount] / 100.0; - DDLogInfo(@"Sleeping %f seconds because parse queue has %lu entries (used/available memory: %.3fMiB / %.3fMiB)...", waittime, (unsigned long)[self->_parseQueue operationCount], [HelperTools report_memory], (CGFloat)os_proc_available_memory() / 1048576); - [NSThread sleepForTimeInterval:waittime]; - wasSleeping = YES; - } - if(wasSleeping) - DDLogInfo(@"Sleeping has ended, parse queue has %lu entries and used/available memory: %.3fMiB / %.3fMiB...", (unsigned long)[self->_parseQueue operationCount], [HelperTools report_memory], (CGFloat)os_proc_available_memory() / 1048576); - } - else + DDLogVerbose(@"%@: Parse finished for new <%@> stanza...", self->_logtag, parsedStanza.element); + + //don't parse any more if we reached > 50 stanzas already parsed and waiting in parse queue + //this makes ure we don't need to much memory while parsing a flood of stanzas and, in theory, + //should create a backpressure ino the tcp stream, too + //the calculated sleep time gives every stanza in the queue ~10ms to be handled (based on statistics) + BOOL wasSleeping = NO; + while(self.accountState >= kStateConnected) { - BOOL wasSleeping = NO; - while(self.accountState >= kStateReconnecting) - { - //use a much smaller limit while in appex because memory there is limited to ~32MiB - //like in the mainapp the calculated sleep time gives every stanza in the queue ~10ms to be handled - unsigned long operationCount = [self->_parseQueue operationCount]; - double usedMemory = [HelperTools report_memory]; - if(!(operationCount > 50 || (appex && usedMemory > 16 && operationCount > MAX(2, 24 - usedMemory)))) - break; - - double waittime = (double)[self->_parseQueue operationCount] / 100.0; - DDLogInfo(@"Sleeping %f seconds because parse queue has %lu entries and used/available memory: %.3fMiB / %.3fMiB...", waittime, (unsigned long)[self->_parseQueue operationCount], usedMemory, (CGFloat)os_proc_available_memory() / 1048576); - [NSThread sleepForTimeInterval:waittime]; - wasSleeping = YES; - } - if(wasSleeping) - DDLogInfo(@"Sleeping has ended, parse queue has %lu entries and used/available memory: %.3fMiB / %.3fMiB...", (unsigned long)[self->_parseQueue operationCount], [HelperTools report_memory], (CGFloat)os_proc_available_memory() / 1048576); + //use a much smaller limit while in appex because memory there is limited to ~32MiB + unsigned long operationCount = [self->_parseQueue operationCount]; + double usedMemory = [HelperTools report_memory]; + if(!(operationCount > 50 || (appex && usedMemory > 16 && operationCount > MAX(2, 24 - usedMemory)))) + break; + + double waittime = (double)[self->_parseQueue operationCount] / 100.0; + DDLogInfo(@"%@: Sleeping %f seconds because parse queue has %lu entries and used/available memory: %.3fMiB / %.3fMiB...", self->_logtag, waittime, (unsigned long)[self->_parseQueue operationCount], usedMemory, (CGFloat)os_proc_available_memory() / 1048576); + [NSThread sleepForTimeInterval:waittime]; + wasSleeping = YES; } + if(wasSleeping) + DDLogInfo(@"%@: Sleeping has ended, parse queue has %lu entries and used/available memory: %.3fMiB / %.3fMiB...", self->_logtag, (unsigned long)[self->_parseQueue operationCount], [HelperTools report_memory], (CGFloat)os_proc_available_memory() / 1048576); - if(self.accountState < kStateReconnecting) + if(self.accountState < kStateConnected) { - DDLogWarn(@"Throwing away incoming stanza *before* queueing in parse queue, accountState < kStateReconnecting"); + DDLogWarn(@"%@: Throwing away incoming stanza *before* queueing in parse queue, accountState < kStateConnected", self->_logtag); return; } @@ -1328,9 +1322,9 @@ -(void) prepareXMPPParser //use a synchronous dispatch to make sure no (old) tcp buffers of disconnected connections leak into the receive queue on app unfreeze DDLogVerbose(@"Synchronously handling next stanza on receive queue (%lu stanzas queued in parse queue, %lu current operations in receive queue, %.3fMiB / %.3fMiB memory used / available)", [self->_parseQueue operationCount], [self->_receiveQueue operationCount], [HelperTools report_memory], (CGFloat)os_proc_available_memory() / 1048576); [self->_receiveQueue addOperations:@[[NSBlockOperation blockOperationWithBlock:^{ - if(self.accountState_logtag); [_baseParserDelegate reset]; } @@ -1369,10 +1363,10 @@ -(void) prepareXMPPParser [_xmlParser setDelegate:_baseParserDelegate]; // do the stanza parsing in the low priority (=utility) global queue - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ - DDLogInfo(@"calling parse"); + dispatch_async(dispatch_queue_create_with_target([NSString stringWithFormat:@"im.monal.xmlparser%@", self->_logtag].UTF8String, DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)), ^{ + DDLogInfo(@"%@: calling parse", self->_logtag); [self->_xmlParser parse]; //blocking operation - DDLogInfo(@"parse ended"); + DDLogInfo(@"%@: parse ended", self->_logtag); }); } @@ -1869,6 +1863,13 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR [self incrementLastHandledStanzaWithDelayedReplay:delayedReplay]; return; } + + if(![[HelperTools defaultsDB] boolForKey: @"allowNonRosterContacts"] && !contact.isSubscribedFrom) + { + //mark this stanza as handled + [self incrementLastHandledStanzaWithDelayedReplay:delayedReplay]; + return; + } if(![presenceNode check:@"/@type"]) { @@ -1933,7 +1934,7 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR BOOL shouldQueryCaps = NO; if(![@"sha-1" isEqualToString:[presenceNode findFirst:@"{http://jabber.org/protocol/caps}c@hash"]]) { - DDLogWarn(@"Unknown caps hash algo '%@', querying disco without checking hash!", [presenceNode findFirst:@"{http://jabber.org/protocol/caps}c@hash"]); + DDLogWarn(@"Unknown caps hash algo '%@', requesting disco query without checking hash!", [presenceNode findFirst:@"{http://jabber.org/protocol/caps}c@hash"]); shouldQueryCaps = YES; } else @@ -1942,28 +1943,26 @@ -(void) processInput:(MLXMLNode*) parsedStanza withDelayedReplay:(BOOL) delayedR if(!ver || ![ver isEqualToString:newVer]) //caps hash of resource changed [[DataLayer sharedInstance] setVer:newVer forUser:presenceNode.fromUser andResource:presenceNode.fromResource onAccountNo:self.accountNo]; - if(![[DataLayer sharedInstance] getCapsforVer:newVer]) + if(![[DataLayer sharedInstance] getCapsforVer:newVer onAccountNo:self.accountNo]) { - if([_runningCapsQueries containsObject:newVer]) - { - DDLogDebug(@"Presence included unknown caps hash %@, but disco query already running", newVer); - shouldQueryCaps = NO; - } - else - { - DDLogInfo(@"Presence included unknown caps hash %@, querying disco", newVer); - shouldQueryCaps = YES; - } + DDLogInfo(@"Presence included unknown caps hash %@, requesting disco query", newVer); + shouldQueryCaps = YES; } } if(shouldQueryCaps) { - XMPPIQ* discoInfo = [[XMPPIQ alloc] initWithType:kiqGetType]; - [discoInfo setiqTo:presenceNode.from]; - [discoInfo setDiscoInfoNode]; - [self sendIq:discoInfo withHandler:$newHandler(MLIQProcessor, handleEntityCapsDisco)]; - [_runningCapsQueries addObject:newVer]; + if([_runningCapsQueries containsObject:newVer]) + DDLogInfo(@"Presence included unknown caps hash %@, but disco query already running, not querying again", newVer); + else + { + DDLogInfo(@"Querying disco for caps hash: %@", newVer); + XMPPIQ* discoInfo = [[XMPPIQ alloc] initWithType:kiqGetType]; + [discoInfo setiqTo:presenceNode.from]; + [discoInfo setDiscoInfoNode]; + [self sendIq:discoInfo withHandler:$newHandler(MLIQProcessor, handleEntityCapsDisco)]; + [_runningCapsQueries addObject:newVer]; + } } } @@ -3365,8 +3364,7 @@ -(void) sendChatState:(BOOL) isTyping toContact:(nonnull MLContact*) contact if(self.accountState < kStateBound) return; - XMPPMessage* messageNode = [XMPPMessage new]; - messageNode.attributes[@"to"] = contact.contactJid; + XMPPMessage* messageNode = [[XMPPMessage alloc] initToContact:contact]; [messageNode setNoStoreHint]; if(isTyping) [messageNode addChildNode:[[MLXMLNode alloc] initWithElement:@"composing" andNamespace:@"http://jabber.org/protocol/chatstates"]]; @@ -3834,9 +3832,9 @@ -(void) queryDisco -(void) queryServerVersion { - XMPPIQ* serverVersion = [[XMPPIQ alloc] initWithType:kiqGetType]; - [serverVersion getEntitySoftWareVersionTo:self.connectionProperties.identity.domain]; - [self send:serverVersion]; + XMPPIQ* serverVersion = [[XMPPIQ alloc] initWithType:kiqGetType to:self.connectionProperties.identity.domain]; + [serverVersion getEntitySoftwareVersionInfo]; + [self sendIq:serverVersion withHandler:$newHandler(MLIQProcessor, handleVersionResponse)]; } -(void) queryExternalServicesOn:(NSString*) jid @@ -4024,6 +4022,10 @@ -(void) initSession -(void) addReconnectionHandler:(MLHandler*) handler { + //don't check if we are bound and execute the handler directly if so + //--> reconnect handlers are frequently used while being bound to schedule a task on *next* (re)connect + //--> in cases where the reconnect handler is only needed if we are not bound, the caller can do this check itself + // (this might introduce small race conditions, though, but these should be negligible in most cases) @synchronized(_reconnectionHandlers) { [_reconnectionHandlers addObject:handler]; } @@ -4059,15 +4061,15 @@ -(void) updateLocalBlocklistCache:(NSSet*) blockedJids #pragma mark vcard --(void) getEntitySoftWareVersion:(NSString*) user +-(void) getEntitySoftWareVersion:(NSString*) jid { - NSDictionary* jid = [HelperTools splitJid:user]; - MLAssert(jid[@"resource"] != nil, @"getEntitySoftWareVersion needs a full jid!"); - if([[DataLayer sharedInstance] checkCap:@"jabber:iq:version" forUser:jid[@"user"] andResource:jid[@"resource"] onAccountNo:self.accountNo]) + NSDictionary* split = [HelperTools splitJid:jid]; + MLAssert(split[@"resource"] != nil, @"getEntitySoftWareVersion needs a full jid!"); + if([[DataLayer sharedInstance] checkCap:@"jabber:iq:version" forUser:split[@"user"] andResource:split[@"resource"] onAccountNo:self.accountNo]) { - XMPPIQ* iqEntitySoftWareVersion = [[XMPPIQ alloc] initWithType:kiqGetType]; - [iqEntitySoftWareVersion getEntitySoftWareVersionTo:user]; - [self send:iqEntitySoftWareVersion]; + XMPPIQ* iqEntitySoftWareVersion = [[XMPPIQ alloc] initWithType:kiqGetType to:jid]; + [iqEntitySoftWareVersion getEntitySoftwareVersionInfo]; + [self sendIq:iqEntitySoftWareVersion withHandler:$newHandler(MLIQProcessor, handleVersionResponse)]; } } @@ -4256,6 +4258,7 @@ -(void) setMAMQueryMostRecentForContact:(MLContact*) contact before:(NSString*) [[DataLayer sharedInstance] createTransaction:^{ DDLogVerbose(@"Handling mam page entry[%u(%@).%u(%@)]): %@", pageNo, @([pageList count]), entryNo, @([page count]), data); MLMessage* msg = [MLMessageProcessor processMessage:data[@"messageNode"] andOuterMessage:data[@"outerMessageNode"] forAccount:self withHistoryId:historyId]; + DDLogVerbose(@"Got message processor result: %@", msg); //add successfully added messages to our display list //stanzas not transporting a body will be processed, too, but the message processor will return nil for these if(msg != nil) @@ -4281,16 +4284,8 @@ -(void) setMAMQueryMostRecentForContact:(MLContact*) contact before:(NSString*) })); if([historyIdList count] < retrievedBodies) DDLogWarn(@"Got %lu mam history messages already contained in history db, possibly ougoing messages that did not have a stanzaid yet!", (unsigned long)(retrievedBodies - [historyIdList count])); - if(![historyIdList count]) - { - //call completion with nil to signal an error, if we could not get any messages not yet in history db - completion(nil, nil); - } - else - { - //query db (again) for the real MLMessage to account for changes in history table by non-body metadata messages received after the body-message - completion([[DataLayer sharedInstance] messagesForHistoryIDs:historyIdList], nil); - } + //query db (again) for the real MLMessage to account for changes in history table by non-body metadata messages received after the body-message + completion([[DataLayer sharedInstance] messagesForHistoryIDs:historyIdList], nil); }; responseHandler = ^(XMPPIQ* response) { NSMutableArray* mamPage = [self getOrderedMamPageFor:[response findFirst:@"/@id"]]; @@ -4378,7 +4373,7 @@ -(void) joinMuc:(NSString* _Nonnull) room -(void) leaveMuc:(NSString* _Nonnull) room { - [self.mucProcessor leave:room withBookmarksUpdate:YES]; + [self.mucProcessor leave:room withBookmarksUpdate:YES keepBuddylistEntry:NO]; } -(void) checkJidType:(NSString*) jid withCompletion:(void (^)(NSString* type, NSString* _Nullable errorMessage)) completion @@ -4649,13 +4644,15 @@ -(void)stream:(NSStream*) stream handleEvent:(NSStreamEvent) eventCode if(_cancelLoginTimer != nil && self->_accountState < kStateLoggedIn) [self reinitLoginTimer]; - if(_blockToCallOnTCPOpen != nil) - { - [self dispatchAsyncOnReceiveQueue:^{ + //we want this to be sync instead of async to make sure we are in kStateConnected before sending anything + [self dispatchOnReceiveQueue:^{ + self->_accountState = kStateConnected; + if(self->_blockToCallOnTCPOpen != nil) + { self->_blockToCallOnTCPOpen(); self->_blockToCallOnTCPOpen = nil; //don't call this twice - }]; - } + } + }]; } break; } @@ -5064,7 +5061,7 @@ -(void) updateIqHandlerTimeouts _iqHandlers[iqid][@"timeout"] = @([_iqHandlers[iqid][@"timeout"] doubleValue] - 1.0); if([_iqHandlers[iqid][@"timeout"] doubleValue] < 0.0) { - DDLogWarn(@"Timeout of handler triggered: %@", _iqHandlers[iqid]); + DDLogWarn(@"%@: Timeout of handler triggered: %@", _logtag, _iqHandlers[iqid]); //only force save state after calling a handler //(timeout changes that don't make it to disk only extend the timeout by a few seconds but don't have any negative sideeffect) stateUpdated = YES; @@ -5103,7 +5100,7 @@ -(void) updateIqHandlerTimeouts }]] waitUntilFinished:NO]; } else - DDLogWarn(@"iq handler for '%@' vanished while switching to receive queue", iqid); + DDLogError(@"%@: iq handler for '%@' vanished while switching to receive queue", _logtag, iqid); } } //now delete iqs marked for deletion @@ -5279,10 +5276,7 @@ -(void) sendDisplayMarkerForMessage:(MLMessage*) msg return; } - XMPPMessage* displayedNode = [XMPPMessage new]; - //the message type is needed so that the store hint is accepted by the server - displayedNode.attributes[@"type"] = msg.isMuc ? @"groupchat" : @"chat"; - displayedNode.attributes[@"to"] = msg.inbound ? msg.buddyName : self.connectionProperties.identity.jid; + XMPPMessage* displayedNode = [[XMPPMessage alloc] initToContact:contact]; [displayedNode setDisplayed:msg.isMuc && msg.stanzaId != nil ? msg.stanzaId : msg.messageId]; [displayedNode setStoreHint]; DDLogVerbose(@"Sending display marker: %@", displayedNode); diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 269ec37e6b..e5d7f9b90f 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -170,12 +170,15 @@ C1049189261301530054AC9E /* MonalXMPPUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1049188261301530054AC9E /* MonalXMPPUnitTests.swift */; }; C104918B261301530054AC9E /* monalxmpp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26CC579223A0867400ABB92A /* monalxmpp.framework */; }; C1049199261301710054AC9E /* MLCryptoTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1049198261301710054AC9E /* MLCryptoTest.swift */; }; + C114D13D2B15B903000FB99F /* ContactEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C114D13C2B15B903000FB99F /* ContactEntry.swift */; }; C117F7E12B086390001F2BC6 /* CreateGroupMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D88BB76295BB6DC00FB30BA /* CreateGroupMenu.swift */; }; C117F7E22B0863B3001F2BC6 /* ContactPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D631822294BAB1D00026BE7 /* ContactPicker.swift */; }; C12436142434AB5D00B8F074 /* MLAttributedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = C12436132434AB5D00B8F074 /* MLAttributedLabel.m */; }; C13A0BCE26E78B7B00987E29 /* ContactDetailsHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19C919A26E26AF000F8CC57 /* ContactDetailsHeader.swift */; }; C13EBB8E24DC685C008AADDA /* MLPrivacySettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C13EBB8D24DC685C008AADDA /* MLPrivacySettingsViewController.m */; }; C1414E9D24312F0100948788 /* MLChatMapsCell.m in Sources */ = {isa = PBXBuildFile; fileRef = C1414E9C24312F0100948788 /* MLChatMapsCell.m */; }; + C153825F2B89BBE600EA83EC /* GroupDetailsEdit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C153825E2B89BBE600EA83EC /* GroupDetailsEdit.swift */; }; + C15382622B89C38300EA83EC /* EditGroupName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15382612B89C38300EA83EC /* EditGroupName.swift */; }; C15489B925680BBE00BBA2F0 /* MLQRCodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15489B825680BBE00BBA2F0 /* MLQRCodeScanner.swift */; }; C158D40025A0AB810005AA40 /* MLMucProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = C158D3FE25A0AB810005AA40 /* MLMucProcessor.h */; }; C158D41425A0AC630005AA40 /* MLMucProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = C158D41225A0AC630005AA40 /* MLMucProcessor.m */; }; @@ -186,6 +189,7 @@ C176F1EC2AF11C31002034E5 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C176F1EB2AF11C31002034E5 /* UserNotifications.framework */; }; C1850EB825F38A2D003D506A /* MonalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1850EB725F38A2D003D506A /* MonalUITests.swift */; }; C1850EC625F3C5EB003D506A /* TestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1850EC525F3C5EB003D506A /* TestHelper.swift */; }; + C18967C72B81F61B0073C7C5 /* ChannelMemberList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18967C62B81F61B0073C7C5 /* ChannelMemberList.swift */; }; C18E757A245E8AE900AE8FB7 /* MLPipe.h in Headers */ = {isa = PBXBuildFile; fileRef = C18E7578245E8AE900AE8FB7 /* MLPipe.h */; }; C18E757C245E8AE900AE8FB7 /* MLPipe.m in Sources */ = {isa = PBXBuildFile; fileRef = C18E7579245E8AE900AE8FB7 /* MLPipe.m */; }; C1943A4C25309A9D0036172F /* MLReloadCell.m in Sources */ = {isa = PBXBuildFile; fileRef = C1943A4B25309A9D0036172F /* MLReloadCell.m */; }; @@ -198,6 +202,7 @@ C1D7D7B0283FB4E700401389 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 26470F511835C4080069E3E0 /* Media.xcassets */; }; C1E1EC7B286A025F0097EC74 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = C1E1EC7A286A025F0097EC74 /* SwiftSoup */; }; C1E4654824EE517000CA5AAF /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1E4654624EE517000CA5AAF /* Localizable.strings */; }; + C1E8A7F72B8E47C300760220 /* EditGroupSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E8A7F62B8E47C300760220 /* EditGroupSubject.swift */; }; C1F5C7A92775DA000001F295 /* MLContactSoftwareVersionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = C1F5C7A72775DA000001F295 /* MLContactSoftwareVersionInfo.h */; }; C1F5C7AA2775DA000001F295 /* MLContactSoftwareVersionInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = C1F5C7A82775DA000001F295 /* MLContactSoftwareVersionInfo.m */; }; C1F5C7AC2777621B0001F295 /* ContactResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F5C7AB2777621B0001F295 /* ContactResources.swift */; }; @@ -614,6 +619,7 @@ C1049188261301530054AC9E /* MonalXMPPUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonalXMPPUnitTests.swift; sourceTree = ""; }; C104918A261301530054AC9E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C1049198261301710054AC9E /* MLCryptoTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLCryptoTest.swift; sourceTree = ""; }; + C114D13C2B15B903000FB99F /* ContactEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactEntry.swift; sourceTree = ""; }; C12436122434AB5D00B8F074 /* MLAttributedLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLAttributedLabel.h; sourceTree = ""; }; C12436132434AB5D00B8F074 /* MLAttributedLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLAttributedLabel.m; sourceTree = ""; }; C132EA9426C92DD900BB9A67 /* pa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pa; path = external/pa.lproj/Main.strings; sourceTree = ""; }; @@ -631,6 +637,8 @@ C13EBB8D24DC685C008AADDA /* MLPrivacySettingsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLPrivacySettingsViewController.m; sourceTree = ""; }; C1414E9B24312F0100948788 /* MLChatMapsCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLChatMapsCell.h; sourceTree = ""; }; C1414E9C24312F0100948788 /* MLChatMapsCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLChatMapsCell.m; sourceTree = ""; }; + C153825E2B89BBE600EA83EC /* GroupDetailsEdit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDetailsEdit.swift; sourceTree = ""; }; + C15382612B89C38300EA83EC /* EditGroupName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupName.swift; sourceTree = ""; }; C15489B825680BBE00BBA2F0 /* MLQRCodeScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MLQRCodeScanner.swift; sourceTree = ""; }; C1567E3528255C64006E9637 /* Monal.macos.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Monal.macos.entitlements; sourceTree = ""; }; C1567E3628255C64006E9637 /* Monal.ios.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Monal.ios.entitlements; sourceTree = ""; }; @@ -654,6 +662,7 @@ C1850EB725F38A2D003D506A /* MonalUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonalUITests.swift; sourceTree = ""; }; C1850EB925F38A2D003D506A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C1850EC525F3C5EB003D506A /* TestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHelper.swift; sourceTree = ""; }; + C18967C62B81F61B0073C7C5 /* ChannelMemberList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMemberList.swift; sourceTree = ""; }; C18E7578245E8AE900AE8FB7 /* MLPipe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MLPipe.h; sourceTree = ""; }; C18E7579245E8AE900AE8FB7 /* MLPipe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MLPipe.m; sourceTree = ""; }; C1943A4A25309A9D0036172F /* MLReloadCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLReloadCell.h; sourceTree = ""; }; @@ -732,6 +741,7 @@ C1E856A728DECF5F00B104E9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = external/eu.lproj/Settings.strings; sourceTree = ""; }; C1E856A828DECF5F00B104E9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = external/eu.lproj/iosShare.strings; sourceTree = ""; }; C1E856A928DECF5F00B104E9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = external/eu.lproj/Localizable.strings; sourceTree = ""; }; + C1E8A7F62B8E47C300760220 /* EditGroupSubject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupSubject.swift; sourceTree = ""; }; C1F0AD15288BCE6F00BB0182 /* Alpha.Monal.ios.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Alpha.Monal.ios.entitlements; sourceTree = ""; }; C1F5C7A72775DA000001F295 /* MLContactSoftwareVersionInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MLContactSoftwareVersionInfo.h; sourceTree = ""; }; C1F5C7A82775DA000001F295 /* MLContactSoftwareVersionInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MLContactSoftwareVersionInfo.m; sourceTree = ""; }; @@ -1061,6 +1071,7 @@ 3D27D957290B0BC80014748B /* ContactRequestsMenu.swift */, 846DF27B2937FAA600AAB9C0 /* ChatPlaceholder.swift */, 841898AB2957DBAC00FEC77D /* RichAlert.swift */, + C114D13C2B15B903000FB99F /* ContactEntry.swift */, ); name = "View Controllers"; path = Classes; @@ -1095,6 +1106,10 @@ 3D65B78C27234B74005A30F4 /* ContactDetails.swift */, C19C919A26E26AF000F8CC57 /* ContactDetailsHeader.swift */, 3D5A91412842B4AE008CE57E /* MemberList.swift */, + C18967C62B81F61B0073C7C5 /* ChannelMemberList.swift */, + C153825E2B89BBE600EA83EC /* GroupDetailsEdit.swift */, + C15382612B89C38300EA83EC /* EditGroupName.swift */, + C1E8A7F62B8E47C300760220 /* EditGroupSubject.swift */, ); name = "Contact Details"; sourceTree = ""; @@ -2021,6 +2036,8 @@ 848717F3295ED64600B8D288 /* MLCall.m in Sources */, 26D89F061A890672009B147C /* MLSwitchCell.m in Sources */, C13EBB8E24DC685C008AADDA /* MLPrivacySettingsViewController.m in Sources */, + C18967C72B81F61B0073C7C5 /* ChannelMemberList.swift in Sources */, + C114D13D2B15B903000FB99F /* ContactEntry.swift in Sources */, 841B6F1A297B18720074F9B7 /* AccountPicker.swift in Sources */, 3D65B791272350F0005A30F4 /* SwiftuiHelpers.swift in Sources */, C1A80DA424D9552400B99E01 /* MLChatViewHelper.m in Sources */, @@ -2032,6 +2049,8 @@ C1943A4C25309A9D0036172F /* MLReloadCell.m in Sources */, 262E51971AD8CB7200788351 /* MLTextInputCell.m in Sources */, 84E55E7D2964424E003E191A /* ActiveChatsViewController.m in Sources */, + C15382622B89C38300EA83EC /* EditGroupName.swift in Sources */, + C1E8A7F72B8E47C300760220 /* EditGroupSubject.swift in Sources */, 263DFAC32187D0E00038E716 /* MLLinkCell.m in Sources */, 3D65B78D27234B74005A30F4 /* ContactDetails.swift in Sources */, E89DD32525C6626400925F62 /* MLFileTransferDataCell.m in Sources */, @@ -2062,6 +2081,7 @@ 26A78ED823C2B59400C7CF40 /* MLPlaceholderViewController.m in Sources */, C12436142434AB5D00B8F074 /* MLAttributedLabel.m in Sources */, C13A0BCE26E78B7B00987E29 /* ContactDetailsHeader.swift in Sources */, + C153825F2B89BBE600EA83EC /* GroupDetailsEdit.swift in Sources */, 3D85E587282AE523006F5B3A /* OmemoQrCodeView.swift in Sources */, 849A53E4287135B2007E941A /* MLVoIPProcessor.m in Sources */, C117F7E12B086390001F2BC6 /* CreateGroupMenu.swift in Sources */, @@ -2425,6 +2445,7 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = Monal.macos.entitlements; COMPILER_INDEX_STORE_ENABLE = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; ENABLE_BITCODE = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_PREVIEWS = YES; @@ -2453,12 +2474,16 @@ ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal"; PRODUCT_MODULE_NAME = Monal; PRODUCT_NAME = Monal; PROVISIONING_PROFILE = ""; "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2481,6 +2506,7 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = NotificationService/NotificationService.macos.entitlements; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -2492,6 +2518,8 @@ "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal.notificationService"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2510,6 +2538,7 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = NotificationService/NotificationService.macos.entitlements; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -2521,6 +2550,8 @@ "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal.notificationService"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2539,6 +2570,7 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = NotificationService/NotificationService.macos.entitlements; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -2550,6 +2582,8 @@ "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal.notificationService"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2596,9 +2630,12 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = S8D843U34Y; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_DYNAMIC_NO_PIC = NO; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -2622,8 +2659,12 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2.0; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTS_MACCATALYST = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2656,9 +2697,11 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = Monal.macos.entitlements; COMPILER_INDEX_STORE_ENABLE = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; ENABLE_BITCODE = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_PREVIEWS = YES; + ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)", @@ -2683,12 +2726,16 @@ ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal"; PRODUCT_MODULE_NAME = Monal; PRODUCT_NAME = Monal; PROVISIONING_PROFILE = ""; "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2707,6 +2754,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = shareSheet.entitlements; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/shareSheet-iOS/Info.plist"; @@ -2718,6 +2766,8 @@ PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM.shareSheet; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal.shareSheet"; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2732,6 +2782,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = shareSheet.entitlements; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/shareSheet-iOS/Info.plist"; @@ -2743,6 +2794,8 @@ PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM.shareSheet; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal.shareSheet"; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2757,6 +2810,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = shareSheet.entitlements; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/shareSheet-iOS/Info.plist"; @@ -2768,6 +2822,8 @@ PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM.shareSheet; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal.shareSheet"; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2783,6 +2839,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2802,6 +2859,9 @@ PRODUCT_BUNDLE_IDENTIFIER = im.Monal.monalxmpp; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2821,6 +2881,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2839,6 +2900,9 @@ PRODUCT_BUNDLE_IDENTIFIER = im.Monal.monalxmpp; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2858,6 +2922,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2876,6 +2941,9 @@ PRODUCT_BUNDLE_IDENTIFIER = im.Monal.monalxmpp; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2926,9 +2994,12 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = S8D843U34Y; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_DYNAMIC_NO_PIC = NO; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -2955,8 +3026,12 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2.0; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTS_MACCATALYST = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2989,9 +3064,11 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = Monal.macos.entitlements; COMPILER_INDEX_STORE_ENABLE = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; ENABLE_BITCODE = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_PREVIEWS = YES; + ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)", @@ -3017,12 +3094,16 @@ ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal"; PRODUCT_MODULE_NAME = Monal; PRODUCT_NAME = Monal; PROVISIONING_PROFILE = ""; "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -3077,6 +3158,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = S8D843U34Y; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -3106,10 +3188,14 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2.0; ONLY_ACTIVE_ARCH = YES; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -3136,6 +3222,7 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = dwarf; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; @@ -3153,6 +3240,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM.MonalXMPPUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_OBJC_BRIDGING_HEADER = "MonalXMPPUnitTests/MonalXMPPUnitTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -3196,6 +3285,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3220,6 +3310,8 @@ PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM.MonalXMPPUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "MonalXMPPUnitTests/MonalXMPPUnitTests-Bridging-Header.h"; @@ -3265,6 +3357,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3289,6 +3382,8 @@ PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM.MonalXMPPUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "MonalXMPPUnitTests/MonalXMPPUnitTests-Bridging-Header.h"; @@ -3337,9 +3432,12 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = S8D843U34Y; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_DYNAMIC_NO_PIC = NO; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -3366,9 +3464,13 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2.0; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_VERSION = 5.0; @@ -3402,9 +3504,11 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = Monal.macos.entitlements; COMPILER_INDEX_STORE_ENABLE = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; ENABLE_BITCODE = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_PREVIEWS = YES; + ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)", @@ -3429,12 +3533,16 @@ ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal"; PRODUCT_MODULE_NAME = Monal; PRODUCT_NAME = Monal; PROVISIONING_PROFILE = ""; "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -3453,6 +3561,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = shareSheet.entitlements; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/shareSheet-iOS/Info.plist"; @@ -3464,6 +3573,8 @@ PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM.shareSheet; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal.shareSheet"; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -3482,6 +3593,7 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = NotificationService/NotificationService.macos.entitlements; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3493,6 +3605,8 @@ "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.monal.notificationService"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -3508,6 +3622,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -3526,6 +3641,9 @@ PRODUCT_BUNDLE_IDENTIFIER = im.Monal.monalxmpp; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -3543,12 +3661,15 @@ baseConfigurationReference = 7D6715099247A9CCC180EE30 /* Pods-MonalUITests.beta.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + DEAD_CODE_STRIPPING = NO; INFOPLIST_FILE = MonalUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; TEST_TARGET_NAME = Monal; VALIDATE_PRODUCT = YES; }; @@ -3589,6 +3710,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3613,6 +3735,8 @@ PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM.MonalXMPPUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "MonalXMPPUnitTests/MonalXMPPUnitTests-Bridging-Header.h"; @@ -3661,9 +3785,12 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = S8D843U34Y; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_DYNAMIC_NO_PIC = NO; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -3690,8 +3817,12 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2.0; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = IS_QUICKSY; SWIFT_VERSION = 5.0; @@ -3727,10 +3858,12 @@ CODE_SIGN_STYLE = Automatic; COMPILER_INDEX_STORE_ENABLE = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = S8D843U34Y; ENABLE_BITCODE = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_PREVIEWS = YES; + ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SRCROOT)", @@ -3755,6 +3888,7 @@ ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = "org.monal-im.prod.ios.quicksy"; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.quicksy"; PRODUCT_MODULE_NAME = Monal; @@ -3762,6 +3896,9 @@ PROVISIONING_PROFILE = ""; "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; PROVISIONING_PROFILE_SPECIFIER = ""; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -3780,6 +3917,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = Quicksy.sharesheet.entitlements; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/shareSheet-iOS/Info.plist"; @@ -3791,6 +3929,8 @@ PRODUCT_BUNDLE_IDENTIFIER = "org.monal-im.prod.ios.quicksy.shareSheet"; "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.quicksy.shareSheet"; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -3809,6 +3949,7 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = NotificationService/Quicksy.NotificationService.macos.entitlements; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3820,6 +3961,8 @@ "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "org.monal-im.prod.catalyst.quicksy.notificationService"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -3835,6 +3978,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -3853,6 +3997,9 @@ PRODUCT_BUNDLE_IDENTIFIER = im.Monal.monalxmpp; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -3870,12 +4017,15 @@ baseConfigurationReference = 26ABE9FB494A9E7F3044C695 /* Pods-MonalUITests.appstore-quicksy.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + DEAD_CODE_STRIPPING = NO; INFOPLIST_FILE = MonalUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; TEST_TARGET_NAME = Monal; VALIDATE_PRODUCT = YES; }; @@ -3916,6 +4066,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3940,6 +4091,8 @@ PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM.MonalXMPPUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "MonalXMPPUnitTests/MonalXMPPUnitTests-Bridging-Header.h"; @@ -3955,12 +4108,15 @@ baseConfigurationReference = F7506FDE7A78EB0CAB14FF60 /* Pods-MonalUITests.debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + DEAD_CODE_STRIPPING = NO; INFOPLIST_FILE = MonalUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; TEST_TARGET_NAME = Monal; }; name = Debug; @@ -3970,12 +4126,15 @@ baseConfigurationReference = 39B989B9775C0725A810D271 /* Pods-MonalUITests.adhoc.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + DEAD_CODE_STRIPPING = NO; INFOPLIST_FILE = MonalUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; TEST_TARGET_NAME = Monal; VALIDATE_PRODUCT = YES; }; @@ -3986,12 +4145,15 @@ baseConfigurationReference = D8D2595B2BE453296E59F1AF /* Pods-MonalUITests.appstore.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + DEAD_CODE_STRIPPING = NO; INFOPLIST_FILE = MonalUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; TEST_TARGET_NAME = Monal; VALIDATE_PRODUCT = YES; }; @@ -4035,9 +4197,12 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = S8D843U34Y; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; GCC_DYNAMIC_NO_PIC = NO; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -4066,9 +4231,13 @@ IPHONEOS_DEPLOYMENT_TARGET = 14.0; LLVM_LTO = NO; MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2.0; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = NO; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "IS_ALPHA DEBUG"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -4103,6 +4272,7 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = Alpha.Monal.macos.entitlements; COMPILER_INDEX_STORE_ENABLE = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; ENABLE_BITCODE = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_PREVIEWS = YES; @@ -4132,11 +4302,15 @@ ); LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)"; + PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES; PRODUCT_BUNDLE_IDENTIFIER = monal.alpha; PRODUCT_MODULE_NAME = Monal; PRODUCT_NAME = Monal.alpha; PROVISIONING_PROFILE = ""; "PROVISIONING_PROFILE[sdk=iphoneos*]" = ""; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -4156,6 +4330,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = Alpha.shareSheet.entitlements; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/shareSheet-iOS/Info.plist"; @@ -4166,6 +4341,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = monal.alpha.shareSheet; PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -4184,6 +4361,7 @@ "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = NotificationService/Alpha.NotificationService.macos.entitlements; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = NotificationService/Alpha.Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4194,6 +4372,8 @@ PRODUCT_BUNDLE_IDENTIFIER = monal.alpha.NotificaionService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -4209,6 +4389,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; CURRENT_PROJECT_VERSION = 0; + DEAD_CODE_STRIPPING = NO; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; @@ -4227,6 +4408,9 @@ PRODUCT_BUNDLE_IDENTIFIER = im.Monal.monalxmpp; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_STYLE = "non-global"; + STRIP_SWIFT_SYMBOLS = NO; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -4244,12 +4428,15 @@ baseConfigurationReference = 5BACDACCFE405FE0C903C897 /* Pods-MonalUITests.alpha.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + DEAD_CODE_STRIPPING = NO; INFOPLIST_FILE = MonalUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; TEST_TARGET_NAME = Monal; VALIDATE_PRODUCT = YES; }; @@ -4290,6 +4477,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -4314,6 +4502,8 @@ PRODUCT_BUNDLE_IDENTIFIER = G7YU7X7KRJ.SworIM.MonalXMPPUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + STRIP_SWIFT_SYMBOLS = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "MonalXMPPUnitTests/MonalXMPPUnitTests-Bridging-Header.h"; diff --git a/Monal/Monal.xcodeproj/xcshareddata/xcschemes/Monal Alpha.xcscheme b/Monal/Monal.xcodeproj/xcshareddata/xcschemes/Monal Alpha.xcscheme index cc98bb9d87..2ba3b11376 100644 --- a/Monal/Monal.xcodeproj/xcshareddata/xcschemes/Monal Alpha.xcscheme +++ b/Monal/Monal.xcodeproj/xcshareddata/xcschemes/Monal Alpha.xcscheme @@ -34,7 +34,6 @@ buildConfiguration = "Alpha" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - enableAddressSanitizer = "NO" enableASanStackUseAfterReturn = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" diff --git a/Monal/NotificationService/NotificationService.m b/Monal/NotificationService/NotificationService.m index 6bf00d4a4f..40d6de49f7 100644 --- a/Monal/NotificationService/NotificationService.m +++ b/Monal/NotificationService/NotificationService.m @@ -362,9 +362,9 @@ -(void) freezeAllParseQueues { //disconnect to prevent endless loops trying to connect dispatch_async(queue, ^{ - DDLogVerbose(@"freezeAllParseQueues: %@", account); + DDLogVerbose(@"freezing: %@", account); [account freezeParseQueue]; - DDLogVerbose(@"freezeAllParseQueues: %@", account); + DDLogVerbose(@"done freezing: %@", account); }); } dispatch_barrier_sync(queue, ^{ diff --git a/Monal/Podfile.lock b/Monal/Podfile.lock index c23d9a1fd3..dd65f5952d 100644 --- a/Monal/Podfile.lock +++ b/Monal/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - ASN1Decoder (1.9.0) + - ASN1Decoder (1.10.0) - DZNEmptyDataSet (1.8.1) - FLAnimatedImage (1.0.17) - KSCrash/Core (1.16.2): @@ -42,9 +42,9 @@ PODS: - MarqueeLabel (~> 4.3.0) - SnapKit (~> 5.6.0) - SAMKeychain (1.5.3) - - SDWebImage (5.18.10): - - SDWebImage/Core (= 5.18.10) - - SDWebImage/Core (5.18.10) + - SDWebImage (5.19.0): + - SDWebImage/Core (= 5.19.0) + - SDWebImage/Core (5.19.0) - SignalProtocolC (2.3.3) - SignalProtocolObjC (1.1.1): - SignalProtocolC (~> 2.3.3) @@ -53,7 +53,7 @@ PODS: - sqlite3/perf-threadsafe (3.45.1): - sqlite3/common - TOCropViewController (2.6.1) - - WebRTC-lib (120.0.0) + - WebRTC-lib (122.0.0) DEPENDENCIES: - ASN1Decoder @@ -107,7 +107,7 @@ CHECKOUT OPTIONS: :git: https://github.com/monal-im/SignalProtocol-ObjC.git SPEC CHECKSUMS: - ASN1Decoder: 4f4bbcaf1d1b8be56daa3280e82863a607f5bda9 + ASN1Decoder: 91cb1d781b5a178ea7375b2f1519e2bdaaa4c427 DZNEmptyDataSet: 9525833b9e68ac21c30253e1d3d7076cc828eaa7 FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b KSCrash: 469b9f982b97309acb719baaecbf9182f62ddf85 @@ -115,14 +115,14 @@ SPEC CHECKSUMS: MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406 NotificationBannerSwift: dce54ded532b26e30cd8e7f4d80e124a0f2ba7d1 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c - SDWebImage: fc8f2d48bbfd72ef39d70e981bd24a3f3be53fec + SDWebImage: 981fd7e860af070920f249fd092420006014c3eb SignalProtocolC: 8092866e45b663a6bc3e45a8d13bad2571dbf236 SignalProtocolObjC: 1beb46b1d35733e7ab96a919f88bac20ec771c73 SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 sqlite3: 73b7fc691fdc43277614250e04d183740cb15078 TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 - WebRTC-lib: bd0d4b76aab1492415216715bd02f4329a33cd10 + WebRTC-lib: 5e9d21edbcf9ce5cc66b7ec7f94f0c15a2779113 PODFILE CHECKSUM: f415f317e34fd11a687374f84ee8dfb14ebb29a6 -COCOAPODS: 1.15.0 +COCOAPODS: 1.15.2 diff --git a/Monal/localization/Base.lproj/Main.storyboard b/Monal/localization/Base.lproj/Main.storyboard index 91c3b4ece8..9e80f78eaa 100644 --- a/Monal/localization/Base.lproj/Main.storyboard +++ b/Monal/localization/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -28,7 +28,7 @@ - + @@ -81,11 +81,11 @@ - + - + @@ -112,7 +112,7 @@ - + @@ -152,17 +152,21 @@ - - - - - - - + + + + + + + + + + + @@ -209,15 +213,15 @@ - + - + - + @@ -225,14 +229,14 @@ - + - + - + - + @@ -327,7 +331,7 @@ - + - + - + @@ -489,10 +493,10 @@ - + - + @@ -503,7 +507,7 @@ - + - + @@ -615,7 +619,7 @@ - + - + @@ -724,7 +728,7 @@ - + - + + + + + - - - + - - - + - - + + @@ -1990,17 +1994,16 @@ - - + - + - + - + - + @@ -2551,25 +2554,25 @@ - + - + - + - + - + diff --git a/Monal/shareSheet-iOS/ShareViewController.m b/Monal/shareSheet-iOS/ShareViewController.m index e30d38febe..241b60ecf0 100644 --- a/Monal/shareSheet-iOS/ShareViewController.m +++ b/Monal/shareSheet-iOS/ShareViewController.m @@ -68,9 +68,7 @@ - (void) presentationAnimationDidFinish // list all contacts, not only active chats // that will clutter the list of selectable contacts, but you can always use sirikit interactions // to get the recently used contacts listed - NSMutableArray* recipients = [[DataLayer sharedInstance] contactList]; - - self.recipients = recipients; + self.recipients = [[DataLayer sharedInstance] contactList]; self.accounts = [[DataLayer sharedInstance] enabledAccountList]; if(self.intentContact != nil) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 81e8c01cbf..38f2bcc7d3 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.4.2" @@ -69,9 +63,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" @@ -127,22 +121,13 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "rustix" version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ - "bitflags 2.4.2", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -161,22 +146,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -237,9 +222,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -248,13 +233,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys", ] @@ -288,9 +272,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] @@ -329,9 +313,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -344,42 +328,42 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"