Finished off the updated settings sections and fixed a couple of bugs

Added the Blocked contacts screen
Added a setting to control whether concurrent audio messages should auto-play
Finished updating the settings screens
Fixed an issue where items that should be removed from the PagedDatabaseObserver due to filter logic weren't getting removed in some cases
This commit is contained in:
Morgan Pretty 2022-08-26 14:09:41 +10:00
parent a1e88329db
commit c82ee0c44b
58 changed files with 1783 additions and 566 deletions

View File

@ -125,7 +125,6 @@
7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; };
7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; };
7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB188270430D20079FF93 /* CallMessageView.swift */; };
7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */; };
7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; };
7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; };
7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; };
@ -753,6 +752,10 @@
FD859EFC27C2F60700510D0C /* MockEd25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFB27C2F60700510D0C /* MockEd25519.swift */; };
FD86585828507B24008B6CF9 /* NSData+messagePadding.m in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D4825589FF20043A11F /* NSData+messagePadding.m */; };
FD87DD0428B8727D00AF0F98 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DD0328B8727D00AF0F98 /* Configuration.swift */; };
FD87DCFA28B74DB300AF0F98 /* ConversationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */; };
FD87DCFC28B755B800AF0F98 /* BlockedContactsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFB28B755B800AF0F98 /* BlockedContactsViewController.swift */; };
FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */; };
FD87DD0028B820F200AF0F98 /* BlockedContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */; };
FD90040F2818AB6D00ABAAF6 /* GetSnodePoolJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */; };
FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73F280402C4004C14C5 /* Job.swift */; };
FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; };
@ -822,7 +825,7 @@
FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; };
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; };
FDE72118286C156E0093DF33 /* ConversationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE72117286C156E0093DF33 /* ConversationSettingsViewController.swift */; };
FDE72154287FE4470093DF33 /* (null) in Sources */ = {isa = PBXBuildFile; };
FDE72154287FE4470093DF33 /* HighlightMentionBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE72153287FE4470093DF33 /* HighlightMentionBackgroundView.swift */; };
FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */; };
FDED2E3C282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDED2E3B282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift */; };
FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */; };
@ -1188,7 +1191,6 @@
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>"; };
7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = "<group>"; };
7B7CB18A270591630079FF93 /* ShareLogsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogsModal.swift; sourceTree = "<group>"; };
7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = "<group>"; };
7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = "<group>"; };
7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = "<group>"; };
@ -1836,6 +1838,10 @@
FD859EF927C2F5C500510D0C /* MockGenericHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGenericHash.swift; sourceTree = "<group>"; };
FD859EFB27C2F60700510D0C /* MockEd25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockEd25519.swift; sourceTree = "<group>"; };
FD87DD0328B8727D00AF0F98 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsViewModel.swift; sourceTree = "<group>"; };
FD87DCFB28B755B800AF0F98 /* BlockedContactsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewController.swift; sourceTree = "<group>"; };
FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewModel.swift; sourceTree = "<group>"; };
FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactCell.swift; sourceTree = "<group>"; };
FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSnodePoolJob.swift; sourceTree = "<group>"; };
FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
FDA8EAFD280E8B78002B68E5 /* FailedMessageSendsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedMessageSendsJob.swift; sourceTree = "<group>"; };
@ -1898,7 +1904,6 @@
FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = "<group>"; };
FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = "<group>"; };
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = "<group>"; };
FDE72117286C156E0093DF33 /* ConversationSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsViewController.swift; sourceTree = "<group>"; };
FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = "<group>"; };
FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = "<group>"; };
FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerError.swift; sourceTree = "<group>"; };
@ -2911,11 +2916,12 @@
FD37EA0428AA00C1003AE748 /* NotificationSettingsViewModel.swift */,
FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */,
FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */,
FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */,
FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */,
FD87DCFB28B755B800AF0F98 /* BlockedContactsViewController.swift */,
FD37E9CB28A1E578003AE748 /* AppearanceViewController.swift */,
FDE72117286C156E0093DF33 /* ConversationSettingsViewController.swift */,
FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */,
B86BD08523399CEF000F5AE3 /* SeedModal.swift */,
7B7CB18A270591630079FF93 /* ShareLogsModal.swift */,
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */,
);
path = Settings;
@ -3723,6 +3729,7 @@
FD37E9DA28A244E9003AE748 /* ThemePreviewView.swift */,
FD37E9DC28A384EB003AE748 /* PrimaryColorSelectionView.swift */,
FD37EA0A28AB12E2003AE748 /* SettingsCell.swift */,
FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */,
);
path = Views;
sourceTree = "<group>";
@ -5502,7 +5509,6 @@
34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */,
34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */,
FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */,
7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */,
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */,
@ -5519,6 +5525,7 @@
B8D0A26925E4A2C200C1835E /* Onboarding.swift in Sources */,
34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */,
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */,
FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */,
FD37E9DD28A384EB003AE748 /* PrimaryColorSelectionView.swift in Sources */,
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */,
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
@ -5563,6 +5570,7 @@
FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */,
B886B4A72398B23E00211ABE /* QRCodeVC.swift in Sources */,
4C586926224FAB83003FD070 /* AVAudioSession+OWS.m in Sources */,
FD87DD0028B820F200AF0F98 /* BlockedContactCell.swift in Sources */,
C331FFF42558FF0300070591 /* PNOptionView.swift in Sources */,
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */,
B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */,
@ -5580,10 +5588,12 @@
B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */,
7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */,
B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */,
FD87DCFC28B755B800AF0F98 /* BlockedContactsViewController.swift in Sources */,
B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */,
7B13E1E92810F01300BD4F64 /* SessionCallManager+Action.swift in Sources */,
C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */,
7B1581E827210ECC00848B49 /* RenderView.swift in Sources */,
FD87DCFA28B74DB300AF0F98 /* ConversationSettingsViewModel.swift in Sources */,
7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */,
7BAADFCE27B215FE007BCF92 /* UIView+Draggable.swift in Sources */,
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,

View File

@ -1146,58 +1146,7 @@ extension ConversationVC:
.filter(id: cellViewModel.id)
.asRequest(of: Int64.self)
.fetchOne(db)
else {
// If the message hasn't been sent yet then just delete locally
guard cellViewModel.state == .sending || cellViewModel.state == .failed else { return }
// Retrieve any message send jobs for this interaction
let jobs: [Job] = Storage.shared
.read { db in
try? Job
.filter(Job.Columns.variant == Job.Variant.messageSend)
.filter(Job.Columns.interactionId == cellViewModel.id)
.fetchAll(db)
}
.defaulting(to: [])
// If the job is currently running then wait until it's done before triggering
// the deletion
let targetJob: Job? = jobs.first(where: { JobRunner.isCurrentlyRunning($0) })
guard targetJob == nil else {
JobRunner.afterCurrentlyRunningJob(targetJob) { [weak self] result in
switch result {
// If it succeeded then we'll need to delete from the server so re-run
// this function (if we still don't have the server id for some reason
// then this would result in a local-only deletion which should be fine
case .succeeded: self?.delete(cellViewModel)
// Otherwise we just need to cancel the pending job (in case it retries)
// and delete the interaction
default:
JobRunner.removePendingJob(targetJob)
Storage.shared.writeAsync { db in
_ = try Interaction
.filter(id: cellViewModel.id)
.deleteAll(db)
}
}
}
return
}
// If it's not currently running then remove any pending jobs (just to be safe) and
// delete the interaction locally
jobs.forEach { JobRunner.removePendingJob($0) }
Storage.shared.writeAsync { db in
_ = try Interaction
.filter(id: cellViewModel.id)
.deleteAll(db)
}
return
}
else { return }
if remove {
OpenGroupAPI
@ -1209,7 +1158,8 @@ extension ConversationVC:
on: openGroup.server
)
.retainUntilComplete()
} else {
}
else {
OpenGroupAPI
.reactionAdd(
db,
@ -1220,8 +1170,8 @@ extension ConversationVC:
)
.retainUntilComplete()
}
} else {
}
else {
// Send the actual message
try MessageSender.send(
db,
@ -1446,7 +1396,58 @@ extension ConversationVC:
on: openGroup.server
)
)
else { return }
else {
// If the message hasn't been sent yet then just delete locally
guard cellViewModel.state == .sending || cellViewModel.state == .failed else { return }
// Retrieve any message send jobs for this interaction
let jobs: [Job] = Storage.shared
.read { db in
try? Job
.filter(Job.Columns.variant == Job.Variant.messageSend)
.filter(Job.Columns.interactionId == cellViewModel.id)
.fetchAll(db)
}
.defaulting(to: [])
// If the job is currently running then wait until it's done before triggering
// the deletion
let targetJob: Job? = jobs.first(where: { JobRunner.isCurrentlyRunning($0) })
guard targetJob == nil else {
JobRunner.afterCurrentlyRunningJob(targetJob) { [weak self] result in
switch result {
// If it succeeded then we'll need to delete from the server so re-run
// this function (if we still don't have the server id for some reason
// then this would result in a local-only deletion which should be fine
case .succeeded: self?.delete(cellViewModel)
// Otherwise we just need to cancel the pending job (in case it retries)
// and delete the interaction
default:
JobRunner.removePendingJob(targetJob)
Storage.shared.writeAsync { db in
_ = try Interaction
.filter(id: cellViewModel.id)
.deleteAll(db)
}
}
}
return
}
// If it's not currently running then remove any pending jobs (just to be safe) and
// delete the interaction locally
jobs.forEach { JobRunner.removePendingJob($0) }
Storage.shared.writeAsync { db in
_ = try Interaction
.filter(id: cellViewModel.id)
.deleteAll(db)
}
return
}
// Delete the message from the open group
deleteRemotely(
@ -1921,6 +1922,28 @@ extension ConversationVC:
default: return
}
}
// MARK: - Data Extraction Notifications
@objc func sendScreenshotNotification() {
// Only send screenshot notifications to one-to-one conversations
guard self.viewModel.threadData.threadVariant == .contact else { return }
let threadId: String = self.viewModel.threadData.threadId
Storage.shared.writeAsync { db in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { return }
try MessageSender.send(
db,
message: DataExtractionNotification(
kind: .screenshot
),
interactionId: nil,
in: thread
)
}
}
// MARK: - Convenience

View File

@ -365,6 +365,12 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
name: UIResponder.keyboardWillHideNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(sendScreenshotNotification),
name: UIApplication.userDidTakeScreenshotNotification,
object: nil
)
}
override func viewWillAppear(_ animated: Bool) {

View File

@ -198,6 +198,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
}()
)
],
joinSQL: MessageViewModel.optimisedJoinSQL,
filterSQL: MessageViewModel.filterSQL(threadId: threadId),
groupSQL: MessageViewModel.groupSQL,
orderSQL: MessageViewModel.orderSQL,
@ -714,7 +715,8 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
let currentIndex: Int = messageSection.elements
.firstIndex(where: { $0.id == interactionId }),
currentIndex < (messageSection.elements.count - 1),
messageSection.elements[currentIndex + 1].cellType == .audio
messageSection.elements[currentIndex + 1].cellType == .audio,
Storage.shared[.shouldAutoPlayConsecutiveAudioMessages] == true
else { return }
let nextItem: MessageViewModel = messageSection.elements[currentIndex + 1]

View File

@ -1,6 +1,7 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
@objc class TypingIndicatorView: UIStackView {
// This represents the spacing between the dots
@ -122,12 +123,18 @@
autoSetDimension(.height, toSize: kMaxRadiusPt)
layer.addSublayer(shapeLayer)
ThemeManager.onThemeChange(observer: self) { [weak self] _, _ in
guard self?.shapeLayer.animationKeys()?.isEmpty == false else { return }
self?.startAnimation()
}
}
fileprivate func startAnimation() {
stopAnimation()
let baseColor = Colors.text
let baseColor: UIColor = (ThemeManager.currentTheme.colors[.messageBubble_incomingText] ?? .white)
let timeIncrement: CFTimeInterval = 0.15
var colorValues = [CGColor]()
var pathValues = [CGPath]()

View File

@ -13,14 +13,14 @@ final class TypingIndicatorCell: MessageCell {
private lazy var bubbleView: UIView = {
let result: UIView = UIView()
result.layer.cornerRadius = VisibleMessageCell.smallCornerRadius
result.backgroundColor = Colors.receivedMessageBackground
result.themeBackgroundColor = .messageBubble_incomingBackground
return result
}()
private let bubbleViewMaskLayer: CAShapeLayer = CAShapeLayer()
private lazy var typingIndicatorView: TypingIndicatorView = TypingIndicatorView()
public lazy var typingIndicatorView: TypingIndicatorView = TypingIndicatorView()
// MARK: - Lifecycle

View File

@ -3,7 +3,6 @@
//
#import "OWSConversationSettingsViewController.h"
#import "OWSSoundSettingsViewController.h"
#import "Session-Swift.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
@ -417,9 +416,9 @@ CGFloat kIconViewLength = 24;
}
customRowHeight:UITableViewAutomaticDimension
actionBlock:^{
OWSSoundSettingsViewController *vc = [OWSSoundSettingsViewController new];
vc.threadId = weakSelf.threadId;
[weakSelf.navigationController pushViewController:vc animated:YES];
// FIXME: Remove `threadId` once we ditch the per-thread notification sound
UIViewController *viewController = [OWSNotificationSoundSettings createWith:weakSelf.threadId];
[weakSelf.navigationController pushViewController:viewController animated:YES];
}]];
if (self.isClosedGroup || self.isOpenGroup) {

View File

@ -527,24 +527,26 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
return true
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
let unswipeAnimationDelay: DispatchTimeInterval = .milliseconds(500)
switch section.model {
case .messageRequests:
let hide = UITableViewRowAction(style: .destructive, title: "TXT_HIDE_TITLE".localized()) { _, _ in
let hide: UIContextualAction = UIContextualAction(style: .destructive, title: "TXT_HIDE_TITLE".localized()) { _, _, completionHandler in
Storage.shared.write { db in db[.hasHiddenMessageRequests] = true }
completionHandler(true)
}
hide.themeBackgroundColor = .conversationButton_swipeDestructive
return [hide]
return UISwipeActionsConfiguration(actions: [hide])
case .threads:
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
let delete: UITableViewRowAction = UITableViewRowAction(
let delete: UIContextualAction = UIContextualAction(
style: .destructive,
title: "TXT_DELETE_TITLE".localized()
) { [weak self] _, _ in
) { [weak self] _, _, completionHandler in
let message = (threadViewModel.currentUserIsClosedGroupAdmin == true ?
"admin_group_leave_warning".localized() :
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE".localized()
@ -576,64 +578,83 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
.filter(id: threadViewModel.threadId)
.deleteAll(db)
}
completionHandler(true)
})
alert.addAction(UIAlertAction(
title: "TXT_CANCEL_TITLE".localized(),
style: .default
))
) { _ in
completionHandler(false)
})
self?.present(alert, animated: true, completion: nil)
}
delete.themeBackgroundColor = .conversationButton_swipeDestructive
let pin: UITableViewRowAction = UITableViewRowAction(
let pin: UIContextualAction = UIContextualAction(
style: .normal,
title: (threadViewModel.threadIsPinned ?
"UNPIN_BUTTON_TEXT".localized() :
"PIN_BUTTON_TEXT".localized()
)
) { _, _ in
Storage.shared.writeAsync { db in
try SessionThread
.filter(id: threadViewModel.threadId)
.updateAll(db, SessionThread.Columns.isPinned.set(to: !threadViewModel.threadIsPinned))
) { _, _, completionHandler in
(tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate(
isPinned: !threadViewModel.threadIsPinned
)
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 SessionThread
.filter(id: threadViewModel.threadId)
.updateAll(db, SessionThread.Columns.isPinned.set(to: !threadViewModel.threadIsPinned))
}
}
}
pin.themeBackgroundColor = .conversationButton_swipeTertiary
guard threadViewModel.threadVariant == .contact && !threadViewModel.threadIsNoteToSelf else {
return [ delete, pin ]
return UISwipeActionsConfiguration(actions: [ delete, pin ])
}
let block: UITableViewRowAction = UITableViewRowAction(
let block: UIContextualAction = UIContextualAction(
style: .normal,
title: (threadViewModel.threadIsBlocked == true ?
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
"BLOCK_LIST_BLOCK_BUTTON".localized()
)
) { _, _ in
Storage.shared.writeAsync { db in
try Contact
.filter(id: threadViewModel.threadId)
.updateAll(
db,
Contact.Columns.isBlocked.set(
to: (threadViewModel.threadIsBlocked == false ?
true:
false
) { _, _, completionHandler in
(tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate(
isBlocked: (threadViewModel.threadIsBlocked == false)
)
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
.filter(id: threadViewModel.threadId)
.updateAll(
db,
Contact.Columns.isBlocked.set(
to: (threadViewModel.threadIsBlocked == false ?
true:
false
)
)
)
)
try MessageSender.syncConfiguration(db, forceSyncNow: true)
.retainUntilComplete()
try MessageSender.syncConfiguration(db, forceSyncNow: true)
.retainUntilComplete()
}
}
}
block.themeBackgroundColor = .conversationButton_swipeSecondary
return [ delete, block, pin ]
return UISwipeActionsConfiguration(actions: [ delete, block, pin ])
default: return []
default: return nil
}
}

View File

@ -136,6 +136,16 @@ public class HomeViewModel {
return SQL("LEFT JOIN \(typingIndicator[.threadId]) = \(thread[.id])")
}()
),
PagedData.ObservedChanges(
table: Setting.self,
columns: [.value],
joinToPagedType: {
let setting: TypedTableAlias<Setting> = TypedTableAlias()
let targetSetting: String = Setting.BoolKey.showScreenshotNotifications.rawValue
return SQL("LEFT JOIN \(Setting.self) ON \(setting[.key]) = \(targetSetting)")
}()
)
],
/// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query but differs

View File

@ -70,7 +70,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
}()
private lazy var clearAllButton: OutlineButton = {
let result: OutlineButton = OutlineButton(style: .destructive, size: .medium)
let result: OutlineButton = OutlineButton(style: .destructive, size: .large)
result.translatesAutoresizingMaskIntoConstraints = false
result.setTitle("MESSAGE_REQUESTS_CLEAR_ALL".localized(), for: .normal)
result.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside)
@ -340,24 +340,25 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let section: MessageRequestsViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
switch section.model {
case .threads:
let threadId: String = section.elements[indexPath.row].threadId
let delete = UITableViewRowAction(
let delete = UIContextualAction(
style: .destructive,
title: "TXT_DELETE_TITLE".localized()
) { [weak self] _, _ in
) { [weak self] _, _, completionHandler in
self?.delete(threadId)
completionHandler(true)
}
delete.themeBackgroundColor = .conversationButton_swipeDestructive
return [ delete ]
return UISwipeActionsConfiguration(actions: [ delete ])
default: return []
default: return nil
}
}

View File

@ -290,7 +290,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "modal_share_logs_title".localized(), style: .default) { _ in
ShareLogsModal.shareLogs(from: alert) { [weak self] in
HelpViewModel.shareLogs(viewControllerToDismiss: alert) { [weak self] in
self?.showFailedMigrationAlert(error: error)
}
})

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Benachrichtigungen";
"vc_privacy_settings_title" = "Datenschutz";
"preferences_notifications_strategy_category_title" = "Benachrichtigungsstrategie";
"modal_seed_title" = "Ihr Wiederherstellungssatz";
"modal_seed_explanation" = "Das ist Ihr Wiederherstellungssatz. Damit können Sie Ihre Session ID wiederherstellen oder auf ein neues Gerät migrieren.";
"vc_qr_code_title" = "QR-Code";
"vc_qr_code_view_my_qr_code_tab_title" = "Meinen QR-Code anzeigen";
"vc_qr_code_view_scan_qr_code_tab_title" = "QR-Code scannen";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Daten nicht gelöscht von Service Node 1. Service Node ID: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Daten nicht gelöscht von %@ Service Noten. Service Noten IDs: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Ihr Wiederherstellungssatz";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Notifications";
"vc_privacy_settings_title" = "Privacy";
"preferences_notifications_strategy_category_title" = "Notification Strategy";
"modal_seed_title" = "Your Recovery Phrase";
"modal_seed_explanation" = "This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device.";
"vc_qr_code_title" = "QR Code";
"vc_qr_code_view_my_qr_code_tab_title" = "View My QR Code";
"vc_qr_code_view_scan_qr_code_tab_title" = "Scan QR Code";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Your Recovery Phrase";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Notificaciones";
"vc_privacy_settings_title" = "Privacidad";
"preferences_notifications_strategy_category_title" = "Estrategia de notificación";
"modal_seed_title" = "Tu frase de recuperación";
"modal_seed_explanation" = "Esta es tu frase de recuperación. Con ella, puedes restaurar o migrar tu ID de Session a un nuevo dispositivo.";
"vc_qr_code_title" = "Código QR";
"vc_qr_code_view_my_qr_code_tab_title" = "Ver mi código QR";
"vc_qr_code_view_scan_qr_code_tab_title" = "Escanear código QR";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Datos no borrados por 1 nodo de servicio. ID del nodo de servicio: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Datos no borrados por %@ nodos de servicio. ID del nodo de servicio: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Tu frase de recuperación";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "اعلان‌ها";
"vc_privacy_settings_title" = "حریم خصوصی";
"preferences_notifications_strategy_category_title" = "استراتژی اعلان";
"modal_seed_title" = "عبارت بازیابی شما";
"modal_seed_explanation" = "این عبارت بازیابی شماست. با استفاده از آن می‌توانید شناسه‌ی Session خود را به دستگاه جدید بازیابی یا انتقال دهید.";
"vc_qr_code_title" = "کد QR";
"vc_qr_code_view_my_qr_code_tab_title" = "مشاهده کد QR من";
"vc_qr_code_view_scan_qr_code_tab_title" = "اسکن کد QR";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "داده ها توسط ۱ گره سرویس حذف نشده است. شناسه گره سرویس: %@.";
"dialog_clear_all_data_deletion_failed_2" = "داده ها توسط گره سرویس %@ حذف نشدند. شناسه گره سرویس: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "عبارت بازیابی شما";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Ilmoitukset";
"vc_privacy_settings_title" = "Yksityisyys";
"preferences_notifications_strategy_category_title" = "Ilmoitustyyli";
"modal_seed_title" = "Palatusvirkkeesi";
"modal_seed_explanation" = "Tämä on palautusvirkkeesi. Sillä voit palauttaa tai siirtää Session ID:si uuteen laitteeseen.";
"vc_qr_code_title" = "QR-koodi";
"vc_qr_code_view_my_qr_code_tab_title" = "Näytä QR-koodini";
"vc_qr_code_view_scan_qr_code_tab_title" = "Skannaa QR-koodi";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Dataa ei poistettu yhdestä palvelusolmusta. Palvelusolmun tunnus: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Dataa ei poistettu %@ palvelusolmusta. Palvelusolmujen tunnukset: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Palatusvirkkeesi";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Notifications";
"vc_privacy_settings_title" = "Confidientalité";
"preferences_notifications_strategy_category_title" = "Stratégie de notification";
"modal_seed_title" = "Votre phrase de récupération";
"modal_seed_explanation" = "Ceci est votre phrase de récupération. Elle vous permet de restaurer ou migrer votre Session ID vers un nouvel appareil.";
"vc_qr_code_title" = "Code QR";
"vc_qr_code_view_my_qr_code_tab_title" = "Afficher mon code QR";
"vc_qr_code_view_scan_qr_code_tab_title" = "Scanner le QR Code";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Les données nont pas été supprimées sur un nœud de service. ID du nœud de service : %@.";
"dialog_clear_all_data_deletion_failed_2" = "Les données nont pas été supprimées sur %@ nœuds de service. ID des nœuds de service : %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Votre phrase de récupération";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Notifications";
"vc_privacy_settings_title" = "Privacy";
"preferences_notifications_strategy_category_title" = "Notification Strategy";
"modal_seed_title" = "Your Recovery Phrase";
"modal_seed_explanation" = "This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device.";
"vc_qr_code_title" = "QR Code";
"vc_qr_code_view_my_qr_code_tab_title" = "View My QR Code";
"vc_qr_code_view_scan_qr_code_tab_title" = "Scan QR Code";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Your Recovery Phrase";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Obavijesti";
"vc_privacy_settings_title" = "Privatnost";
"preferences_notifications_strategy_category_title" = "Strategija obavijesti";
"modal_seed_title" = "Fraza za oporavak";
"modal_seed_explanation" = "Ovo je vaša fraza za oporavak. Pomoću nje možete vratiti ili migrirati svoj Session ID na novi uređaj.";
"vc_qr_code_title" = "QR kôd";
"vc_qr_code_view_my_qr_code_tab_title" = "Pogledaj moj QR kôd";
"vc_qr_code_view_scan_qr_code_tab_title" = "Skeniraj QR kôd";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Podatke nije izbrisao 1 uslužni čvor. ID uslužnog čvora: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Podatke nije izbrisao %@ uslužni čvor. ID uslužnog čvora: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Fraza za oporavak";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Notifikasi";
"vc_privacy_settings_title" = "Privasi";
"preferences_notifications_strategy_category_title" = "Strategi notofikasi";
"modal_seed_title" = "Kata pemulihan anda";
"modal_seed_explanation" = "Ini adalah kata pemulihan anda. Gunakan untuk mengembalikan atau memindahkan Session ID anda ke perangkat lain";
"vc_qr_code_title" = "Kode QR";
"vc_qr_code_view_my_qr_code_tab_title" = "Lihat kode QR saya";
"vc_qr_code_view_scan_qr_code_tab_title" = "Pindai kode QR";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Data tidak dihapus oleh 1 Service Node. Service Node ID: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Data tidak dihapus oleh %@ Service Nodes. Service Node IDs: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Kata pemulihan anda";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Notifiche";
"vc_privacy_settings_title" = "Privacy";
"preferences_notifications_strategy_category_title" = "Strategia di notifica";
"modal_seed_title" = "Frase di recupero";
"modal_seed_explanation" = "Questa è la tua frase di recupero. Usala per ripristinare o migrare la Sessione ID a un nuovo dispositivo.";
"vc_qr_code_title" = "Codice QR";
"vc_qr_code_view_my_qr_code_tab_title" = "Visualizza il mio codice QR";
"vc_qr_code_view_scan_qr_code_tab_title" = "Scansiona il codice QR";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Dati non eliminati da 1 nodi di servizio. ID Nodo di servizio: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Dati non eliminati da %@ nodi di servizio. ID Nodo di servizio: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Frase di recupero";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "通知";
"vc_privacy_settings_title" = "プライバシー";
"preferences_notifications_strategy_category_title" = "通知戦略";
"modal_seed_title" = "あなたのリカバリーフレーズ";
"modal_seed_explanation" = "これはあなたのリカバリーフレーズです。これにより、Session ID を新しい端末に復元または移行できます。";
"vc_qr_code_title" = "QR コード";
"vc_qr_code_view_my_qr_code_tab_title" = "私の QR コードを表示する";
"vc_qr_code_view_scan_qr_code_tab_title" = "QR コードをスキャンする";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "このサービスードからデータが削除されませんでした。ID: %@";
"dialog_clear_all_data_deletion_failed_2" = "%@ つのサービスードからデータが削除されませんでした。ID %@";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "あなたのリカバリーフレーズ";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Meldingen";
"vc_privacy_settings_title" = "Privacy";
"preferences_notifications_strategy_category_title" = "Notificatie Inhoud";
"modal_seed_title" = "Uw Herstel Zin";
"modal_seed_explanation" = "Dit is uw herstel zin, Hiermee kun je je sessie-ID herstellen of migreren naar een nieuw apparaat.";
"vc_qr_code_title" = "QR-code";
"vc_qr_code_view_my_qr_code_tab_title" = "Bekijk mijn QR-code";
"vc_qr_code_view_scan_qr_code_tab_title" = "QR-code scannen";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Gegevens niet verwijderd door 1 Service Node. Service Node ID: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Gegevens niet verwijderd door %@ Service Nodes. Service Node IDs: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Uw Herstel Zin";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Powiadomienia";
"vc_privacy_settings_title" = "Prywatność";
"preferences_notifications_strategy_category_title" = "Strategia powiadomień";
"modal_seed_title" = "Twoja fraza odzyskiwania";
"modal_seed_explanation" = "To jest twoja fraza odzyskiwania. Dzięki niej możesz przywrócić lub przenieść identyfikator Session na nowe urządzenie.";
"vc_qr_code_title" = "Kod QR";
"vc_qr_code_view_my_qr_code_tab_title" = "Wyświetl mój kod QR";
"vc_qr_code_view_scan_qr_code_tab_title" = "Skanowania QR code";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Dane nie zostały usunięte przez 1 węzeł usługowy. Identyfikator węzła: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Dane nie zostały usunięte przez %@ węzły usługowe. Identyfikatory węzłów: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Twoja fraza odzyskiwania";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Notificações";
"vc_privacy_settings_title" = "Privacidade";
"preferences_notifications_strategy_category_title" = "Estratégia de notificação";
"modal_seed_title" = "Sua frase de recuperação";
"modal_seed_explanation" = "Esta é sua frase de recuperação. Com ela, você pode restaurar ou migrar seu ID Session para um novo dispositivo.";
"vc_qr_code_title" = "Código QR";
"vc_qr_code_view_my_qr_code_tab_title" = "Ver meu código QR";
"vc_qr_code_view_scan_qr_code_tab_title" = "Escanear código QR";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Dados não excluídos por 1 Service Node. ID do Service Node: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Dados não excluídos por %@ Service Nodes. IDs dos Service Nodes: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Sua frase de recuperação";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Уведомления";
"vc_privacy_settings_title" = "Конфиденциальность";
"preferences_notifications_strategy_category_title" = "Метод уведомлений";
"modal_seed_title" = "Ваша секретная фраза";
"modal_seed_explanation" = "Это ваша секретная фраза. С ее помощью вы можете восстановить или перенести свой Session ID на новое устройство.";
"vc_qr_code_title" = "QR-код";
"vc_qr_code_view_my_qr_code_tab_title" = "Посмотреть мой QR-код";
"vc_qr_code_view_scan_qr_code_tab_title" = "Сканировать QR-код";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Данные не удалены 1 узлом сервиса. Номер узла: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Данные не удалены %@ узлами сервиса. Номера узлов: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Ваша секретная фраза";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "දැනුම්දීම්";
"vc_privacy_settings_title" = "පෞද්ගලිකත්වය";
"preferences_notifications_strategy_category_title" = "Notification Strategy";
"modal_seed_title" = "Your Recovery Phrase";
"modal_seed_explanation" = "This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device.";
"vc_qr_code_title" = "QR Code";
"vc_qr_code_view_my_qr_code_tab_title" = "View My QR Code";
"vc_qr_code_view_scan_qr_code_tab_title" = "Scan QR Code";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Your Recovery Phrase";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Hlásenia";
"vc_privacy_settings_title" = "Súkromie";
"preferences_notifications_strategy_category_title" = "Stratégia upozornení";
"modal_seed_title" = "Vaša fráza pre obnovenie";
"modal_seed_explanation" = "Toto je vaša fráza pre obnovenie. S jej pomocou môžete obnoviť alebo presunúť svoje Session ID na nové zariadenie.";
"vc_qr_code_title" = "QR kód";
"vc_qr_code_view_my_qr_code_tab_title" = "Zobraziť môj QR kód";
"vc_qr_code_view_scan_qr_code_tab_title" = "Skenovať QR kód";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Data neboli odstránené 1 Servisným Uzlom. Servisný Uzol ID:%@.";
"dialog_clear_all_data_deletion_failed_2" = "Data neboli odstránené 1 Servisným Uzlom. Servisný Uzol ID:%@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Vaša fráza pre obnovenie";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Aviseringar";
"vc_privacy_settings_title" = "Integritet";
"preferences_notifications_strategy_category_title" = "Strategi för aviseringar";
"modal_seed_title" = "Din Återställningsfras";
"modal_seed_explanation" = "Detta är din återställningsfras. Med den kan du återställa eller migrera ditt Sessions-ID till en ny enhet.";
"vc_qr_code_title" = "QR-kod";
"vc_qr_code_view_my_qr_code_tab_title" = "Visa min QR-kod";
"vc_qr_code_view_scan_qr_code_tab_title" = "Skanna QR-kod";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Din Återställningsfras";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "การแจ้งเตือน";
"vc_privacy_settings_title" = "ความเป็นส่วนตัว";
"preferences_notifications_strategy_category_title" = "กลยุทธ์สำคัญแจ้งเตือน";
"modal_seed_title" = "วลีกู้คืนของคุณ";
"modal_seed_explanation" = "นี่คือวลีกู้คืนของคุณ ด้วยวิธีนี้ คุณสามารถกู้คืนหรือย้ายไอดีเซสชันSessionของคุณไปยังอุปกรณ์ใหม่ได้";
"vc_qr_code_title" = "QR โค้ด";
"vc_qr_code_view_my_qr_code_tab_title" = "แสดง QR โค้ดของคุน";
"vc_qr_code_view_scan_qr_code_tab_title" = "สแกน QR โค้ด";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "วลีกู้คืนของคุณ";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "Thông báo";
"vc_privacy_settings_title" = "Riêng tưy";
"preferences_notifications_strategy_category_title" = "Chiến lược thông báo";
"modal_seed_title" = "Cụm từ khôi phục của bạn";
"modal_seed_explanation" = "Đây là cụm từ khôi phục của bạn. Bạn có thể dùng nó để khôi phục hoặc chuyển Session ID của mình sang một thiết bị mới.";
"vc_qr_code_title" = "Mã QR";
"vc_qr_code_view_my_qr_code_tab_title" = "Xem mã QR của tôi";
"vc_qr_code_view_scan_qr_code_tab_title" = "Quét mã QR";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@.";
"dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@.";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Cụm từ khôi phục của bạn";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "通知";
"vc_privacy_settings_title" = "隱私權條款";
"preferences_notifications_strategy_category_title" = "通知類型";
"modal_seed_title" = "您的回復用字句";
"modal_seed_explanation" = "這是您的回復用字句,您可以利用此字句來回復或轉移您的帳號至新的裝置上。";
"vc_qr_code_title" = "QR Code";
"vc_qr_code_view_my_qr_code_tab_title" = "查看我的 QR Code";
"vc_qr_code_view_scan_qr_code_tab_title" = "掃描 QR Code";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "數據已被1個服務節點刪除。節點ID: %@";
"dialog_clear_all_data_deletion_failed_2" = "數據沒有被%@個服務節點刪除。節點ID: %@";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "您的回復用字句";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -520,8 +520,6 @@
"vc_notification_settings_title" = "通知";
"vc_privacy_settings_title" = "隐私";
"preferences_notifications_strategy_category_title" = "通知选项";
"modal_seed_title" = "您的恢复口令";
"modal_seed_explanation" = "这是您的恢复口令。您可以通过该口令将Session ID还原或迁移到新设备上。";
"vc_qr_code_title" = "二维码";
"vc_qr_code_view_my_qr_code_tab_title" = "查看我的二维码";
"vc_qr_code_view_scan_qr_code_tab_title" = "扫描二维码";
@ -701,7 +699,7 @@
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message.";
"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats.";
@ -730,6 +728,18 @@
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only";
"NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content";
"CONVERSATION_SETTINGS_TITLE" = "Conversations";
"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities";
"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages.";
"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages";
"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts.";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?";
"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock";
"APPEARANCE_TITLE" = "Appearance";
"APPEARANCE_THEMES_TITLE" = "Themes";
"APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour";
@ -754,3 +764,5 @@
"dialog_clear_all_data_deletion_failed_1" = "数据未被一个服务节点删除。服务节点ID %@";
"dialog_clear_all_data_deletion_failed_2" = "数据未被 %@ 服务节点删除。服务节点ID %@";
"modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "您的恢复口令";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";

View File

@ -15,6 +15,7 @@ final class PathVC: BaseVC {
private lazy var pathStackView: UIStackView = {
let result = UIStackView()
result.axis = .vertical
return result
}()
@ -22,6 +23,7 @@ final class PathVC: BaseVC {
let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
result.set(.width, to: 64)
result.set(.height, to: 64)
return result
}()
@ -29,6 +31,7 @@ final class PathVC: BaseVC {
let result = OutlineButton(style: .regular, size: .large)
result.setTitle("vc_path_learn_more_button_title".localized(), for: UIControl.State.normal)
result.addTarget(self, action: #selector(learnMore), for: UIControl.Event.touchUpInside)
return result
}()
@ -50,11 +53,12 @@ final class PathVC: BaseVC {
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
explanationLabel.text = "vc_path_explanation".localized()
explanationLabel.numberOfLines = 0
explanationLabel.themeTextColor = .textSecondary
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.numberOfLines = 0
// Set up path stack view
let pathStackViewContainer = UIView()
pathStackViewContainer.addSubview(pathStackView)
@ -68,12 +72,15 @@ final class PathVC: BaseVC {
pathStackViewContainer.trailingAnchor.constraint(greaterThanOrEqualTo: spinner.trailingAnchor).isActive = true
pathStackViewContainer.bottomAnchor.constraint(greaterThanOrEqualTo: spinner.bottomAnchor).isActive = true
spinner.center(in: pathStackViewContainer)
// Set up rebuild path button
let inset: CGFloat = isIPhone5OrSmaller ? 64 : 80
let learnMoreButtonContainer = UIView(wrapping: learnMoreButton, withInsets: UIEdgeInsets(top: 0, leading: inset, bottom: 0, trailing: inset), shouldAdaptForIPadWithWidth: Values.iPadButtonWidth)
// Set up spacers
let topSpacer = UIView.vStretchingSpacer()
let bottomSpacer = UIView.vStretchingSpacer()
// Set up main stack view
let mainStackView = UIStackView(arrangedSubviews: [ explanationLabel, topSpacer, pathStackViewContainer, bottomSpacer, learnMoreButtonContainer ])
mainStackView.axis = .vertical
@ -82,8 +89,10 @@ final class PathVC: BaseVC {
mainStackView.isLayoutMarginsRelativeArrangement = true
view.addSubview(mainStackView)
mainStackView.pin(to: view)
// Set up spacer constraints
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor).isActive = true
// Perform initial update
update()
}
@ -99,7 +108,8 @@ final class PathVC: BaseVC {
NotificationCenter.default.removeObserver(self)
}
// MARK: Updating
// MARK: - Updating
@objc private func handleBuildingPathsNotification() { update() }
@objc private func handlePathsBuiltNotification() { update() }
@objc private func handleOnionRequestPathCountriesLoadedNotification() { update() }
@ -130,14 +140,14 @@ final class PathVC: BaseVC {
}
let youRow = getPathRow(
title: NSLocalizedString("vc_path_device_row_title", comment: ""),
title: "vc_path_device_row_title".localized(),
subtitle: nil,
location: .top,
dotAnimationStartDelay: 1,
dotAnimationRepeatInterval: dotAnimationRepeatInterval
)
let destinationRow = getPathRow(
title: NSLocalizedString("vc_path_destination_row_title", comment: ""),
title: "vc_path_destination_row_title".localized(),
subtitle: nil,
location: .bottom,
dotAnimationStartDelay: Double(pathToDisplay.count) + 2,
@ -152,7 +162,8 @@ final class PathVC: BaseVC {
}
}
// MARK: General
// MARK: - General
private func getPathRow(title: String, subtitle: String?, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) -> UIStackView {
let lineView = LineView(
location: location,
@ -194,7 +205,8 @@ final class PathVC: BaseVC {
return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
}
// MARK: Interaction
// MARK: - Interaction
@objc private func learnMore() {
let urlAsString = "https://getsession.org/faq/#onion-routing"
let url = URL(string: urlAsString)!
@ -202,8 +214,9 @@ final class PathVC: BaseVC {
}
}
// MARK: Line View
private final class LineView : UIView {
// MARK: - Line View
private final class LineView: UIView {
private let location: Location
private let dotAnimationStartDelay: Double
private let dotAnimationRepeatInterval: Double
@ -217,12 +230,22 @@ private final class LineView : UIView {
private lazy var dotView: UIView = {
let result = UIView()
result.layer.cornerRadius = PathVC.dotSize / 2
let glowRadius: CGFloat = isLightMode ? 1 : 2
let glowColor = isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black
let glowConfiguration = UIView.CircularGlowConfiguration(size: PathVC.dotSize, color: glowColor, isAnimated: true, animationDuration: 0.5, radius: glowRadius)
result.setCircularGlow(with: glowConfiguration)
result.backgroundColor = Colors.accent
result.themeBackgroundColor = .path_connected
result.layer.themeShadowColor = .path_connected
result.layer.shadowOffset = .zero
result.layer.shadowPath = UIBezierPath(
ovalIn: CGRect(
origin: CGPoint.zero,
size: CGSize(width: PathVC.dotSize, height: PathVC.dotSize)
)
).cgPath
result.layer.cornerRadius = (PathVC.dotSize / 2)
ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in
result?.layer.shadowOpacity = (theme.interfaceStyle == .light ? 0.4 : 1)
result?.layer.shadowRadius = (theme.interfaceStyle == .light ? 1 : 2)
}
return result
}()
@ -247,28 +270,33 @@ private final class LineView : UIView {
private func setUpViewHierarchy() {
let lineView = UIView()
lineView.set(.width, to: Values.separatorThickness)
lineView.backgroundColor = Colors.text
lineView.themeBackgroundColor = .textPrimary
addSubview(lineView)
lineView.center(.horizontal, in: self)
switch location {
case .top: lineView.topAnchor.constraint(equalTo: centerYAnchor).isActive = true
case .middle, .bottom: lineView.pin(.top, to: .top, of: self)
case .top: lineView.topAnchor.constraint(equalTo: centerYAnchor).isActive = true
case .middle, .bottom: lineView.pin(.top, to: .top, of: self)
}
switch location {
case .top, .middle: lineView.pin(.bottom, to: .bottom, of: self)
case .bottom: lineView.bottomAnchor.constraint(equalTo: centerYAnchor).isActive = true
case .top, .middle: lineView.pin(.bottom, to: .bottom, of: self)
case .bottom: lineView.bottomAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
let dotSize = PathVC.dotSize
dotViewWidthConstraint = dotView.set(.width, to: dotSize)
dotViewHeightConstraint = dotView.set(.height, to: dotSize)
addSubview(dotView)
dotView.center(in: self)
let repeatInterval: TimeInterval = self.dotAnimationRepeatInterval
Timer.scheduledTimer(withTimeInterval: dotAnimationStartDelay, repeats: false) { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.animate()
strongSelf.dotViewAnimationTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.dotAnimationRepeatInterval, repeats: true) { _ in
guard let strongSelf = self else { return }
strongSelf.animate()
self?.animate()
self?.dotViewAnimationTimer = Timer.scheduledTimer(withTimeInterval: repeatInterval, repeats: true) { _ in
self?.animate()
}
}
}
@ -279,36 +307,21 @@ private final class LineView : UIView {
private func animate() {
expandDot()
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in
self?.collapseDot()
}
}
private func expandDot() {
let newSize = PathVC.expandedDotSize
let newGlowRadius: CGFloat = isLightMode ? 4 : 6
let newGlowColor = Colors.accent.withAlphaComponent(0.6)
updateDotView(size: newSize, glowRadius: newGlowRadius, glowColor: newGlowColor)
UIView.animate(withDuration: 0.5) { [weak self] in
self?.dotView.transform = CGAffineTransform.scale(PathVC.expandedDotSize / PathVC.dotSize)
}
}
private func collapseDot() {
let newSize = PathVC.dotSize
let newGlowRadius: CGFloat = isLightMode ? 1 : 2
let newGlowColor = isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black
updateDotView(size: newSize, glowRadius: newGlowRadius, glowColor: newGlowColor)
}
private func updateDotView(size: CGFloat, glowRadius: CGFloat, glowColor: UIColor) {
let frame = CGRect(center: dotView.center, size: CGSize(width: size, height: size))
dotViewWidthConstraint.constant = size
dotViewHeightConstraint.constant = size
UIView.animate(withDuration: 0.5) {
self.layoutIfNeeded()
self.dotView.frame = frame
self.dotView.layer.cornerRadius = size / 2
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, animationDuration: 0.5, radius: glowRadius)
self.dotView.setCircularGlow(with: glowConfiguration)
self.dotView.backgroundColor = Colors.accent
UIView.animate(withDuration: 0.5) { [weak self] in
self?.dotView.transform = CGAffineTransform.scale(1)
}
}
}

View File

@ -0,0 +1,382 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import DifferenceKit
import SessionUIKit
import SessionMessagingKit
import SignalUtilitiesKit
class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
private static let loadingHeaderHeight: CGFloat = 20
private let viewModel: BlockedContactsViewModel = BlockedContactsViewModel()
private var dataChangeObservable: DatabaseCancellable?
private var hasLoadedInitialContactData: Bool = false
private var isLoadingMore: Bool = false
private var isAutoLoadingNextPage: Bool = false
private var viewHasAppeared: Bool = false
// MARK: - Intialization
init() {
Storage.shared.addObserver(viewModel.pagedDataObserver)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
preconditionFailure("Use init() instead.")
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - UI
private lazy var tableView: UITableView = {
let result: UITableView = UITableView()
result.translatesAutoresizingMaskIntoConstraints = false
result.backgroundColor = .clear
result.separatorStyle = .none
result.register(view: BlockedContactCell.self)
result.dataSource = self
result.delegate = self
let bottomInset = Values.newConversationButtonBottomOffset + NewConversationButtonSet.expandedButtonSize + Values.largeSpacing + NewConversationButtonSet.collapsedButtonSize
result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
result.showsVerticalScrollIndicator = false
if #available(iOS 15.0, *) {
result.sectionHeaderTopPadding = 0
}
return result
}()
private lazy var emptyStateLabel: UILabel = {
let result: UILabel = UILabel()
result.translatesAutoresizingMaskIntoConstraints = false
result.isUserInteractionEnabled = false
result.font = UIFont.systemFont(ofSize: Values.smallFontSize)
result.text = NSLocalizedString("CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE", comment: "")
result.textColor = Colors.text
result.textAlignment = .center
result.numberOfLines = 0
result.isHidden = true
return result
}()
private lazy var unblockButton: OutlineButton = {
let result: OutlineButton = OutlineButton(style: .destructive, size: .large)
result.translatesAutoresizingMaskIntoConstraints = false
result.setTitle("CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK".localized(), for: .normal)
result.addTarget(self, action: #selector(unblockTapped), for: .touchUpInside)
return result
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
ViewControllerUtilities.setUpDefaultSessionStyle(
for: self,
title: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE".localized(),
hasCustomBackButton: false
)
// Add the UI (MUST be done after the thread freeze so the 'tableView' creation and setting
// the dataSource has the correct data)
view.addSubview(tableView)
view.addSubview(emptyStateLabel)
view.addSubview(unblockButton)
setupLayout()
// Notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidBecomeActive(_:)),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidResignActive(_:)),
name: UIApplication.didEnterBackgroundNotification, object: nil
)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
startObservingChanges()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.viewHasAppeared = true
self.autoLoadNextPageIfNeeded()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Stop observing database changes
dataChangeObservable?.cancel()
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
startObservingChanges(didReturnFromBackground: true)
}
@objc func applicationDidResignActive(_ notification: Notification) {
// Stop observing database changes
dataChangeObservable?.cancel()
}
// MARK: - Layout
private func setupLayout() {
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.smallSpacing),
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
emptyStateLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.massiveSpacing),
emptyStateLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.mediumSpacing),
emptyStateLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Values.mediumSpacing),
emptyStateLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
unblockButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
unblockButton.bottomAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -Values.largeSpacing
),
unblockButton.widthAnchor.constraint(equalToConstant: Values.iPadButtonWidth),
unblockButton.heightAnchor.constraint(equalToConstant: NewConversationButtonSet.collapsedButtonSize)
])
}
// MARK: - Updating
private func startObservingChanges(didReturnFromBackground: Bool = false) {
self.viewModel.onContactChange = { [weak self] updatedContactData in
self?.handleContactUpdates(updatedContactData)
}
// Note: When returning from the background we could have received notifications but the
// PagedDatabaseObserver won't have them so we need to force a re-fetch of the current
// data to ensure everything is up to date
if didReturnFromBackground {
self.viewModel.pagedDataObserver?.reload()
}
}
private func handleContactUpdates(_ updatedData: [BlockedContactsViewModel.SectionModel], initialLoad: Bool = false) {
// Ensure the first load runs without animations (if we don't do this the cells will animate
// in from a frame of CGRect.zero)
guard hasLoadedInitialContactData else {
hasLoadedInitialContactData = true
UIView.performWithoutAnimation { handleContactUpdates(updatedData, initialLoad: true) }
return
}
// Show the empty state if there is no data
unblockButton.isEnabled = !viewModel.selectedContactIds.isEmpty
unblockButton.isHidden = updatedData.isEmpty
emptyStateLabel.isHidden = !updatedData.isEmpty
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
// Complete page loading
self?.isLoadingMore = false
self?.autoLoadNextPageIfNeeded()
}
// Reload the table content (animate changes after the first load)
tableView.reload(
using: StagedChangeset(source: viewModel.contactData, target: updatedData),
deleteSectionsAnimation: .none,
insertSectionsAnimation: .none,
reloadSectionsAnimation: .none,
deleteRowsAnimation: .bottom,
insertRowsAnimation: .top,
reloadRowsAnimation: .none,
interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues
) { [weak self] updatedData in
self?.viewModel.updateContactData(updatedData)
}
CATransaction.commit()
}
private func autoLoadNextPageIfNeeded() {
guard !self.isAutoLoadingNextPage && !self.isLoadingMore else { return }
self.isAutoLoadingNextPage = true
DispatchQueue.main.asyncAfter(deadline: .now() + PagedData.autoLoadNextPageDelay) { [weak self] in
self?.isAutoLoadingNextPage = false
// Note: We sort the headers as we want to prioritise loading newer pages over older ones
let sections: [(BlockedContactsViewModel.Section, CGRect)] = (self?.viewModel.contactData
.enumerated()
.map { index, section in
(section.model, (self?.tableView.rectForHeader(inSection: index) ?? .zero))
})
.defaulting(to: [])
let shouldLoadMore: Bool = sections
.contains { section, headerRect in
section == .loadMore &&
headerRect != .zero &&
(self?.tableView.bounds.contains(headerRect) == true)
}
guard shouldLoadMore else { return }
self?.isLoadingMore = true
DispatchQueue.global(qos: .default).async { [weak self] in
self?.viewModel.pagedDataObserver?.load(.pageAfter)
}
}
}
// MARK: - UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int {
return viewModel.contactData.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section]
return section.elements.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[indexPath.section]
switch section.model {
case .contacts:
let cellViewModel: BlockedContactsViewModel.DataModel = section.elements[indexPath.row]
let cell: BlockedContactCell = tableView.dequeue(type: BlockedContactCell.self, for: indexPath)
cell.update(
with: cellViewModel,
isSelected: viewModel.selectedContactIds.contains(cellViewModel.id)
)
return cell
default: preconditionFailure("Other sections should have no content")
}
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section]
switch section.model {
case .loadMore:
let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium)
loadingIndicator.tintColor = Colors.text
loadingIndicator.alpha = 0.5
loadingIndicator.startAnimating()
let view: UIView = UIView()
view.addSubview(loadingIndicator)
loadingIndicator.center(in: view)
return view
default: return nil
}
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section]
switch section.model {
case .loadMore: return BlockedContactsViewController.loadingHeaderHeight
default: return 0
}
}
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
guard self.hasLoadedInitialContactData && self.viewHasAppeared && !self.isLoadingMore else { return }
let section: BlockedContactsViewModel.SectionModel = self.viewModel.contactData[section]
switch section.model {
case .loadMore:
self.isLoadingMore = true
DispatchQueue.global(qos: .default).async { [weak self] in
self?.viewModel.pagedDataObserver?.load(.pageAfter)
}
default: break
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let section: BlockedContactsViewModel.SectionModel = self.viewModel.contactData[indexPath.section]
switch section.model {
case .contacts:
let cellViewModel: BlockedContactsViewModel.DataModel = section.elements[indexPath.row]
self.viewModel.toggleSelection(contactId: cellViewModel.id)
self.tableView.reloadRows(at: [indexPath], with: .none)
self.unblockButton.isEnabled = !self.viewModel.selectedContactIds.isEmpty
default: break
}
}
// MARK: - Interaction
@objc private func unblockTapped() {
guard !viewModel.selectedContactIds.isEmpty else { return }
let contactIds: Set<String> = viewModel.selectedContactIds
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE".localized(),
confirmTitle: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON".localized(),
confirmStyle: .danger,
cancelStyle: .textPrimary
)
) { [weak self] _ in
// Unblock the contacts
Storage.shared.write { db in
_ = try Contact
.filter(ids: contactIds)
.updateAll(db, Contact.Columns.isBlocked.set(to: false))
// Force a config sync
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
}
self?.dismiss(animated: true, completion: nil)
}
self.present(confirmationModal, animated: true, completion: nil)
}
}

View File

@ -0,0 +1,214 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import DifferenceKit
import SignalUtilitiesKit
public class BlockedContactsViewModel {
public typealias SectionModel = ArraySection<Section, DataModel>
// MARK: - Section
public enum Section: Differentiable {
case contacts
case loadMore
}
// MARK: - Variables
public static let pageSize: Int = 30
// MARK: - Initialization
init() {
self.pagedDataObserver = nil
// Note: Since this references self we need to finish initializing before setting it, we
// also want to skip the initial query and trigger it async so that the push animation
// doesn't stutter (it should load basically immediately but without this there is a
// distinct stutter)
self.pagedDataObserver = PagedDatabaseObserver(
pagedTable: Profile.self,
pageSize: BlockedContactsViewModel.pageSize,
idColumn: .id,
observedChanges: [
PagedData.ObservedChanges(
table: Profile.self,
columns: [
.id,
.name,
.nickname,
.profilePictureFileName
]
),
PagedData.ObservedChanges(
table: Contact.self,
columns: [.isBlocked],
joinToPagedType: {
let profile: TypedTableAlias<Profile> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias()
return SQL("JOIN \(Contact.self) ON \(contact[.id]) = \(profile[.id])")
}()
)
],
/// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query
joinSQL: DataModel.optimisedJoinSQL,
filterSQL: DataModel.filterSQL,
orderSQL: DataModel.orderSQL,
dataQuery: DataModel.query(
filterSQL: DataModel.filterSQL,
orderSQL: DataModel.orderSQL
),
onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in
guard let updatedContactData: [SectionModel] = self?.process(data: updatedData, for: updatedPageInfo) else {
return
}
// If we have the 'onThreadChange' callback then trigger it, otherwise just store the changes
// to be sent to the callback if we ever start observing again (when we have the callback it needs
// to do the data updating as it's tied to UI updates and can cause crashes if not updated in the
// correct order)
guard let onContactChange: (([SectionModel]) -> ()) = self?.onContactChange else {
self?.unobservedContactDataChanges = updatedContactData
return
}
onContactChange(updatedContactData)
}
)
// Run the initial query on a background thread so we don't block the push transition
DispatchQueue.global(qos: .default).async { [weak self] in
// The `.pageBefore` will query from a `0` offset loading the first page
self?.pagedDataObserver?.load(.pageBefore)
}
}
// MARK: - Contact Data
public private(set) var selectedContactIds: Set<String> = []
public private(set) var unobservedContactDataChanges: [SectionModel]?
public private(set) var contactData: [SectionModel] = []
public private(set) var pagedDataObserver: PagedDatabaseObserver<Profile, DataModel>?
public var onContactChange: (([SectionModel]) -> ())? {
didSet {
// When starting to observe interaction changes we want to trigger a UI update just in case the
// data was changed while we weren't observing
if let unobservedContactDataChanges: [SectionModel] = self.unobservedContactDataChanges {
onContactChange?(unobservedContactDataChanges)
self.unobservedContactDataChanges = nil
}
}
}
private func process(data: [DataModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] {
// Update the 'selectedContactIds' to only include selected contacts which are within the
// data (ie. handle profile deletions)
let profileIds: Set<String> = data.map { $0.id }.asSet()
selectedContactIds = selectedContactIds.intersection(profileIds)
return [
[
SectionModel(
section: .contacts,
elements: data
.sorted { lhs, rhs -> Bool in
lhs.profile.displayName() > rhs.profile.displayName()
}
)
],
(!data.isEmpty && (pageInfo.pageOffset + pageInfo.currentCount) < pageInfo.totalCount ?
[SectionModel(section: .loadMore)] :
[]
)
].flatMap { $0 }
}
public func updateContactData(_ updatedData: [SectionModel]) {
self.contactData = updatedData
}
public func toggleSelection(contactId: String) {
guard selectedContactIds.contains(contactId) else {
selectedContactIds.insert(contactId)
return
}
selectedContactIds.remove(contactId)
}
// MARK: - DataModel
public struct DataModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable {
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue)
public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue)
public static let profileString: String = CodingKeys.profile.stringValue
public var differenceIdentifier: String { profile.id }
public var id: String { profile.id }
public let rowId: Int64
public let profile: Profile
static func query(
filterSQL: SQL,
orderSQL: SQL
) -> (([Int64]) -> AdaptedFetchRequest<SQLRequest<DataModel>>) {
return { rowIds -> AdaptedFetchRequest<SQLRequest<DataModel>> in
let profile: TypedTableAlias<Profile> = TypedTableAlias()
/// **Note:** The `numColumnsBeforeProfile` value **MUST** match the number of fields before
/// the `DataModel.profileKey` entry below otherwise the query will fail to
/// parse and might throw
///
/// Explicitly set default values for the fields ignored for search results
let numColumnsBeforeProfile: Int = 1
let request: SQLRequest<DataModel> = """
SELECT
\(profile.alias[Column.rowID]) AS \(DataModel.rowIdKey),
\(DataModel.profileKey).*
FROM \(Profile.self)
WHERE \(profile.alias[Column.rowID]) IN \(rowIds)
ORDER BY \(orderSQL)
"""
return request.adapted { db in
let adapters = try splittingRowAdapters(columnCounts: [
numColumnsBeforeProfile,
Profile.numberOfSelectedColumns(db)
])
return ScopeAdapter([
DataModel.profileString: adapters[1]
])
}
}
}
static var optimisedJoinSQL: SQL = {
let profile: TypedTableAlias<Profile> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias()
return SQL("JOIN \(Contact.self) ON \(contact[.id]) = \(profile[.id])")
}()
static var filterSQL: SQL = {
let contact: TypedTableAlias<Contact> = TypedTableAlias()
return SQL("\(contact[.isBlocked]) = true")
}()
static let orderSQL: SQL = {
let profile: TypedTableAlias<Profile> = TypedTableAlias()
return SQL("IFNULL(IFNULL(\(profile[.nickname]), \(profile[.name])), \(profile[.id])) ASC")
}()
}
}

View File

@ -1,62 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SignalUtilitiesKit
// FIXME: Refactor to be MVVM and use database observation
class ConversationSettingsViewController: OWSTableViewController {
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.updateTableContents()
ViewControllerUtilities.setUpDefaultSessionStyle(for: self, title: "CONVERSATIONS_TITLE".localized(), hasCustomBackButton: false)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.updateTableContents()
}
// MARK: - Table Contents
func updateTableContents() {
let updatedContents: OWSTableContents = OWSTableContents()
let messageTrimming: OWSTableSection = OWSTableSection()
messageTrimming.headerTitle = "MESSAGE_TRIMMING_TITLE".localized()
messageTrimming.footerTitle = "MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION".localized()
messageTrimming.add(OWSTableItem.switch(
withText: "MESSAGE_TRIMMING_OPEN_GROUP_TITLE".localized(),
isOn: { Storage.shared[.trimOpenGroupMessagesOlderThanSixMonths] },
target: self,
selector: #selector(didToggleTrimOpenGroupsSwitch(_:))
))
updatedContents.addSection(messageTrimming)
self.contents = updatedContents
}
// MARK: - Actions
@objc private func didToggleTrimOpenGroupsSwitch(_ sender: UISwitch) {
let switchIsOn: Bool = sender.isOn
Storage.shared.writeAsync(
updates: { db in
db[.trimOpenGroupMessagesOlderThanSixMonths] = !switchIsOn
},
completion: { [weak self] _, _ in
self?.updateTableContents()
}
)
}
@objc private func close(_ sender: UIBarButtonItem) {
self.navigationController?.dismiss(animated: true)
}
}

View File

@ -0,0 +1,91 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import DifferenceKit
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
class ConversationSettingsViewModel: SettingsTableViewModel<ConversationSettingsViewModel.Section, ConversationSettingsViewModel.Section> {
// MARK: - Section
public enum Section: SettingSection {
case messageTrimming
case audioMessages
case blockedContacts
var title: String {
switch self {
case .messageTrimming: return "CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING".localized()
case .audioMessages: return "CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES".localized()
case .blockedContacts: return "" // No title
}
}
}
// MARK: - Content
override var title: String { "CONVERSATION_SETTINGS_TITLE".localized() }
private var _settingsData: [SectionModel] = []
public override var settingsData: [SectionModel] { _settingsData }
public override var observableSettingsData: ObservableData { _observableSettingsData }
/// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise
/// performance https://github.com/groue/GRDB.swift#valueobservation-performance
///
/// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`)
/// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`)
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
private lazy var _observableSettingsData: ObservableData = ValueObservation
.trackingConstantRegion { db -> [SectionModel] in
return [
SectionModel(
model: .messageTrimming,
elements: [
SettingInfo(
id: .messageTrimming,
title: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE".localized(),
subtitle: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION".localized(),
action: .settingBool(key: .trimOpenGroupMessagesOlderThanSixMonths)
)
]
),
SectionModel(
model: .audioMessages,
elements: [
SettingInfo(
id: .audioMessages,
title: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE".localized(),
subtitle: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION".localized(),
action: .settingBool(key: .shouldAutoPlayConsecutiveAudioMessages)
)
]
),
SectionModel(
model: .blockedContacts,
elements: [
SettingInfo(
id: .blockedContacts,
title: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE".localized(),
action: .dangerPush(createDestination: {
BlockedContactsViewController()
})
)
]
)
]
}
.removeDuplicates()
// MARK: - Functions
public override func updateSettings(_ updatedSettings: [SectionModel]) {
self._settingsData = updatedSettings
}
public override func saveChanges() {}
}

View File

@ -46,15 +46,9 @@ class HelpViewModel: SettingsTableViewModel<HelpViewModel.Section, HelpViewModel
id: .report,
title: "HELP_REPORT_BUG_TITLE".localized(),
subtitle: "HELP_REPORT_BUG_DESCRIPTION".localized(),
action: .rightButtonModal(
action: .rightButtonAction(
title: "HELP_REPORT_BUG_ACTION_TITLE".localized(),
createModal: {
let shareLogsModal: ShareLogsModal = ShareLogsModal()
shareLogsModal.modalPresentationStyle = .overFullScreen
shareLogsModal.modalTransitionStyle = .crossDissolve
return shareLogsModal
}
action: { HelpViewModel.shareLogs(targetView: $0) }
)
)
]
@ -134,4 +128,47 @@ class HelpViewModel: SettingsTableViewModel<HelpViewModel.Section, HelpViewModel
}
public override func saveChanges() {}
public static func shareLogs(
viewControllerToDismiss: UIViewController? = nil,
targetView: UIView? = nil,
onShareComplete: (() -> ())? = nil
) {
let version: String = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)
.defaulting(to: "")
OWSLogger.info("[Version] iOS \(UIDevice.current.systemVersion) \(version)")
DDLog.flushLog()
let logFilePaths: [String] = AppEnvironment.shared.fileLogger.logFileManager.sortedLogFilePaths
guard
let latestLogFilePath: String = logFilePaths.first,
let viewController: UIViewController = CurrentAppContext().frontmostViewController()
else { return }
let showShareSheet: () -> () = {
let shareVC = UIActivityViewController(
activityItems: [ URL(fileURLWithPath: latestLogFilePath) ],
applicationActivities: nil
)
shareVC.completionWithItemsHandler = { _, _, _, _ in onShareComplete?() }
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : [])
shareVC.popoverPresentationController?.sourceView = (targetView ?? viewController.view)
shareVC.popoverPresentationController?.sourceRect = (targetView ?? viewController.view).bounds
}
viewController.present(shareVC, animated: true, completion: nil)
}
guard let viewControllerToDismiss: UIViewController = viewControllerToDismiss else {
showShareSheet()
return
}
viewControllerToDismiss.dismiss(animated: true) {
showShareSheet()
}
}
}

View File

@ -8,9 +8,17 @@ import SessionMessagingKit
import SessionUtilitiesKit
class NotificationSoundViewModel: SettingsTableViewModel<NotificationSettingsViewModel.Section, Preferences.Sound> {
// FIXME: Remove `threadId` once we ditch the per-thread notification sound
private let threadId: String?
private var audioPlayer: OWSAudioPlayer?
private var currentSelection: Preferences.Sound?
// MARK: - Initialization
init(threadId: String? = nil) {
self.threadId = threadId
}
deinit {
self.audioPlayer?.stop()
self.audioPlayer = nil
@ -106,3 +114,14 @@ class NotificationSoundViewModel: SettingsTableViewModel<NotificationSettingsVie
}
}
}
// MARK: - Objective-C Support
// FIXME: Remove this once we ditch the per-thread notification sound
@objc class OWSNotificationSoundSettings: NSObject {
@objc static func create(with threadId: String?) -> UIViewController {
return SettingsTableViewController(
viewModel: NotificationSoundViewModel(threadId: threadId)
)
}
}

View File

@ -7,7 +7,6 @@ import SessionMessagingKit
import SignalUtilitiesKit
final class NukeDataModal: Modal {
// MARK: - Components
private lazy var titleLabel: UILabel = {

View File

@ -89,6 +89,32 @@ class PrivacySettingsViewModel: SettingsTableViewModel<PrivacySettingsViewModel.
id: .typingIndicators,
title: "PRIVACY_TYPING_INDICATORS_TITLE".localized(),
subtitle: "PRIVACY_TYPING_INDICATORS_DESCRIPTION".localized(),
subtitleExtraViewGenerator: {
let targetHeight: CGFloat = 20
let targetWidth: CGFloat = ceil(20 * (targetHeight / 12))
let result: UIView = UIView(
frame: CGRect(x: 0, y: 0, width: targetWidth, height: targetHeight)
)
result.set(.width, to: targetWidth)
result.set(.height, to: targetHeight)
// Use a transform scale to reduce the size of the typing indicator to the
// desired size (this way the animation remains intact)
let cell: TypingIndicatorCell = TypingIndicatorCell()
cell.transform = CGAffineTransform.scale(targetHeight / cell.bounds.height)
cell.typingIndicatorView.startAnimation()
result.addSubview(cell)
// Note: Because we are messing with the transform these values don't work
// logically so we inset the positioning to make it look visually centered
// within the layout inspector
cell.center(.vertical, in: result, withInset: -(targetHeight * 0.15))
cell.center(.horizontal, in: result, withInset: -(targetWidth * 0.35))
cell.set(.width, to: .width, of: result)
cell.set(.height, to: .height, of: result)
return result
},
action: .settingBool(key: .typingIndicatorsEnabled)
)
]

View File

@ -1,11 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
@objc(LKSeedModal)
final class SeedModal: Modal {
private let mnemonic: String = {
if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() {
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
@ -15,75 +14,104 @@ final class SeedModal: Modal {
return Mnemonic.encode(hexEncodedString: Identity.fetchUserPrivateKey()!.toHexString())
}()
// MARK: - Components
private let titleLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.text = "modal_seed_title".localized()
result.themeTextColor = .textPrimary
result.textAlignment = .center
result.lineBreakMode = .byWordWrapping
result.numberOfLines = 0
return result
}()
private let explanationLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.smallFontSize)
result.text = "modal_seed_explanation".localized()
result.themeTextColor = .textPrimary
result.textAlignment = .center
result.lineBreakMode = .byWordWrapping
result.numberOfLines = 0
return result
}()
private lazy var mnemonicLabelContainer: UIView = {
let result: UIView = UIView()
result.themeBorderColor = .textPrimary
result.layer.cornerRadius = TextField.cornerRadius
result.layer.borderWidth = 1
return result
}()
private lazy var mnemonicLabel: UILabel = {
let result: UILabel = UILabel()
result.font = Fonts.spaceMono(ofSize: Values.smallFontSize)
result.text = mnemonic
result.themeTextColor = .textPrimary
result.textAlignment = .center
result.lineBreakMode = .byWordWrapping
result.numberOfLines = 0
return result
}()
private lazy var copyButton: UIButton = {
let result: UIButton = Modal.createButton(
title: "copy".localized(),
titleColor: .textPrimary
)
result.addTarget(self, action: #selector(copySeed), for: .touchUpInside)
return result
}()
private lazy var buttonStackView: UIStackView = {
let result = UIStackView(arrangedSubviews: [ copyButton, cancelButton ])
result.axis = .horizontal
result.spacing = Values.mediumSpacing
result.distribution = .fillEqually
return result
}()
private lazy var contentStackView: UIStackView = {
let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, mnemonicLabelContainer ])
result.axis = .vertical
result.spacing = Values.smallSpacing
result.isLayoutMarginsRelativeArrangement = true
result.layoutMargins = UIEdgeInsets(
top: Values.largeSpacing,
leading: Values.largeSpacing,
bottom: Values.verySmallSpacing,
trailing: Values.largeSpacing
)
return result
}()
private lazy var mainStackView: UIStackView = {
let result = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ])
result.axis = .vertical
result.spacing = Values.largeSpacing - Values.smallFontSize / 2
return result
}()
// MARK: - Lifecycle
override func populateContentView() {
// Set up title label
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = NSLocalizedString("modal_seed_title", comment: "")
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.textAlignment = .center
contentView.addSubview(mainStackView)
// Set up mnemonic label
let mnemonicLabel = UILabel()
mnemonicLabel.textColor = Colors.text
mnemonicLabel.font = Fonts.spaceMono(ofSize: Values.smallFontSize)
mnemonicLabel.text = mnemonic
mnemonicLabel.numberOfLines = 0
mnemonicLabel.lineBreakMode = .byWordWrapping
mnemonicLabel.textAlignment = .center
mainStackView.pin(to: contentView)
// Set up mnemonic label container
let mnemonicLabelContainer = UIView()
mnemonicLabelContainer.addSubview(mnemonicLabel)
mnemonicLabel.pin(to: mnemonicLabelContainer, withInset: isIPhone6OrSmaller ? 4 : Values.smallSpacing)
mnemonicLabelContainer.layer.cornerRadius = TextField.cornerRadius
mnemonicLabelContainer.layer.borderWidth = 1
mnemonicLabelContainer.layer.borderColor = Colors.text.cgColor
// Set up explanation label
let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.text = NSLocalizedString("modal_seed_explanation", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping
explanationLabel.textAlignment = .center
// Set up copy button
let copyButton = UIButton()
copyButton.set(.height, to: Values.mediumButtonHeight)
copyButton.layer.cornerRadius = Modal.buttonCornerRadius
copyButton.backgroundColor = Colors.buttonBackground
copyButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
copyButton.setTitleColor(Colors.text, for: UIControl.State.normal)
copyButton.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
copyButton.addTarget(self, action: #selector(copySeed), for: UIControl.Event.touchUpInside)
// Set up button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, copyButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Content stack view
let contentStackView = UIStackView(arrangedSubviews: [ titleLabel, mnemonicLabelContainer, explanationLabel ])
contentStackView.axis = .vertical
contentStackView.spacing = Values.largeSpacing
// Set up stack view
let spacing = Values.largeSpacing - Values.smallFontSize / 2
let stackView = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ])
stackView.axis = .vertical
stackView.spacing = spacing
contentView.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
stackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: spacing)
// Mark seed as viewed
Storage.shared.writeAsync { db in db[.hasViewedSeed] = true }
@ -93,6 +121,7 @@ final class SeedModal: Modal {
@objc private func copySeed() {
UIPasteboard.general.string = mnemonic
dismiss(animated: true, completion: nil)
}
}

View File

@ -215,6 +215,7 @@ class SettingsTableViewController<Section: SettingSection, SettingItem: Hashable
cell.update(
title: settingInfo.title,
subtitle: settingInfo.subtitle,
subtitleExtraViewGenerator: settingInfo.subtitleExtraViewGenerator,
action: settingInfo.action,
extraActionTitle: settingInfo.extraActionTitle,
onExtraAction: settingInfo.onExtraAction,
@ -228,12 +229,23 @@ class SettingsTableViewController<Section: SettingSection, SettingItem: Hashable
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let section: SectionModel = viewModel.settingsData[section]
let view: SettingHeaderView = tableView.dequeueHeaderFooterView(type: SettingHeaderView.self)
view.update(with: section.model.title)
view.update(
with: section.model.title,
hasSeparator: (section.elements.first?.action.shouldHaveBackground != false)
)
return view
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
@ -245,9 +257,12 @@ class SettingsTableViewController<Section: SettingSection, SettingItem: Hashable
case .trigger(let action):
action()
case .rightButtonModal(_, let createModal):
let viewController: UIViewController = createModal()
present(viewController, animated: true, completion: nil)
case .rightButtonAction(_, let action):
guard let cell: SettingsCell = tableView.cellForRow(at: indexPath) as? SettingsCell else {
return
}
action(cell.rightActionButtonContainerView)
case .userDefaultsBool(let defaults, let key, let onChange):
defaults.set(!defaults.bool(forKey: key), forKey: key)
@ -319,6 +334,7 @@ class SettingsTableViewController<Section: SettingSection, SettingItem: Hashable
existingCell.update(
title: settingInfo.title,
subtitle: settingInfo.subtitle,
subtitleExtraViewGenerator: settingInfo.subtitleExtraViewGenerator,
action: settingInfo.action,
extraActionTitle: settingInfo.extraActionTitle,
onExtraAction: settingInfo.onExtraAction,
@ -410,7 +426,7 @@ class SettingHeaderView: UITableViewHeaderFooterView {
// MARK: - Content
fileprivate func update(with title: String) {
fileprivate func update(with title: String, hasSeparator: Bool) {
titleLabel.text = title
titleLabel.isHidden = title.isEmpty
stackView.layoutMargins = UIEdgeInsets(
@ -421,6 +437,8 @@ class SettingHeaderView: UITableViewHeaderFooterView {
)
emptyHeightConstraint.isActive = title.isEmpty
filledHeightConstraint.isActive = !title.isEmpty
separator.isHidden = !hasSeparator
self.layoutIfNeeded()
}
}

View File

@ -38,6 +38,7 @@ struct SettingInfo<ID: Hashable & Differentiable>: Equatable, Hashable, Differen
let title: String
let subtitle: String?
let action: SettingsAction
let subtitleExtraViewGenerator: (() -> UIView)?
let extraActionTitle: ((Theme, Theme.PrimaryColor) -> NSAttributedString)?
let onExtraAction: (() -> Void)?
@ -47,6 +48,7 @@ struct SettingInfo<ID: Hashable & Differentiable>: Equatable, Hashable, Differen
id: ID,
title: String,
subtitle: String? = nil,
subtitleExtraViewGenerator: (() -> UIView)? = nil,
action: SettingsAction,
extraActionTitle: ((Theme, Theme.PrimaryColor) -> NSAttributedString)? = nil,
onExtraAction: (() -> Void)? = nil
@ -54,6 +56,7 @@ struct SettingInfo<ID: Hashable & Differentiable>: Equatable, Hashable, Differen
self.id = id
self.title = title
self.subtitle = subtitle
self.subtitleExtraViewGenerator = subtitleExtraViewGenerator
self.action = action
self.extraActionTitle = extraActionTitle
self.onExtraAction = onExtraAction
@ -107,9 +110,9 @@ public enum SettingsAction: Hashable, Equatable {
shouldAutoSave: Bool,
selectValue: () -> Void
)
case rightButtonModal(
case rightButtonAction(
title: String,
createModal: () -> UIViewController
action: (UIView) -> ()
)
private var actionName: String {
@ -122,7 +125,14 @@ public enum SettingsAction: Hashable, Equatable {
case .push: return "push"
case .dangerPush: return "dangerPush"
case .listSelection: return "listSelection"
case .rightButtonModal: return "rightButtonModal"
case .rightButtonAction: return "rightButtonAction"
}
}
var shouldHaveBackground: Bool {
switch self {
case .dangerPush: return false
default: return true
}
}

View File

@ -254,7 +254,7 @@ final class SettingsVC: BaseVC, AvatarViewHelperDelegate {
UIView.separator(),
getSettingButton(title: "vc_settings_notifications_button_title".localized(), action: #selector(showNotificationSettings)),
UIView.separator(),
getSettingButton(title: "CONVERSATIONS_TITLE".localized(), action: #selector(showConversationSettings)),
getSettingButton(title: "CONVERSATION_SETTINGS_TITLE".localized(), action: #selector(showConversationSettings)),
UIView.separator(),
getSettingButton(title: "MESSAGE_REQUESTS_TITLE".localized(), action: #selector(showMessageRequests)),
UIView.separator(),
@ -539,8 +539,10 @@ final class SettingsVC: BaseVC, AvatarViewHelperDelegate {
}
@objc private func showPrivacySettings() {
let privacySettingsVC = PrivacySettingsTableViewController()
self.navigationController?.pushViewController(privacySettingsVC, animated: true)
let settingsViewController: SettingsTableViewController = SettingsTableViewController(
viewModel: PrivacySettingsViewModel()
)
self.navigationController?.pushViewController(settingsViewController, animated: true)
}
@objc private func showNotificationSettings() {
@ -556,8 +558,10 @@ final class SettingsVC: BaseVC, AvatarViewHelperDelegate {
}
@objc private func showConversationSettings() {
let conversationSettingsVC = ConversationSettingsViewController()
self.navigationController?.pushViewController(conversationSettingsVC, animated: true)
let settingsViewController: SettingsTableViewController = SettingsTableViewController(
viewModel: ConversationSettingsViewModel()
)
self.navigationController?.pushViewController(settingsViewController, animated: true)
}
@objc private func showAppearanceSettings() {
@ -597,26 +601,4 @@ final class SettingsVC: BaseVC, AvatarViewHelperDelegate {
}
navigationController!.present(shareVC, animated: true, completion: nil)
}
@objc private func openFAQ() {
let url = URL(string: "https://getsession.org/faq")!
UIApplication.shared.open(url)
}
@objc private func openSurvey() {
let url = URL(string: "https://getsession.org/survey")!
UIApplication.shared.open(url)
}
@objc private func shareLogs() {
let shareLogsModal = ShareLogsModal()
shareLogsModal.modalPresentationStyle = .overFullScreen
shareLogsModal.modalTransitionStyle = .crossDissolve
present(shareLogsModal, animated: true, completion: nil)
}
@objc private func helpTranslate() {
let url = URL(string: "https://crowdin.com/project/session-ios")!
UIApplication.shared.open(url)
}
}

View File

@ -1,103 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SignalUtilitiesKit
final class ShareLogsModal: Modal {
// MARK: - Lifecycle
init() {
super.init(nibName: nil, bundle: nil)
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init() instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init() instead.")
}
override func populateContentView() {
// Title
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = NSLocalizedString("modal_share_logs_title", comment: "")
titleLabel.textAlignment = .center
// Message
let messageLabel = UILabel()
messageLabel.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
messageLabel.text = NSLocalizedString("modal_share_logs_explanation", comment: "")
messageLabel.numberOfLines = 0
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.textAlignment = .center
// Open button
let shareButton = UIButton()
shareButton.set(.height, to: Values.mediumButtonHeight)
shareButton.layer.cornerRadius = Modal.buttonCornerRadius
shareButton.backgroundColor = Colors.buttonBackground
shareButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
shareButton.setTitleColor(Colors.text, for: UIControl.State.normal)
shareButton.setTitle(NSLocalizedString("share", comment: ""), for: UIControl.State.normal)
shareButton.addTarget(self, action: #selector(shareLogs), for: UIControl.Event.touchUpInside)
// Button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, shareButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Content stack view
let contentStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel ])
contentStackView.axis = .vertical
contentStackView.spacing = Values.largeSpacing
// Main stack view
let spacing = Values.largeSpacing - Values.smallFontSize / 2
let mainStackView = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ])
mainStackView.axis = .vertical
mainStackView.spacing = spacing
contentView.addSubview(mainStackView)
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: spacing)
}
// MARK: - Interaction
@objc private func shareLogs() {
ShareLogsModal.shareLogs(from: self)
}
public static func shareLogs(from viewController: UIViewController, onShareComplete: (() -> ())? = nil) {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
OWSLogger.info("[Version] iOS \(UIDevice.current.systemVersion) \(version)")
DDLog.flushLog()
let logFilePaths = AppEnvironment.shared.fileLogger.logFileManager.sortedLogFilePaths
if let latestLogFilePath = logFilePaths.first {
let latestLogFileURL = URL(fileURLWithPath: latestLogFilePath)
viewController.dismiss(animated: true, completion: {
if let vc = CurrentAppContext().frontmostViewController() {
let shareVC = UIActivityViewController(activityItems: [ latestLogFileURL ], applicationActivities: nil)
shareVC.completionWithItemsHandler = { _, _, _, _ in onShareComplete?() }
if UIDevice.current.isIPad {
shareVC.excludedActivityTypes = []
shareVC.popoverPresentationController?.permittedArrowDirections = []
shareVC.popoverPresentationController?.sourceView = vc.view
shareVC.popoverPresentationController?.sourceRect = vc.view.bounds
}
vc.present(shareVC, animated: true, completion: nil)
}
})
}
}
}

View File

@ -0,0 +1,90 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionMessagingKit
import SignalUtilitiesKit
class BlockedContactCell: UITableViewCell {
// MARK: - Components
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView()
private let selectionView: RadioButton = {
let result: RadioButton = RadioButton(size: .medium)
result.translatesAutoresizingMaskIntoConstraints = false
result.isUserInteractionEnabled = false
result.font = .systemFont(ofSize: Values.mediumFontSize, weight: .bold)
return result
}()
// MARK: - Initializtion
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpViewHierarchy()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setUpViewHierarchy()
}
// MARK: - Layout
private func setUpViewHierarchy() {
// Background color
themeBackgroundColor = .conversationButton_background
// Highlight color
let selectedBackgroundView = UIView()
selectedBackgroundView.themeBackgroundColor = .conversationButton_highlight
self.selectedBackgroundView = selectedBackgroundView
// Add the UI
contentView.addSubview(profilePictureView)
contentView.addSubview(selectionView)
setupLayout()
}
private func setupLayout() {
// Profile picture view
profilePictureView.center(.vertical, in: contentView)
profilePictureView.topAnchor
.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: Values.mediumSpacing)
.isActive = true
profilePictureView.bottomAnchor
.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -Values.mediumSpacing)
.isActive = true
profilePictureView.pin(.left, to: .left, of: contentView, withInset: Values.veryLargeSpacing)
profilePictureView.set(.width, to: Values.mediumProfilePictureSize)
profilePictureView.set(.height, to: Values.mediumProfilePictureSize)
profilePictureView.size = Values.mediumProfilePictureSize
selectionView.center(.vertical, in: contentView)
selectionView.topAnchor
.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: Values.mediumSpacing)
.isActive = true
selectionView.bottomAnchor
.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -Values.mediumSpacing)
.isActive = true
selectionView.pin(.left, to: .right, of: profilePictureView, withInset: Values.mediumSpacing)
selectionView.pin(.right, to: .right, of: contentView, withInset: -Values.veryLargeSpacing)
}
// MARK: - Content
public func update(with cellViewModel: BlockedContactsViewModel.DataModel, isSelected: Bool) {
profilePictureView.update(
publicKey: cellViewModel.profile.id,
profile: cellViewModel.profile,
threadVariant: .contact
)
selectionView.text = cellViewModel.profile.displayName()
selectionView.update(isSelected: isSelected)
}
}

View File

@ -7,6 +7,7 @@ import SessionUIKit
class SettingsCell: UITableViewCell {
/// This value is here to allow the theming update callback to be released when preparing for reuse
private var instanceView: UIView = UIView()
private var subtitleExtraView: UIView?
private var onExtraAction: (() -> Void)?
// MARK: - UI
@ -140,7 +141,7 @@ class SettingsCell: UITableViewCell {
return result
}()
private lazy var rightActionButtonContainerView: UIView = {
public lazy var rightActionButtonContainerView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.themeBackgroundColor = .solidButton_background
@ -182,8 +183,6 @@ class SettingsCell: UITableViewCell {
}
private func setupViewHierarchy() {
themeBackgroundColor = .settings_tabBackground
// Highlight color
let selectedBackgroundView = UIView()
selectedBackgroundView.themeBackgroundColor = .settings_tabHighlight
@ -259,6 +258,8 @@ class SettingsCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
self.themeBackgroundColor = nil
self.selectedBackgroundView = nil
self.instanceView = UIView()
self.onExtraAction = nil
@ -279,11 +280,15 @@ class SettingsCell: UITableViewCell {
tickImageView.alpha = 1
rightActionButtonContainerView.isHidden = true
botSeparator.isHidden = true
subtitleExtraView?.removeFromSuperview()
subtitleExtraView = nil
}
public func update(
title: String,
subtitle: String?,
subtitleExtraViewGenerator: (() -> UIView)?,
action: SettingsAction,
extraActionTitle: ((Theme, Theme.PrimaryColor) -> NSAttributedString)?,
onExtraAction: (() -> Void)?,
@ -291,6 +296,7 @@ class SettingsCell: UITableViewCell {
isLastInSection: Bool
) {
self.instanceView = UIView()
self.subtitleExtraView = subtitleExtraViewGenerator?()
self.onExtraAction = onExtraAction
// Left content
@ -299,15 +305,66 @@ class SettingsCell: UITableViewCell {
subtitleLabel.isHidden = (subtitle == nil)
extraActionButton.isHidden = (extraActionTitle == nil)
// Separator Visibility
switch action {
case .dangerPush:
topSeparator.isHidden = true
botSeparator.isHidden = true
default:
topSeparator.isHidden = isFirstInSection
botSeparator.isHidden = !isLastInSection
// Position the 'subtitleExtraView' at the end of the last line of text
if
let subtitleExtraView: UIView = self.subtitleExtraView,
let subtitle: String = subtitle,
let font: UIFont = subtitleLabel.font
{
self.layoutIfNeeded()
let layoutManager: NSLayoutManager = NSLayoutManager()
let textStorage = NSTextStorage(
attributedString: NSAttributedString(
string: subtitle,
attributes: [ .font: font ]
)
)
textStorage.addLayoutManager(layoutManager)
let textContainer: NSTextContainer = NSTextContainer(
size: CGSize(
width: subtitleLabel.bounds.size.width,
height: 999
)
)
textContainer.lineFragmentPadding = 0
layoutManager.addTextContainer(textContainer)
var glyphRange: NSRange = NSRange()
layoutManager.characterRange(
forGlyphRange: NSRange(location: subtitle.glyphCount - 1, length: 1),
actualGlyphRange: &glyphRange
)
let lastGlyphRect: CGRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
contentView.addSubview(subtitleExtraView)
subtitleExtraView.pin(
.top,
to: .top,
of: subtitleLabel,
withInset: (lastGlyphRect.minY + ((lastGlyphRect.height / 2) - (subtitleExtraView.bounds.height / 2)))
)
subtitleExtraView.pin(
.left,
to: .left,
of: subtitleLabel,
withInset: lastGlyphRect.minX + 5
)
}
// Separator/background Visibility
if action.shouldHaveBackground {
self.themeBackgroundColor = .settings_tabBackground
topSeparator.isHidden = isFirstInSection
botSeparator.isHidden = !isLastInSection
}
else {
self.themeBackgroundColor = nil
topSeparator.isHidden = true
botSeparator.isHidden = true
}
// Action Behaviours
@ -351,7 +408,7 @@ class SettingsCell: UITableViewCell {
titleLabel.themeTextColor = .danger
actionContainerView.isHidden = false
case .rightButtonModal(let title, _):
case .rightButtonAction(let title, _):
actionContainerView.isHidden = false
rightActionButtonContainerView.isHidden = false
rightActionButtonLabel.text = title

View File

@ -50,31 +50,11 @@ class ThemeSelectionView: UIView {
return result
}()
private let titleLabel: UILabel = {
let result: UILabel = UILabel()
private let selectionView: RadioButton = {
let result: RadioButton = RadioButton(size: .medium)
result.translatesAutoresizingMaskIntoConstraints = false
result.isUserInteractionEnabled = false
result.font = UIFont.systemFont(ofSize: Values.mediumFontSize, weight: .bold)
result.themeTextColor = .textPrimary
return result
}()
private let selectionBorderView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.isUserInteractionEnabled = false
result.layer.borderWidth = 1
result.layer.cornerRadius = (ThemeSelectionView.selectionBorderSize / 2)
return result
}()
private let selectionView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.isUserInteractionEnabled = false
result.layer.cornerRadius = (ThemeSelectionView.selectionSize / 2)
result.font = .systemFont(ofSize: Values.mediumFontSize, weight: .bold)
return result
}()
@ -104,13 +84,11 @@ class ThemeSelectionView: UIView {
previewView.layer.borderColor = theme.colors[.borderSeparator]?.cgColor
previewIncomingMessageView.backgroundColor = theme.colors[.messageBubble_incomingBackground]
previewOutgoingMessageView.backgroundColor = theme.colors[.defaultPrimary]
titleLabel.text = theme.title
selectionView.text = theme.title
// Add the UI
addSubview(backgroundButton)
addSubview(previewView)
addSubview(titleLabel)
addSubview(selectionBorderView)
addSubview(selectionView)
previewView.addSubview(previewIncomingMessageView)
@ -142,30 +120,15 @@ class ThemeSelectionView: UIView {
previewOutgoingMessageView.set(.width, to: 40)
previewOutgoingMessageView.set(.height, to: 12)
titleLabel.center(.vertical, in: self)
titleLabel.pin(.left, to: .right, of: previewView, withInset: Values.mediumSpacing)
selectionBorderView.center(.vertical, in: self)
selectionBorderView.pin(.right, to: .right, of: self, withInset: -Values.veryLargeSpacing)
selectionBorderView.set(.width, to: ThemeSelectionView.selectionBorderSize)
selectionBorderView.set(.height, to: ThemeSelectionView.selectionBorderSize)
selectionView.center(in: selectionBorderView)
selectionView.set(.width, to: ThemeSelectionView.selectionSize)
selectionView.set(.height, to: ThemeSelectionView.selectionSize)
selectionView.center(.vertical, in: self)
selectionView.pin(.left, to: .right, of: previewView, withInset: Values.mediumSpacing)
selectionView.pin(.right, to: .right, of: self, withInset: -Values.veryLargeSpacing)
}
// MARK: - Content
func update(isSelected: Bool) {
selectionBorderView.themeBorderColor = (isSelected ?
.radioButton_selectedBorder :
.radioButton_unselectedBorder
)
selectionView.themeBackgroundColor = (isSelected ?
.radioButton_selectedBackground :
.radioButton_unselectedBackground
)
selectionView.update(isSelected: isSelected)
}
@objc func itemSelected() {

View File

@ -414,6 +414,26 @@ public final class FullConversationCell: UITableViewCell {
)
}
public func optimisticUpdate(
isBlocked: Bool? = nil,
isPinned: 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 isPinned: Bool = isPinned {
isPinnedIcon.isHidden = !isPinned
}
}
// MARK: - Snippet generation
private func getSnippet(

View File

@ -112,7 +112,15 @@ public class Modal: BaseVC, UIGestureRecognizerDelegate {
// MARK: - Interaction
@objc func close() {
dismiss(animated: true, completion: nil)
// Recursively dismiss all modals (ie. find the first modal presented by a non-modal
// and get that to dismiss it's presented view controller)
var targetViewController: UIViewController? = self
while targetViewController?.presentingViewController is Modal {
targetViewController = targetViewController?.presentingViewController
}
targetViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
}
// MARK: - UIGestureRecognizerDelegate

View File

@ -602,10 +602,26 @@ extension MessageViewModel {
public extension MessageViewModel {
static func filterSQL(threadId: String) -> SQL {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let setting: TypedTableAlias<Setting> = TypedTableAlias()
return SQL("\(interaction[.threadId]) = \(threadId)")
var targetValue: Bool = true
let boolSettingLiteral: Data = Data(bytes: &targetValue, count: MemoryLayout.size(ofValue: targetValue))
return SQL("""
\(interaction[.threadId]) = \(threadId) AND (
\(SQL("\(interaction[.variant]) != \(Interaction.Variant.infoScreenshotNotification)")) OR
\(SQL("IFNULL(\(setting[.value]), false) == \(boolSettingLiteral)"))
)
""")
}
static let optimisedJoinSQL: SQL = {
let setting: TypedTableAlias<Setting> = TypedTableAlias()
let targetSetting: String = Setting.BoolKey.showScreenshotNotifications.rawValue
return SQL("LEFT JOIN \(Setting.self) ON \(setting[.key]) = \(targetSetting)")
}()
static let groupSQL: SQL = {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()

View File

@ -425,6 +425,11 @@ public extension SessionThreadViewModel {
let attachment: TypedTableAlias<Attachment> = TypedTableAlias()
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
let profile: TypedTableAlias<Profile> = TypedTableAlias()
let setting: TypedTableAlias<Setting> = TypedTableAlias()
let targetSetting: String = Setting.BoolKey.showScreenshotNotifications.rawValue
var targetValue: Bool = true
let boolSettingLiteral: Data = Data(bytes: &targetValue, count: MemoryLayout.size(ofValue: targetValue))
let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name)
@ -512,7 +517,12 @@ public extension SessionThreadViewModel {
SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey)
FROM \(Interaction.self)
WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
LEFT JOIN \(Setting.self) ON \(setting[.key]) = \(targetSetting)
WHERE
\(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) AND (
\(SQL("\(interaction[.variant]) != \(Interaction.Variant.infoScreenshotNotification)")) OR
\(SQL("IFNULL(\(setting[.value]), false) == \(boolSettingLiteral)"))
)
GROUP BY \(interaction[.threadId])
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
@ -611,6 +621,11 @@ public extension SessionThreadViewModel {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let setting: TypedTableAlias<Setting> = TypedTableAlias()
let targetSetting: String = Setting.BoolKey.showScreenshotNotifications.rawValue
var targetValue: Bool = true
let boolSettingLiteral: Data = Data(bytes: &targetValue, count: MemoryLayout.size(ofValue: targetValue))
let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
@ -621,7 +636,12 @@ public extension SessionThreadViewModel {
\(interaction[.threadId]),
MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral)
FROM \(Interaction.self)
WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
LEFT JOIN \(Setting.self) ON \(setting[.key]) = \(targetSetting)
WHERE
\(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) AND (
\(SQL("\(interaction[.variant]) != \(Interaction.Variant.infoScreenshotNotification)")) OR
\(SQL("IFNULL(\(setting[.value]), false) == \(boolSettingLiteral)"))
)
GROUP BY \(interaction[.threadId])
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
"""

View File

@ -66,6 +66,10 @@ public extension Setting.BoolKey {
/// Controls whether the device should show screenshot notifications in one-to-one conversations (will always
/// send screenshot notifications, this just controls whether they get filtered out or not)
static let showScreenshotNotifications: Setting.BoolKey = "showScreenshotNotifications"
/// Controls whether concurrent audio messages should automatically be played after the one the user starts
/// playing finishes
static let shouldAutoPlayConsecutiveAudioMessages: Setting.BoolKey = "shouldAutoPlayConsecutiveAudioMessages"
}
public extension Setting.StringKey {

View File

@ -17,6 +17,14 @@ public final class OutlineButton: UIButton {
case large
}
public override var isEnabled: Bool {
didSet {
self.alpha = (isEnabled ? 1 : 0.5)
}
}
// MARK: - Initialization
public init(style: Style, size: Size) {
super.init(frame: .zero)

View File

@ -123,7 +123,7 @@ public extension UIProgressView {
}
}
public extension UITableViewRowAction {
public extension UIContextualAction {
var themeBackgroundColor: ThemeValue? {
set { ThemeManager.set(self, keyPath: \.backgroundColor, to: newValue) }
get { return nil }

View File

@ -141,7 +141,6 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
// The 'event' object only exists during this method so we need to copy the info
// from it, otherwise it will cease to exist after this metod call finishes
changesInCommit.mutate { $0.insert(PagedData.TrackedChange(event: event)) }
changesInCommit.mutate { $0.insert(trackedChange) }
}
@ -288,9 +287,14 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
}
// Fetch the indexes of the rowIds so we can determine whether they should be added to the screen
let directRowIds: Set<Int64> = changesToQuery.map { $0.rowId }.asSet()
let pagedRowIdsForRelatedDeletions: Set<Int64> = relatedDeletions
.compactMap { $0.pagedRowIdsForRelatedDeletion }
.flatMap { $0 }
.asSet()
let itemIndexes: [PagedData.RowIndexInfo] = PagedData.indexes(
db,
rowIds: changesToQuery.map { $0.rowId },
rowIds: Array(directRowIds),
tableName: pagedTableName,
requiredJoinSQL: joinSQL,
orderSQL: orderSQL,
@ -306,9 +310,7 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
)
let relatedDeletionIndexes: [PagedData.RowIndexInfo] = PagedData.indexes(
db,
rowIds: relatedDeletions
.compactMap { $0.pagedRowIdsForRelatedDeletion }
.flatMap { $0 },
rowIds: Array(pagedRowIdsForRelatedDeletions),
tableName: pagedTableName,
requiredJoinSQL: joinSQL,
orderSQL: orderSQL,
@ -353,6 +355,37 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
let validRelatedDeletionRowIds: [Int64] = determineValidChanges(for: relatedDeletionIndexes)
let countBefore: Int = itemIndexes.filter { $0.rowIndex < updatedPageInfo.pageOffset }.count
// If the number of indexes doesn't match the number of rowIds then it means something changed
// resulting in an item being filtered out
func performRemovalsIfNeeded(for rowIds: Set<Int64>, indexes: [PagedData.RowIndexInfo]) {
let uniqueIndexes: Set<Int64> = indexes.map { $0.rowId }.asSet()
// If they have the same count then nothin was filtered out so do nothing
guard rowIds.count != uniqueIndexes.count else { return }
// Otherwise something was probably removed so try to remove it from the cache
let rowIdsRemoved: Set<Int64> = rowIds.subtracting(uniqueIndexes)
let preDeletionCount: Int = updatedDataCache.count
updatedDataCache = updatedDataCache.deleting(rowIds: Array(rowIdsRemoved))
// Lastly make sure there were actually changes before updating the page info
guard updatedDataCache.count != preDeletionCount else { return }
let dataSizeDiff: Int = (updatedDataCache.count - preDeletionCount)
updatedPageInfo = PagedData.PageInfo(
pageSize: updatedPageInfo.pageSize,
pageOffset: updatedPageInfo.pageOffset,
currentCount: (updatedPageInfo.currentCount + dataSizeDiff),
totalCount: (updatedPageInfo.totalCount + dataSizeDiff)
)
}
// Actually perform any required removals
performRemovalsIfNeeded(for: directRowIds, indexes: itemIndexes)
performRemovalsIfNeeded(for: pagedRowIdsForRelatedChanges, indexes: relatedChangeIndexes)
performRemovalsIfNeeded(for: pagedRowIdsForRelatedDeletions, indexes: relatedDeletionIndexes)
// Update the offset and totalCount even if the rows are outside of the current page (need to
// in order to ensure the 'load more' sections are accurate)
updatedPageInfo = PagedData.PageInfo(
@ -374,7 +407,7 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
updateDataAndCallbackIfNeeded(updatedDataCache, updatedPageInfo, true)
return
}
// Fetch the inserted/updated rows
let targetRowIds: [Int64] = Array((validChangeRowIds + validRelatedChangeRowIds + validRelatedDeletionRowIds).asSet())
let updatedItems: [T] = (try? dataQuery(targetRowIds)