diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index e593f16fd..5a083dfc1 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -109,11 +109,12 @@ 7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; }; 7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; }; 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; }; + 7B2E985829AC227C001792D7 /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */; }; 7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; }; 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; }; 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; }; - 7B521E0829BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B521E0729BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift */; }; + 7B521E0A29BFF84400C3C36A /* GroupLeavingJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */; }; 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; }; @@ -123,6 +124,7 @@ 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; }; 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; }; 7B81682C28B72F480069F315 /* PendingChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682B28B72F480069F315 /* PendingChange.swift */; }; + 7B89FF4629C016E300C4C708 /* _012_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B89FF4529C016E300C4C708 /* _012_AddFTSIfNeeded.swift */; }; 7B8C44C528B49DDA00FBE25F /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */; }; 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; }; 7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; }; @@ -1183,11 +1185,12 @@ 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = ""; }; 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = ""; }; 7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; + 7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = ""; }; 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = ""; }; 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = ""; }; 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; 7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = ""; }; - 7B521E0729BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = ""; }; + 7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLeavingJob.swift; sourceTree = ""; }; 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = ""; }; @@ -1197,6 +1200,7 @@ 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; 7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = ""; }; 7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = ""; }; + 7B89FF4529C016E300C4C708 /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = ""; }; 7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = ""; }; 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = ""; }; 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = ""; }; @@ -2826,6 +2830,7 @@ B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */, C33100272559000A00070591 /* UIView+Utilities.swift */, FD71161F28D97ABC00B47552 /* UIImage+Tinting.swift */, + 7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */, ); path = Utilities; sourceTree = ""; @@ -3557,7 +3562,7 @@ 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */, FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */, FD432431299C6933008A0213 /* _011_AddPendingReadReceipts.swift */, - 7B521E0729BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift */, + 7B89FF4529C016E300C4C708 /* _012_AddFTSIfNeeded.swift */, ); path = Migrations; sourceTree = ""; @@ -4171,6 +4176,7 @@ FDF0B74E28079E5E004C14C5 /* SendReadReceiptsJob.swift */, C352A348255781F400338F3E /* AttachmentDownloadJob.swift */, C352A35A2557824E00338F3E /* AttachmentUploadJob.swift */, + 7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */, ); path = Types; sourceTree = ""; @@ -5177,6 +5183,7 @@ FD37E9D528A1FCE8003AE748 /* Theme+OceanLight.swift in Sources */, FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */, FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */, + 7B2E985829AC227C001792D7 /* UIContextualAction+Theming.swift in Sources */, FD37E9CF28A1EB1B003AE748 /* Theme.swift in Sources */, C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */, FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */, @@ -5435,11 +5442,13 @@ buildActionMask = 2147483647; files = ( 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */, + 7B89FF4629C016E300C4C708 /* _012_AddFTSIfNeeded.swift in Sources */, FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */, B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */, C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */, FD245C672850665E00B966DD /* AttachmentDownloadJob.swift in Sources */, C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */, + 7B521E0A29BFF84400C3C36A /* GroupLeavingJob.swift in Sources */, FD09799927FFC1A300936362 /* Attachment.swift in Sources */, FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */, C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */, @@ -5470,7 +5479,6 @@ FD09797527FAB64300936362 /* ProfileManager.swift in Sources */, FD245C57285065F100B966DD /* Poller.swift in Sources */, FDA8EAFE280E8B78002B68E5 /* FailedMessageSendsJob.swift in Sources */, - 7B521E0829BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift in Sources */, FD245C6A2850666F00B966DD /* FileServerAPI.swift in Sources */, FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */, FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */, diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift index 1c67c9e25..2a04c703f 100644 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ b/Session/Closed Groups/EditClosedGroupVC.swift @@ -451,7 +451,12 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat Storage.shared .writeAsync { db in if !updatedMemberIds.contains(userPublicKey) { - return try MessageSender.leave(db, groupPublicKey: threadId) + try MessageSender.leave( + db, + groupPublicKey: threadId, + deleteThread: false + ) + return Promise.value(()) } return try MessageSender.update( diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 04e652f8f..a66dcfef3 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -136,7 +136,8 @@ extension ContextMenuVC { switch cellViewModel.variant { case .standardIncomingDeleted, .infoCall, .infoScreenshotNotification, .infoMediaSavedNotification, - .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, + .infoClosedGroupCreated, .infoClosedGroupUpdated, + .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving, .infoMessageRequestAccepted, .infoDisappearingMessagesUpdate: // Let the user delete info messages and unsent messages return [ Action.delete(cellViewModel, delegate) ] diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 8f533f286..a266e5b83 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1706,7 +1706,8 @@ extension ConversationVC: switch cellViewModel.variant { case .standardIncomingDeleted, .infoCall, .infoScreenshotNotification, .infoMediaSavedNotification, - .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, + .infoClosedGroupCreated, .infoClosedGroupUpdated, + .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving, .infoMessageRequestAccepted, .infoDisappearingMessagesUpdate: // Info messages and unsent messages should just trigger a local // deletion (they are created as side effects so we wouldn't be diff --git a/Session/Conversations/Message Cells/InfoMessageCell.swift b/Session/Conversations/Message Cells/InfoMessageCell.swift index 7c4a90248..c1f7e63a1 100644 --- a/Session/Conversations/Message Cells/InfoMessageCell.swift +++ b/Session/Conversations/Message Cells/InfoMessageCell.swift @@ -97,6 +97,7 @@ final class InfoMessageCell: MessageCell { iconImageViewHeightConstraint.constant = (icon != nil) ? InfoMessageCell.iconSize : 0 self.label.text = cellViewModel.body + self.label.themeTextColor = (cellViewModel.variant == .infoClosedGroupCurrentUserErrorLeaving) ? .danger : .textPrimary } override func dynamicUpdate(with cellViewModel: MessageViewModel, playbackInfo: ConversationViewModel.PlaybackInfo?) { diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 1f7356bc2..44886ba9c 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -70,7 +70,8 @@ public class MessageCell: UITableViewCell { case .standardOutgoing, .standardIncoming, .standardIncomingDeleted: return VisibleMessageCell.self - case .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, + case .infoClosedGroupCreated, .infoClosedGroupUpdated, + .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving, .infoDisappearingMessagesUpdate, .infoScreenshotNotification, .infoMediaSavedNotification, .infoMessageRequestAccepted: return InfoMessageCell.self diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 21732ffda..447732eeb 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -394,18 +394,32 @@ class ThreadSettingsViewModel: SessionTableViewModel UISwipeActionsConfiguration? { + return nil + } + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section] let unswipeAnimationDelay: DispatchTimeInterval = .milliseconds(500) @@ -634,44 +638,18 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi case .threads: let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] - let delete: UIContextualAction = UIContextualAction( - style: .destructive, - title: "TXT_DELETE_TITLE".localized() - ) { [weak self] _, _, completionHandler in - let confirmationModal: ConfirmationModal = ConfirmationModal( - info: ConfirmationModal.Info( - title: "CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE".localized(), - explanation: (threadViewModel.currentUserIsClosedGroupAdmin == true ? - "admin_group_leave_warning".localized() : - "CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE".localized() - ), - confirmTitle: "TXT_DELETE_TITLE".localized(), - confirmStyle: .danger, - cancelStyle: .alert_text, - dismissOnConfirm: true, - onConfirm: { [weak self] _ in - self?.viewModel.delete( - threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant - ) - self?.dismiss(animated: true, completion: nil) - - completionHandler(true) - }, - afterClosed: { completionHandler(false) } - ) - ) - - self?.present(confirmationModal, animated: true, completion: nil) - } - delete.themeBackgroundColor = .conversationButton_swipeDestructive + guard threadViewModel.interactionVariant != .infoClosedGroupCurrentUserLeaving else { return nil } let pin: UIContextualAction = UIContextualAction( - style: .normal, - title: (threadViewModel.threadIsPinned ? - "UNPIN_BUTTON_TEXT".localized() : - "PIN_BUTTON_TEXT".localized() - ) + title: (threadViewModel.threadIsPinned ? "UNPIN_BUTTON_TEXT".localized() : "PIN_BUTTON_TEXT".localized()), + icon: UIImage(systemName: "pin"), + iconHeight: Values.mediumFontSize, + themeTintColor: .white, + themeBackgroundColor: .conversationButton_swipeDestructive, + side: .trailing, + actionIndex: 0, + indexPath: indexPath, + tableView: tableView ) { _, _, completionHandler in (tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate( isPinned: !threadViewModel.threadIsPinned @@ -688,51 +666,201 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi } } pin.themeBackgroundColor = .conversationButton_swipeTertiary - - guard threadViewModel.threadVariant == .contact && !threadViewModel.threadIsNoteToSelf else { - return UISwipeActionsConfiguration(actions: [ delete, pin ]) - } - - let block: UIContextualAction = UIContextualAction( - style: .normal, - title: (threadViewModel.threadIsBlocked == true ? - "BLOCK_LIST_UNBLOCK_BUTTON".localized() : - "BLOCK_LIST_BLOCK_BUTTON".localized() - ) + + let mute: UIContextualAction = UIContextualAction( + title: ((threadViewModel.threadMutedUntilTimestamp != nil) ? "unmute_button_text".localized() : "mute_button_text".localized()), + icon: UIImage(systemName: "speaker.slash"), + iconHeight: Values.mediumFontSize, + themeTintColor: .white, + themeBackgroundColor: .conversationButton_swipeDestructive, + side: .trailing, + actionIndex: 1, + indexPath: indexPath, + tableView: tableView ) { _, _, completionHandler in (tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate( - isBlocked: (threadViewModel.threadIsBlocked == false) + isMuted: !(threadViewModel.threadMutedUntilTimestamp != nil) ) completionHandler(true) // Delay the change to give the cell "unswipe" animation some time to complete DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) { Storage.shared.writeAsync { db in - try Contact + let currentValue: TimeInterval? = try SessionThread + .filter(id: threadViewModel.threadId) + .select(.mutedUntilTimestamp) + .asRequest(of: TimeInterval.self) + .fetchOne(db) + + try SessionThread .filter(id: threadViewModel.threadId) .updateAll( db, - Contact.Columns.isBlocked.set( - to: (threadViewModel.threadIsBlocked == false ? - true: - false + SessionThread.Columns.mutedUntilTimestamp.set( + to: (currentValue == nil ? + Date.distantFuture.timeIntervalSince1970 : + nil ) ) ) - - try MessageSender.syncConfiguration(db, forceSyncNow: true) - .retainUntilComplete() } } } - block.themeBackgroundColor = .conversationButton_swipeSecondary - - return UISwipeActionsConfiguration(actions: [ delete, block, pin ]) - + mute.themeBackgroundColor = .conversationButton_swipeSecondary + + switch (threadViewModel.threadVariant, threadViewModel.currentUserIsClosedGroupMember) { + case (.contact, _): + let delete: UIContextualAction = UIContextualAction( + title: "TXT_DELETE_TITLE".localized(), + icon: UIImage(named: "icon_bin")?.resizedImage(to: CGSize(width: Values.mediumFontSize, height: Values.mediumFontSize)), + iconHeight: Values.mediumFontSize, + themeTintColor: .white, + themeBackgroundColor: .conversationButton_swipeDestructive, + side: .trailing, + actionIndex: 2, + indexPath: indexPath, + tableView: tableView + ) { [weak self] _, _, completionHandler in + let confirmationModalExplanation: NSAttributedString = { + let mutableAttributedString = NSMutableAttributedString( + string: String( + format: "delete_conversation_confirmation_alert_message".localized(), + threadViewModel.displayName + ) + ) + mutableAttributedString.addAttribute( + .font, + value: UIFont.boldSystemFont(ofSize: Values.smallFontSize), + range: (mutableAttributedString.string as NSString).range(of: threadViewModel.displayName) + ) + return mutableAttributedString + }() + + let confirmationModal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "delete_conversation_confirmation_alert_title".localized(), + attributedExplanation: confirmationModalExplanation, + confirmTitle: "TXT_DELETE_TITLE".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + dismissOnConfirm: true, + onConfirm: { [weak self] _ in + self?.viewModel.delete( + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant + ) + self?.dismiss(animated: true, completion: nil) + + completionHandler(true) + }, + afterClosed: { completionHandler(false) } + ) + ) + + self?.present(confirmationModal, animated: true, completion: nil) + } + delete.themeBackgroundColor = .conversationButton_swipeDestructive + + return UISwipeActionsConfiguration(actions: [ delete, mute, pin ]) + + case (.closedGroup, false): + let delete: UIContextualAction = UIContextualAction( + title: "TXT_DELETE_TITLE".localized(), + icon: UIImage(named: "icon_bin")?.resizedImage(to: CGSize(width: Values.mediumFontSize, height: Values.mediumFontSize)), + iconHeight: Values.mediumFontSize, + themeTintColor: .white, + themeBackgroundColor: .conversationButton_swipeDestructive, + side: .trailing, + actionIndex: 2, + indexPath: indexPath, + tableView: tableView + ) { [weak self] _, _, completionHandler in + self?.viewModel.delete( + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant, + force: true + ) + + completionHandler(true) + } + + return UISwipeActionsConfiguration(actions: [ delete, mute, pin ]) + + default: + let leave: UIContextualAction = UIContextualAction( + title: "LEAVE_BUTTON_TITLE".localized(), + icon: UIImage(systemName: "rectangle.portrait.and.arrow.right"), + iconHeight: Values.mediumFontSize, + themeTintColor: .white, + themeBackgroundColor: .conversationButton_swipeDestructive, + side: .trailing, + actionIndex: 2, + indexPath: indexPath, + tableView: tableView + ) { [weak self] _, _, completionHandler in + let confirmationModalTitle: String = (threadViewModel.threadVariant == .closedGroup) ? + "leave_group_confirmation_alert_title".localized() : + "leave_community_confirmation_alert_title".localized() + + let confirmationModalExplanation: NSAttributedString = { + if threadViewModel.threadVariant == .closedGroup && threadViewModel.currentUserIsClosedGroupAdmin == true { + return NSAttributedString(string: "admin_group_leave_warning".localized()) + } + + let mutableAttributedString = NSMutableAttributedString( + string: String( + format: "leave_community_confirmation_alert_message".localized(), + threadViewModel.displayName + ) + ) + mutableAttributedString.addAttribute( + .font, + value: UIFont.boldSystemFont(ofSize: Values.smallFontSize), + range: (mutableAttributedString.string as NSString).range(of: threadViewModel.displayName) + ) + return mutableAttributedString + }() + + let confirmationModal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: confirmationModalTitle, + attributedExplanation: confirmationModalExplanation, + confirmTitle: "LEAVE_BUTTON_TITLE".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + dismissOnConfirm: true, + onConfirm: { [weak self] _ in + self?.viewModel.delete( + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant + ) + self?.dismiss(animated: true, completion: nil) + + completionHandler(true) + }, + afterClosed: { completionHandler(false) } + ) + ) + + self?.present(confirmationModal, animated: true, completion: nil) + } + leave.themeBackgroundColor = .conversationButton_swipeDestructive + + return UISwipeActionsConfiguration(actions: [ leave, mute, pin ]) + } + default: return nil } } + func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) { + UIContextualAction.willBeginEditing(indexPath: indexPath, tableView: tableView) + } + + func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { + UIContextualAction.didEndEditing(indexPath: indexPath, tableView: tableView) + } + // MARK: - Interaction func handleContinueButtonTapped(from seedReminderView: SeedReminderView) { diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 30f44b6d5..7effafa61 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -301,23 +301,29 @@ public class HomeViewModel { // MARK: - Functions - public func delete(threadId: String, threadVariant: SessionThread.Variant) { + public func delete(threadId: String, threadVariant: SessionThread.Variant, force: Bool = false) { + + func delete(_ db: Database, threadId: String) throws { + _ = try SessionThread + .filter(id: threadId) + .deleteAll(db) + } + Storage.shared.writeAsync { db in - switch threadVariant { - case .closedGroup: - try MessageSender - .leave(db, groupPublicKey: threadId) - .retainUntilComplete() + switch (threadVariant, force) { + case (.closedGroup, false): + try MessageSender.leave( + db, + groupPublicKey: threadId, + deleteThread: true + ) - case .openGroup: + case (.openGroup, _): OpenGroupManager.shared.delete(db, openGroupId: threadId) - default: break + default: + try delete(db, threadId: threadId) } - - _ = try SessionThread - .filter(id: threadId) - .deleteAll(db) } } } diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 21f699aca..f5007fbe3 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Fertig"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Auswählen"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Du wirst in dieser Gruppe keine Nachrichten mehr versenden oder empfangen können."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Wollen Sie die Gruppe wirklich verlassen?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Dies kann nicht rückgängig gemacht werden."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Unterhaltung löschen?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 9bef16abf..da74d3933 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Done"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Select"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "You will no longer be able to send or receive messages in this group."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Do you really want to leave?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "This cannot be undone."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Delete Conversation?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index bfb99bd79..46107d2bc 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Hecho"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Seleccionar"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "No podrás enviar o recibir más mensajes en este grupo."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "¿De verdad quieres abandonar el grupo?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Este paso no se puede deshacer."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "¿Eliminar conversación?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index 262854bfe..b5cc4f0f8 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "انجام شد"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "انتخاب"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "شما دیگر قادر به ارسال یا دریافت پیام از این گروه نخواهید بود"; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "آیا واقعا قصد ترک کردن دارید؟"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "این نمیتواند انجام نشود."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "گفتگو حذف شود؟"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "درحال جستجو..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index 8b1b9a3c5..666585405 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Valmis"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Valitse"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Et pysty enään lähettämään tai vastaanottamaan viestejä tässä ryhmässä."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Haluatko varmasti poistua ryhmästä?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Tätä ei voida perua."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Poistetaanko keskustelu?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 28b5ba47f..6625f503b 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Terminé"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Sélectionner"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Vous ne pourrez plus recevoir ni envoyer de messages dans ce groupe."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Voulez-vous vraiment quitter ce groupe ?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Cette action est irréversible."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Supprimer la conversation ?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index ff4d5d873..e1cdaac55 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "पूरा हुआ"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "चुनें"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "अब आप इस समूह में संदेश भेजने या प्राप्त करने में सक्षम नहीं होंगे।"; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "क्या आप वाकई छोड़ना चाहते हैं?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "This cannot be undone."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Delete Conversation?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index d5f5b0a94..77cafa902 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Gotovo"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Odaberi"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Više nećete moći slati niti primati poruke u ovoj grupi."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Da li zaista želite izaći?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Ovaj je postupak nepovratan."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Obriši razgovor?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index effc945b5..1f23902a3 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Selesai"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Pilih"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Anda tidak dapat lagi mengirim atau menerima pesan dari grup ini."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Apakah Anda benar-benar ingin keluar?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Tindakan ini tidak dapat dibatalkan."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Hapus Percakapan?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 766457324..c824b802c 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Fatto"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Seleziona"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Non sarei più in grado di inviare o ricevere messaggi in questo gruppo."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Vuoi davvero lasciare?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Non potrà essere annullato."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Elimina conversazione?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 798f4bac9..75cb1e6bf 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "完了"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "選択"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "このグループとの会話が出来なくなりますがよろしいでしょうか。"; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "離脱してよろしいですか?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "消去すると元に戻せません"; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "消去しますか?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 0a39aa25c..4c9b31f6e 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Ok"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Selecteer"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Je kunt geen berichten meer versturen of ontvangen in deze groep."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Wilt u echt deze groep verlaten?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Dit kan niet ongedaan worden gemaakt."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Gesprek verwijderen?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index be530b2f2..35d1f7a04 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Gotowe"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Zaznacz"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Nie będziesz już móc odbierać lub wysyłać wiadomości w tej grupie."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Czy na pewno chcesz wyjść?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Tego nie można cofnąć."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Usunąć konwersację?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index c61ecd667..c351ad411 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Pronto"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Selecionar"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Você não poderá mais enviar nem receber mensagens neste grupo."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Você tem certeza que deseja sair?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Isso não pode ser desfeito."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Excluir conversa?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index d683c3892..26a64e4a4 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Готово"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Выбор"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Вы больше не сможете отправлять и получать сообщения в этой группе."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Вы хотите покинуть группу?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Это не может быть отменено."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Удалить разговор?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index ef161ff64..349867b2c 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Done"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Select"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "You will no longer be able to send or receive messages in this group."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Do you really want to leave?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "This cannot be undone."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Delete Conversation?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 4b023ae1e..71cbe0cd5 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Hotovo"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Vybrať"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Už nebudete môcť posielať a prijímať správy v tejto skupine."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Ste si istý/á, že chcete odísť?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Táto akcia sa nedá vrátiť."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Zmazať konverzáciu?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 066e5ba5d..f0a1f421a 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Klart"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Välj"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Du kommer inte längre att kunna skicka eller ta emot meddelanden i denna grupp."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Vill du verkligen lämna?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Detta kan inte ångras."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Radera konversation?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index cafa26215..e32ffabad 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "เสร็จ"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "เลือก"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "คุณจะไม่สามารถส่งและรับข้อความในกลุ่มนี้ได้อีกต่อไป"; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "แน่ใจออกจากไหม"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "การกระทำนี้ไม่สามารถยกเลิกได้"; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "ลบการสนทนาไหม"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 0b2bc8a02..049e0782d 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "Xong"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "Chọn"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "Bạn sẽ không thể gửi hoặc nhận tin nhắn trong nhóm này nữa."; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "Bạn thực sự muốn rời khỏi nhóm?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "Tác vụ này không thể hoàn tất."; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Xóa cuộc hội thoại?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 9f4292ed6..2eb03bd1a 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "完成"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "選擇"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "您已經無法再於此群組傳送或接收訊息。"; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "確定要離開嗎?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "此操作無法復原。"; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "刪除對話?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 164c814da..9e1b96ac2 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -60,14 +60,6 @@ "BUTTON_DONE" = "完成"; /* Button text to enable batch selection mode */ "BUTTON_SELECT" = "选择"; -/* Alert body */ -"CONFIRM_LEAVE_GROUP_DESCRIPTION" = "您将无法在此群组中继续发送或接收消息。"; -/* Alert title */ -"CONFIRM_LEAVE_GROUP_TITLE" = "确定离开群聊?"; -/* Message for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE" = "该操作无法撤销。"; -/* Title for the 'conversation delete confirmation' alert. */ -"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "删除会话?"; /* keyboard toolbar label when starting to search with no current results */ "CONVERSATION_SEARCH_SEARCHING" = "Searching..."; /* keyboard toolbar label when no messages match the search string */ @@ -609,3 +601,15 @@ "context_menu_resync" = "Resync"; "GIPHY_PERMISSION_TITLE" = "Search GIFs?"; "GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs."; +"mute_button_text" = "Mute"; +"unmute_button_text" = "Unmute"; +"mark_read_button_text" = "Mark read"; +"mark_unread_button_text" = "Mark unread"; +"leave_group_confirmation_alert_title" = "Leave Group"; +"leave_community_confirmation_alert_title" = "Leave Community"; +"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?"; +"group_you_leaving" = "Leaving..."; +"group_leave_error" = "Failed to leave Group!"; +"group_unable_to_leave" = "Unable to leave the Group, please try again"; +"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?"; +"delete_conversation_confirmation_alert_title" = "Delete Conversation"; diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift index 11fa90c97..61f61e2a0 100644 --- a/Session/Shared/FullConversationCell.swift +++ b/Session/Shared/FullConversationCell.swift @@ -346,7 +346,7 @@ public final class FullConversationCell: UITableViewCell { } else { accentLineView.themeBackgroundColor = .conversationButton_unreadStripBackground - accentLineView.alpha = (unreadCount > 0 ? 1 : 0.0001) // Setting the alpha to exactly 0 causes an issue on iOS 12 + accentLineView.alpha = (unreadCount > 0 ? 1 : 0) } isPinnedIcon.isHidden = !cellViewModel.threadIsPinned @@ -384,12 +384,34 @@ public final class FullConversationCell: UITableViewCell { typingIndicatorView.stopAnimation() ThemeManager.onThemeChange(observer: snippetLabel) { [weak self, weak snippetLabel] theme, _ in - guard let textColor: UIColor = theme.color(for: .textPrimary) else { return } - - snippetLabel?.attributedText = self?.getSnippet( - cellViewModel: cellViewModel, - textColor: textColor - ) + if cellViewModel.interactionVariant == .infoClosedGroupCurrentUserLeaving { + guard let textColor: UIColor = theme.color(for: .textSecondary) else { return } + + self?.displayNameLabel.themeTextColor = .textSecondary + + snippetLabel?.attributedText = self?.getSnippet( + cellViewModel: cellViewModel, + textColor: textColor + ) + } else if cellViewModel.interactionVariant == .infoClosedGroupCurrentUserErrorLeaving { + guard let textColor: UIColor = theme.color(for: .danger) else { return } + + self?.displayNameLabel.themeTextColor = .textPrimary + + snippetLabel?.attributedText = self?.getSnippet( + cellViewModel: cellViewModel, + textColor: textColor + ) + } else { + guard let textColor: UIColor = theme.color(for: .textPrimary) else { return } + + self?.displayNameLabel.themeTextColor = .textPrimary + + snippetLabel?.attributedText = self?.getSnippet( + cellViewModel: cellViewModel, + textColor: textColor + ) + } } } @@ -406,23 +428,33 @@ public final class FullConversationCell: UITableViewCell { } public func optimisticUpdate( - isBlocked: Bool? = nil, - isPinned: Bool? = nil + isMuted: Bool? = nil, + isPinned: Bool? = nil, + hasUnread: Bool? = nil ) { - if let isBlocked: Bool = isBlocked { - if isBlocked { - accentLineView.themeBackgroundColor = .danger - accentLineView.alpha = 1 - } - else { - accentLineView.themeBackgroundColor = .conversationButton_unreadStripBackground - accentLineView.alpha = (!unreadCountView.isHidden ? 1 : 0.0001) // Setting the alpha to exactly 0 causes an issue on iOS 12 + if let isMuted: Bool = isMuted { + if isMuted { + + } else { + } } if let isPinned: Bool = isPinned { isPinnedIcon.isHidden = !isPinned } + + if let hasUnread: Bool = hasUnread { + if hasUnread { + unreadCountView.isHidden = false + unreadCountLabel.text = "1" + unreadCountLabel.font = .boldSystemFont(ofSize: Values.verySmallFontSize) + accentLineView.alpha = 1 + } else { + unreadCountView.isHidden = true + accentLineView.alpha = 0 + } + } } // MARK: - Snippet generation @@ -461,7 +493,10 @@ public final class FullConversationCell: UITableViewCell { )) } - if cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup { + if + (cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup) && + (cellViewModel.interactionVariant?.isGroupControlMessage == false) + { let authorName: String = cellViewModel.authorName(for: cellViewModel.threadVariant) result.append(NSAttributedString( @@ -470,17 +505,22 @@ public final class FullConversationCell: UITableViewCell { )) } + let previewText: String = { + if cellViewModel.interactionVariant == .infoClosedGroupCurrentUserErrorLeaving { return "group_leave_error".localized() } + return Interaction.previewText( + variant: (cellViewModel.interactionVariant ?? .standardIncoming), + body: cellViewModel.interactionBody, + threadContactDisplayName: cellViewModel.threadContactName(), + authorDisplayName: cellViewModel.authorName(for: cellViewModel.threadVariant), + attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo, + attachmentCount: cellViewModel.interactionAttachmentCount, + isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true) + ) + }() + result.append(NSAttributedString( string: MentionUtilities.highlightMentionsNoAttributes( - in: Interaction.previewText( - variant: (cellViewModel.interactionVariant ?? .standardIncoming), - body: cellViewModel.interactionBody, - threadContactDisplayName: cellViewModel.threadContactName(), - authorDisplayName: cellViewModel.authorName(for: cellViewModel.threadVariant), - attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo, - attachmentCount: cellViewModel.interactionAttachmentCount, - isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true) - ), + in: previewText, threadVariant: cellViewModel.threadVariant, currentUserPublicKey: cellViewModel.currentUserPublicKey, currentUserBlindedPublicKey: cellViewModel.currentUserBlindedPublicKey diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 2edd387b3..938dae99c 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -46,5 +46,6 @@ public enum SNMessagingKit { // Just to make the external API nice JobRunner.setExecutor(SendReadReceiptsJob.self, for: .sendReadReceipts) JobRunner.setExecutor(AttachmentDownloadJob.self, for: .attachmentDownload) JobRunner.setExecutor(AttachmentUploadJob.self, for: .attachmentUpload) + JobRunner.setExecutor(GroupLeavingJob.self, for: .groupLeaving) } } diff --git a/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift b/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift index c7cd38636..bc01ef67c 100644 --- a/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift +++ b/SessionMessagingKit/Database/Models/ControlMessageProcessRecord.swift @@ -151,7 +151,7 @@ internal extension ControlMessageProcessRecord { .infoClosedGroupCreated: return nil - case .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft: + case .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving: self.variant = .closedGroupControlMessage case .infoDisappearingMessagesUpdate: diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index fe244b8f0..0fd032333 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -73,6 +73,8 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu case infoClosedGroupCreated = 1000 case infoClosedGroupUpdated case infoClosedGroupCurrentUserLeft + case infoClosedGroupCurrentUserErrorLeaving + case infoClosedGroupCurrentUserLeaving case infoDisappearingMessagesUpdate = 2000 @@ -87,7 +89,8 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu public var isInfoMessage: Bool { switch self { - case .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, + case .infoClosedGroupCreated, .infoClosedGroupUpdated, + .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving, .infoDisappearingMessagesUpdate, .infoScreenshotNotification, .infoMediaSavedNotification, .infoMessageRequestAccepted, .infoCall: return true @@ -97,6 +100,25 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu } } + public var isGroupControlMessage: Bool { + switch self { + case .infoClosedGroupCreated, .infoClosedGroupUpdated, + .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving: + return true + default: + return false + } + } + + public var isGroupLeavingStatus: Bool { + switch self { + case .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving: + return true + default: + return false + } + } + /// This flag controls whether the `wasRead` flag is automatically set to true based on the message variant (as a result it they will /// or won't affect the unread count) fileprivate var canBeUnread: Bool { @@ -106,7 +128,8 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu case .standardOutgoing, .standardIncomingDeleted: return false - case .infoClosedGroupCreated, .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, + case .infoClosedGroupCreated, .infoClosedGroupUpdated, + .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving, .infoDisappearingMessagesUpdate, .infoScreenshotNotification, .infoMediaSavedNotification, .infoMessageRequestAccepted: return false @@ -846,6 +869,8 @@ public extension Interaction { case .infoClosedGroupCreated: return "GROUP_CREATED".localized() case .infoClosedGroupCurrentUserLeft: return "GROUP_YOU_LEFT".localized() + case .infoClosedGroupCurrentUserLeaving: return "group_you_leaving".localized() + case .infoClosedGroupCurrentUserErrorLeaving: return "group_unable_to_leave".localized() case .infoClosedGroupUpdated: return (body ?? "GROUP_UPDATED".localized()) case .infoMessageRequestAccepted: return (body ?? "MESSAGE_REQUESTS_ACCEPTED".localized()) diff --git a/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift b/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift index 4eeb8e27c..cbaa8e092 100644 --- a/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift +++ b/SessionMessagingKit/Jobs/Types/AttachmentDownloadJob.swift @@ -26,7 +26,7 @@ public enum AttachmentDownloadJob: JobExecutor { let attachment: Attachment = Storage.shared .read({ db in try Attachment.fetchOne(db, id: details.attachmentId) }) else { - failure(job, JobRunnerError.missingRequiredDetails, false, dependencies) + failure(job, JobRunnerError.missingRequiredDetails, true, dependencies) return } diff --git a/SessionMessagingKit/Jobs/Types/AttachmentUploadJob.swift b/SessionMessagingKit/Jobs/Types/AttachmentUploadJob.swift index eb47948fc..c0a2c9503 100644 --- a/SessionMessagingKit/Jobs/Types/AttachmentUploadJob.swift +++ b/SessionMessagingKit/Jobs/Types/AttachmentUploadJob.swift @@ -32,7 +32,7 @@ public enum AttachmentUploadJob: JobExecutor { return (attachment, try OpenGroup.fetchOne(db, id: threadId)) }) else { - failure(job, JobRunnerError.missingRequiredDetails, false, dependencies) + failure(job, JobRunnerError.missingRequiredDetails, true, dependencies) return } diff --git a/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift b/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift new file mode 100644 index 000000000..5ebe5a9cd --- /dev/null +++ b/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift @@ -0,0 +1,166 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import PromiseKit +import SignalCoreKit +import SessionUtilitiesKit +import SessionSnodeKit + +public enum GroupLeavingJob: JobExecutor { + public static var maxFailureCount: Int = 0 + public static var requiresThreadId: Bool = true + public static var requiresInteractionId: Bool = true + + public static func run( + _ job: SessionUtilitiesKit.Job, + queue: DispatchQueue, + success: @escaping (SessionUtilitiesKit.Job, Bool) -> (), + failure: @escaping (SessionUtilitiesKit.Job, Error?, Bool) -> (), + deferred: @escaping (SessionUtilitiesKit.Job) -> ()) + { + guard + let detailsData: Data = job.details, + let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData), + let interactionId: Int64 = job.interactionId + else { + failure(job, JobRunnerError.missingRequiredDetails, true) + return + } + + guard let thread: SessionThread = Storage.shared.read({ db in try? SessionThread.fetchOne(db, id: details.groupPublicKey)}) else { + SNLog("Can't leave nonexistent closed group.") + failure(job, MessageSenderError.noThread, true) + return + } + + guard let closedGroup: ClosedGroup = Storage.shared.read({ db in try? thread.closedGroup.fetchOne(db)}) else { + failure(job, MessageSenderError.invalidClosedGroupUpdate, true) + return + } + + Storage.shared.writeAsync { db -> Promise in + try MessageSender.sendNonDurably( + db, + message: ClosedGroupControlMessage( + kind: .memberLeft + ), + interactionId: interactionId, + in: thread + ) + } + .done(on: queue) { _ in + // Remove the group from the database and unsubscribe from PNs + ClosedGroupPoller.shared.stopPolling(for: details.groupPublicKey) + + Storage.shared.writeAsync { db in + let userPublicKey: String = getUserHexEncodedPublicKey(db) + + try closedGroup + .keyPairs + .deleteAll(db) + + let _ = PushNotificationAPI.performOperation( + .unsubscribe, + for: details.groupPublicKey, + publicKey: userPublicKey + ) + + try Interaction + .filter(id: interactionId) + .updateAll( + db, + [ + Interaction.Columns.variant.set(to: Interaction.Variant.infoClosedGroupCurrentUserLeft), + Interaction.Columns.body.set(to: "GROUP_YOU_LEFT".localized()) + ] + ) + + // Update the group (if the admin leaves the group is disbanded) + let wasAdminUser: Bool = try GroupMember + .filter(GroupMember.Columns.groupId == thread.id) + .filter(GroupMember.Columns.profileId == userPublicKey) + .filter(GroupMember.Columns.role == GroupMember.Role.admin) + .isNotEmpty(db) + + if wasAdminUser { + try GroupMember + .filter(GroupMember.Columns.groupId == thread.id) + .deleteAll(db) + } + else { + try GroupMember + .filter(GroupMember.Columns.groupId == thread.id) + .filter(GroupMember.Columns.profileId == userPublicKey) + .deleteAll(db) + } + + if details.deleteThread { + _ = try SessionThread + .filter(id: thread.id) + .deleteAll(db) + } + } + success(job, false) + } + .catch(on: queue) { error in + Storage.shared.writeAsync { db in + try Interaction + .filter(id: job.interactionId) + .updateAll( + db, + [ + Interaction.Columns.variant.set(to: Interaction.Variant.infoClosedGroupCurrentUserErrorLeaving), + Interaction.Columns.body.set(to: "group_unable_to_leave".localized()) + ] + ) + } + success(job, false) + } + .retainUntilComplete() + + } +} + +// MARK: - GroupLeavingJob.Details + +extension GroupLeavingJob { + public struct Details: Codable { + private enum CodingKeys: String, CodingKey { + case groupPublicKey + case deleteThread + } + + public let groupPublicKey: String + public let deleteThread: Bool + + // MARK: - Initialization + + public init( + groupPublicKey: String, + deleteThread: Bool + ) { + self.groupPublicKey = groupPublicKey + self.deleteThread = deleteThread + } + + // MARK: - Codable + + public init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + self = Details( + groupPublicKey: try container.decode(String.self, forKey: .groupPublicKey), + deleteThread: try container.decode(Bool.self, forKey: .deleteThread) + ) + } + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(groupPublicKey, forKey: .groupPublicKey) + try container.encode(deleteThread, forKey: .deleteThread) + } + } +} + diff --git a/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift b/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift index e12265c18..2ea8fb90a 100644 --- a/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift @@ -22,7 +22,7 @@ public enum MessageReceiveJob: JobExecutor { let detailsData: Data = job.details, let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData) else { - failure(job, JobRunnerError.missingRequiredDetails, false, dependencies) + failure(job, JobRunnerError.missingRequiredDetails, true, dependencies) return } diff --git a/SessionMessagingKit/Jobs/Types/NotifyPushServerJob.swift b/SessionMessagingKit/Jobs/Types/NotifyPushServerJob.swift index 3be8e20ab..cf30ea3c1 100644 --- a/SessionMessagingKit/Jobs/Types/NotifyPushServerJob.swift +++ b/SessionMessagingKit/Jobs/Types/NotifyPushServerJob.swift @@ -22,7 +22,7 @@ public enum NotifyPushServerJob: JobExecutor { let detailsData: Data = job.details, let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData) else { - failure(job, JobRunnerError.missingRequiredDetails, false, dependencies) + failure(job, JobRunnerError.missingRequiredDetails, true, dependencies) return } diff --git a/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift b/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift index 0082bd277..62f9ff825 100644 --- a/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift +++ b/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift @@ -24,7 +24,7 @@ public enum SendReadReceiptsJob: JobExecutor { let detailsData: Data = job.details, let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData) else { - failure(job, JobRunnerError.missingRequiredDetails, false, dependencies) + failure(job, JobRunnerError.missingRequiredDetails, true, dependencies) return } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index 0d219b5b2..20f1c8574 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -478,13 +478,9 @@ extension MessageSender { /// unregisters from push notifications. /// /// The returned promise is fulfilled when the `MEMBER_LEFT` message has been sent to the group. - public static func leave(_ db: Database, groupPublicKey: String) throws -> Promise { + public static func leave(_ db: Database, groupPublicKey: String, deleteThread: Bool) throws { guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: groupPublicKey) else { - SNLog("Can't leave nonexistent closed group.") - return Promise(error: MessageSenderError.noThread) - } - guard let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db) else { - return Promise(error: MessageSenderError.invalidClosedGroupUpdate) + return } let userPublicKey: String = getUserHexEncodedPublicKey(db) @@ -493,66 +489,23 @@ extension MessageSender { let interaction: Interaction = try Interaction( threadId: thread.id, authorId: userPublicKey, - variant: .infoClosedGroupCurrentUserLeft, - body: ClosedGroupControlMessage.Kind - .memberLeft - .infoMessage(db, sender: userPublicKey), + variant: .infoClosedGroupCurrentUserLeaving, + body: "group_you_leaving".localized(), timestampMs: SnodeAPI.currentOffsetTimestampMs() ).inserted(db) - guard let interactionId: Int64 = interaction.id else { - throw StorageError.objectNotSaved - } - - // Send the update to the group - let promise = try MessageSender - .sendNonDurably( - db, - message: ClosedGroupControlMessage( - kind: .memberLeft - ), - interactionId: interactionId, - in: thread + JobRunner.upsert( + db, + job: Job( + variant: .groupLeaving, + threadId: thread.id, + interactionId: interaction.id, + details: GroupLeavingJob.Details( + groupPublicKey: groupPublicKey, + deleteThread: deleteThread + ) ) - .done { - // Remove the group from the database and unsubscribe from PNs - ClosedGroupPoller.shared.stopPolling(for: groupPublicKey) - - Storage.shared.write { db in - try closedGroup - .keyPairs - .deleteAll(db) - - let _ = PushNotificationAPI.performOperation( - .unsubscribe, - for: groupPublicKey, - publicKey: userPublicKey - ) - } - } - .map { _ in } - - // Update the group (if the admin leaves the group is disbanded) - let wasAdminUser: Bool = try GroupMember - .filter(GroupMember.Columns.groupId == thread.id) - .filter(GroupMember.Columns.profileId == userPublicKey) - .filter(GroupMember.Columns.role == GroupMember.Role.admin) - .isNotEmpty(db) - - if wasAdminUser { - try GroupMember - .filter(GroupMember.Columns.groupId == thread.id) - .deleteAll(db) - } - else { - try GroupMember - .filter(GroupMember.Columns.groupId == thread.id) - .filter(GroupMember.Columns.profileId == userPublicKey) - .deleteAll(db) - } - - // Return - return promise + ) } /* diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 6c02a3ccb..8f34c9467 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -763,26 +763,3 @@ public final class MessageSender { } } } - -// MARK: - Objective-C Support - -// FIXME: Remove when possible - -@objc(SMKMessageSender) -public class SMKMessageSender: NSObject { - @objc(leaveClosedGroupWithPublicKey:) - public static func objc_leave(_ groupPublicKey: String) -> AnyPromise { - let promise = Storage.shared.writeAsync { db in - try MessageSender.leave(db, groupPublicKey: groupPublicKey) - } - - return AnyPromise.from(promise) - } - - @objc(forceSyncConfigurationNow) - public static func objc_forceSyncConfigurationNow() { - Storage.shared.write { db in - try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() - } - } -} diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 022b87893..6b20a0fe0 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -100,7 +100,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat public var canWrite: Bool { switch threadVariant { case .contact: return true - case .closedGroup: return currentUserIsClosedGroupMember == true + case .closedGroup: return (currentUserIsClosedGroupMember == true) && (interactionVariant?.isGroupLeavingStatus != true) case .openGroup: return openGroupPermissions?.contains(.write) ?? false } } @@ -459,6 +459,9 @@ public extension SessionThreadViewModel { let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) + let groupMemberProfileIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.profileId.name) + let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name) + let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to @@ -466,7 +469,7 @@ public extension SessionThreadViewModel { /// /// Explicitly set default values for the fields ignored for search results let numColumnsBeforeProfiles: Int = 12 - let numColumnsBetweenProfilesAndAttachmentInfo: Int = 11 // The attachment info columns will be combined + let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined let request: SQLRequest = """ SELECT @@ -490,7 +493,8 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileBackKey).*, \(ViewModel.closedGroupProfileBackFallbackKey).*, \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - (\(groupMember[.profileId]) IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupAdminKey), + (\(ViewModel.currentUserIsClosedGroupMemberKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + (\(ViewModel.currentUserIsClosedGroupAdminKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupAdminKey), \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), @@ -565,10 +569,15 @@ public extension SessionThreadViewModel { LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(GroupMember.self) ON ( - \(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND - \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND - \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) + LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupMemberKey) ON ( + \(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberRoleColumnLiteral) != \(GroupMember.Role.zombie)")) AND + \(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND + \(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)")) + ) + LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupAdminKey) ON ( + \(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)")) AND + \(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND + \(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)")) ) LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( diff --git a/SessionUIKit/Utilities/UIContextualAction+Theming.swift b/SessionUIKit/Utilities/UIContextualAction+Theming.swift new file mode 100644 index 000000000..4e4396c1e --- /dev/null +++ b/SessionUIKit/Utilities/UIContextualAction+Theming.swift @@ -0,0 +1,182 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUtilitiesKit + +public extension UIContextualAction { + private static var lookupMap: Atomic<[Int: [String: [Int: ThemeValue]]]> = Atomic([:]) + + enum Side: Int { + case leading + case trailing + + func key(for indexPath: IndexPath) -> String { + return "\(indexPath.section)-\(indexPath.row)-\(rawValue)" + } + + init?(for view: UIView) { + guard view.frame.minX == 0 else { + self = .trailing + return + } + + self = .leading + } + } + + convenience init( + title: String? = nil, + icon: UIImage? = nil, + iconHeight: CGFloat = Values.mediumFontSize, + themeTintColor: ThemeValue = .white, + themeBackgroundColor: ThemeValue, + side: Side, + actionIndex: Int, + indexPath: IndexPath, + tableView: UITableView, + handler: @escaping UIContextualAction.Handler + ) { + self.init(style: .normal, title: title, handler: handler) + self.image = UIContextualAction + .imageWith( + title: title, + icon: icon, + iconHeight: iconHeight, + themeTintColor: themeTintColor + )? + .withRenderingMode(.alwaysTemplate) + self.themeBackgroundColor = themeBackgroundColor + + UIContextualAction.lookupMap.mutate { + $0[tableView.hashValue] = ($0[tableView.hashValue] ?? [:]) + .setting( + side.key(for: indexPath), + (($0[tableView.hashValue] ?? [:])[side.key(for: indexPath)] ?? [:]) + .setting(actionIndex, themeTintColor) + ) + } + } + + private static func imageWith( + title: String?, + icon: UIImage?, + iconHeight: CGFloat, + themeTintColor: ThemeValue + ) -> UIImage? { + let stackView: UIStackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .center + stackView.spacing = 4 + + if let icon: UIImage = icon { + let scale: Double = iconHeight / icon.size.height + let aspectRatio: CGFloat = (icon.size.width / icon.size.height) + let imageView: UIImageView = UIImageView(image: icon) + imageView.frame = CGRect(x: 0, y: 0, width: iconHeight * aspectRatio, height: iconHeight) + imageView.contentMode = .scaleAspectFit + imageView.themeTintColor = themeTintColor + stackView.addArrangedSubview(imageView) + } + + if let title: String = title { + let label: UILabel = UILabel() + label.font = .systemFont(ofSize: Values.verySmallFontSize) + label.text = title + label.textAlignment = .center + label.themeTextColor = themeTintColor + label.minimumScaleFactor = 0.75 + label.numberOfLines = (title.components(separatedBy: " ").count > 1 ? 2 : 1) + label.frame = CGRect( + origin: .zero, + // Note: It looks like there is a semi-max width of 68px for images in the swipe actions + // if the image ends up larger then there an odd behaviour can occur where 8/10 times the + // image is scaled down to fit, but ocassionally (primarily if you hide the action and + // immediately swipe to show it again once the cell hits the edge of the screen) the image + // won't be scaled down but will be full size - appearing as if two different images are used + size: label.sizeThatFits(CGSize(width: 68, height: 999)) + ) + label.set(.width, to: label.frame.width) + + stackView.addArrangedSubview(label) + } + + stackView.frame = CGRect( + origin: .zero, + size: stackView.systemLayoutSizeFitting(CGSize(width: 999, height: 999)) + ) + + // Based on https://stackoverflow.com/a/41288197/1118398 + let renderFormat: UIGraphicsImageRendererFormat = UIGraphicsImageRendererFormat() + renderFormat.scale = UIScreen.main.scale + + let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer( + size: stackView.bounds.size, + format: renderFormat + ) + return renderer.image { rendererContext in + stackView.layer.render(in: rendererContext.cgContext) + } + } + + private static func firstSubviewOfType(in superview: UIView) -> T? { + guard !(superview is T) else { return superview as? T } + guard !superview.subviews.isEmpty else { return nil } + + for subview in superview.subviews { + if let result: T = firstSubviewOfType(in: subview) { + return result + } + } + + return nil + } + + static func willBeginEditing(indexPath: IndexPath, tableView: UITableView) { + guard + let targetCell: UITableViewCell = tableView.cellForRow(at: indexPath), + targetCell.superview != tableView, + let targetSuperview: UIView = targetCell.superview? + .subviews + .filter({ $0 != targetCell }) + .first, + let side: Side = Side(for: targetSuperview), + let themeMap: [Int: ThemeValue] = UIContextualAction.lookupMap.wrappedValue + .getting(tableView.hashValue)? + .getting(side.key(for: indexPath)), + targetSuperview.subviews.count == themeMap.count + else { return } + + let targetViews: [UIImageView] = targetSuperview.subviews + .compactMap { subview in firstSubviewOfType(in: subview) } + + guard targetViews.count == themeMap.count else { return } + + // Set the imageView and background colours (so they change correctly when the theme changes) + targetViews.enumerated().forEach { index, targetView in + guard let themeTintColor: ThemeValue = themeMap[index] else { return } + + targetView.themeTintColor = themeTintColor + } + } + + static func didEndEditing(indexPath: IndexPath?, tableView: UITableView) { + guard let indexPath: IndexPath = indexPath else { return } + + let leadingKey: String = Side.leading.key(for: indexPath) + let trailingKey: String = Side.trailing.key(for: indexPath) + + guard + UIContextualAction.lookupMap.wrappedValue[tableView.hashValue]?[leadingKey] != nil || + UIContextualAction.lookupMap.wrappedValue[tableView.hashValue]?[trailingKey] != nil + else { return } + + UIContextualAction.lookupMap.mutate { + $0[tableView.hashValue]?[leadingKey] = nil + $0[tableView.hashValue]?[trailingKey] = nil + + if $0[tableView.hashValue]?.isEmpty == true { + $0[tableView.hashValue] = nil + } + } + } +} diff --git a/SessionUtilitiesKit/Database/Models/Job.swift b/SessionUtilitiesKit/Database/Models/Job.swift index b2f7dc8fd..0c98cbf81 100644 --- a/SessionUtilitiesKit/Database/Models/Job.swift +++ b/SessionUtilitiesKit/Database/Models/Job.swift @@ -102,6 +102,10 @@ public struct Job: Codable, Hashable, Equatable, Identifiable, FetchableRecord, /// This is a job that runs once whenever an attachment is downloaded to attempt to decode and properly /// download the attachment case attachmentDownload + + /// This is a job that runs once whenever the user leaves a group to send a group leaving message, remove group + /// record and group member record + case groupLeaving } public enum Behaviour: Int, Codable, DatabaseValueConvertible, CaseIterable { diff --git a/SessionUtilitiesKit/General/Dictionary+Utilities.swift b/SessionUtilitiesKit/General/Dictionary+Utilities.swift index d05bfe761..694844492 100644 --- a/SessionUtilitiesKit/General/Dictionary+Utilities.swift +++ b/SessionUtilitiesKit/General/Dictionary+Utilities.swift @@ -34,6 +34,12 @@ public extension Dictionary { return self[key] } + func getting(_ key: Key?) -> Value? { + guard let key: Key = key else { return nil } + + return self[key] + } + func setting(_ key: Key?, _ value: Value?) -> [Key: Value] { guard let key: Key = key else { return self } diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index 484a36aa7..4404ba7ca 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -164,7 +164,8 @@ public final class JobRunner: JobRunnerType { jobVariants.remove(.attachmentUpload), jobVariants.remove(.messageSend), jobVariants.remove(.notifyPushServer), - jobVariants.remove(.sendReadReceipts) + jobVariants.remove(.sendReadReceipts), + jobVariants.remove(.groupLeaving) ].compactMap { $0 } ), diff --git a/SessionUtilitiesKit/Networking/HTTP.swift b/SessionUtilitiesKit/Networking/HTTP.swift index 9e5946735..cbe2ec9de 100644 --- a/SessionUtilitiesKit/Networking/HTTP.swift +++ b/SessionUtilitiesKit/Networking/HTTP.swift @@ -41,8 +41,25 @@ public enum HTTP { guard SecTrustSetAnchorCertificates(trust, certificates as CFArray) == errSecSuccess else { return completionHandler(.cancelAuthenticationChallenge, nil) } + + // We want to make sure that the pinned certification was valid during it's validity + // period (which has now expired) so set the date to validate against to be within the + // valid period + let dateFormatter: DateFormatter = DateFormatter() + dateFormatter.dateFormat = "dd/MM/yyyy HH:mm:ss" + + if let validDate: Date = dateFormatter.date(from: "01/01/2022 12:00:00") { + if SecTrustSetVerifyDate(trust, validDate as CFDate) != errSecSuccess { + SNLog("Unable to set date for seed node certificate validation.") + } + } + else { + SNLog("Unable to set date for seed node certificate validation.") + } + // Check that the presented certificate is one of the seed node certificates var result: SecTrustResultType = .invalid + guard SecTrustEvaluate(trust, &result) == errSecSuccess else { return completionHandler(.cancelAuthenticationChallenge, nil) }