Merge pull request #799 from RyanRory/conversation-swipe-actions
Conversation swipe actions
This commit is contained in:
commit
2568d50835
|
@ -242,6 +242,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: 2bf7639359fecebe56e9757d88f4eb48864652d2
|
||||
PODFILE CHECKSUM: 97324ae5888b01db2f2adc4dcc239e2e7d6867f7
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) ]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,7 +151,7 @@ internal extension ControlMessageProcessRecord {
|
|||
.infoClosedGroupCreated:
|
||||
return nil
|
||||
|
||||
case .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft:
|
||||
case .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving:
|
||||
self.variant = .closedGroupControlMessage
|
||||
|
||||
case .infoDisappearingMessagesUpdate:
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue