Merge pull request #799 from RyanRory/conversation-swipe-actions

Conversation swipe actions
This commit is contained in:
Morgan Pretty 2023-04-05 16:50:11 +10:00 committed by GitHub
commit 2568d50835
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1012 additions and 395 deletions

View File

@ -242,6 +242,6 @@ SPEC CHECKSUMS:
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 2bf7639359fecebe56e9757d88f4eb48864652d2
PODFILE CHECKSUM: 97324ae5888b01db2f2adc4dcc239e2e7d6867f7
COCOAPODS: 1.11.3

View File

@ -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 */; };
@ -1178,11 +1180,12 @@
7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = "<group>"; };
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = "<group>"; };
7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = "<group>"; };
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = "<group>"; };
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = "<group>"; };
7B521E0729BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = "<group>"; };
7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLeavingJob.swift; sourceTree = "<group>"; };
7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = "<group>"; };
7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = "<group>"; };
@ -1192,6 +1195,7 @@
7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = "<group>"; };
7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = "<group>"; };
7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = "<group>"; };
7B89FF4529C016E300C4C708 /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = "<group>"; };
7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = "<group>"; };
7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = "<group>"; };
7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
@ -2818,6 +2822,7 @@
B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */,
C33100272559000A00070591 /* UIView+Utilities.swift */,
FD71161F28D97ABC00B47552 /* UIImage+Tinting.swift */,
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -3549,7 +3554,7 @@
7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */,
FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */,
FD432431299C6933008A0213 /* _011_AddPendingReadReceipts.swift */,
7B521E0729BFEAFF00C3C36A /* _012_AddFTSIfNeeded.swift */,
7B89FF4529C016E300C4C708 /* _012_AddFTSIfNeeded.swift */,
);
path = Migrations;
sourceTree = "<group>";
@ -4136,6 +4141,7 @@
FDF0B74E28079E5E004C14C5 /* SendReadReceiptsJob.swift */,
C352A348255781F400338F3E /* AttachmentDownloadJob.swift */,
C352A35A2557824E00338F3E /* AttachmentUploadJob.swift */,
7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */,
);
path = Types;
sourceTree = "<group>";
@ -5142,6 +5148,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 */,
@ -5400,11 +5407,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 */,
@ -5435,7 +5444,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 */,

View File

@ -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(

View File

@ -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) ]

View File

@ -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

View File

@ -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?) {

View File

@ -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

View File

@ -394,18 +394,32 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
accessibilityIdentifier: "Leave group",
accessibilityLabel: "Leave group",
confirmationInfo: ConfirmationModal.Info(
title: "CONFIRM_LEAVE_GROUP_TITLE".localized(),
explanation: (currentUserIsClosedGroupAdmin ?
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone." :
"CONFIRM_LEAVE_GROUP_DESCRIPTION".localized()
),
title: "leave_group_confirmation_alert_title".localized(),
attributedExplanation: {
if currentUserIsClosedGroupAdmin {
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
}(),
confirmTitle: "LEAVE_BUTTON_TITLE".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text
),
onTap: { [weak self] in
dependencies.storage.writeAsync { db in
try MessageSender.leave(db, groupPublicKey: threadId)
try MessageSender.leave(db, groupPublicKey: threadId, deleteThread: false)
}
}
)

View File

@ -618,6 +618,10 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
return true
}
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> 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) {

View File

@ -301,23 +301,29 @@ public class HomeViewModel {
// MARK: - Functions
public func delete(threadId: String, threadVariant: SessionThread.Variant) {
Storage.shared.writeAsync { db in
switch threadVariant {
case .closedGroup:
try MessageSender
.leave(db, groupPublicKey: threadId)
.retainUntilComplete()
case .openGroup:
OpenGroupManager.shared.delete(db, openGroupId: threadId)
default: break
}
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, force) {
case (.closedGroup, false):
try MessageSender.leave(
db,
groupPublicKey: threadId,
deleteThread: true
)
case (.openGroup, _):
OpenGroupManager.shared.delete(db, openGroupId: threadId)
default:
try delete(db, threadId: threadId)
}
}
}
}

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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

View File

@ -46,5 +46,6 @@ public enum SNMessagingKit { // Just to make the external API nice
JobRunner.add(executor: SendReadReceiptsJob.self, for: .sendReadReceipts)
JobRunner.add(executor: AttachmentDownloadJob.self, for: .attachmentDownload)
JobRunner.add(executor: AttachmentUploadJob.self, for: .attachmentUpload)
JobRunner.add(executor: GroupLeavingJob.self, for: .groupLeaving)
}
}

View File

@ -151,7 +151,7 @@ internal extension ControlMessageProcessRecord {
.infoClosedGroupCreated:
return nil
case .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft:
case .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving:
self.variant = .closedGroupControlMessage
case .infoDisappearingMessagesUpdate:

View File

@ -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())

View File

@ -25,7 +25,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)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

View File

@ -31,7 +31,7 @@ public enum AttachmentUploadJob: JobExecutor {
return (attachment, try OpenGroup.fetchOne(db, id: threadId))
})
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

View File

@ -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<Void> 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<CodingKeys> = 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<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
try container.encode(groupPublicKey, forKey: .groupPublicKey)
try container.encode(deleteThread, forKey: .deleteThread)
}
}
}

View File

@ -21,7 +21,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)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

View File

@ -23,7 +23,7 @@ public enum MessageSendJob: JobExecutor {
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}
@ -36,7 +36,7 @@ public enum MessageSendJob: JobExecutor {
let jobId: Int64 = job.id,
let interactionId: Int64 = job.interactionId
else {
failure(job, JobRunnerError.missingRequiredDetails, false)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

View File

@ -21,7 +21,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)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

View File

@ -23,7 +23,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)
failure(job, JobRunnerError.missingRequiredDetails, true)
return
}

View File

@ -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<Void> {
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
)
}
/*

View File

@ -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()
}
}
}

View File

@ -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<ViewModel> = """
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 (

View File

@ -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<T>(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
}
}
}
}

View File

@ -102,6 +102,10 @@ public struct Job: Codable, Equatable, Identifiable, FetchableRecord, MutablePer
/// 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 {

View File

@ -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 }

View File

@ -66,7 +66,8 @@ public final class JobRunner {
jobVariants.remove(.attachmentUpload),
jobVariants.remove(.messageSend),
jobVariants.remove(.notifyPushServer),
jobVariants.remove(.sendReadReceipts)
jobVariants.remove(.sendReadReceipts),
jobVariants.remove(.groupLeaving)
].compactMap { $0 }
)
let messageReceiveQueue: JobQueue = JobQueue(