mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
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:
parent
a1e88329db
commit
c82ee0c44b
58 changed files with 1783 additions and 566 deletions
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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 n’ont 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 n’ont 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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
382
Session/Settings/BlockedContactsViewController.swift
Normal file
382
Session/Settings/BlockedContactsViewController.swift
Normal 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)
|
||||
}
|
||||
}
|
214
Session/Settings/BlockedContactsViewModel.swift
Normal file
214
Session/Settings/BlockedContactsViewModel.swift
Normal 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")
|
||||
}()
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
91
Session/Settings/ConversationSettingsViewModel.swift
Normal file
91
Session/Settings/ConversationSettingsViewModel.swift
Normal 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() {}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import SessionMessagingKit
|
|||
import SignalUtilitiesKit
|
||||
|
||||
final class NukeDataModal: Modal {
|
||||
|
||||
// MARK: - Components
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
90
Session/Settings/Views/BlockedContactCell.swift
Normal file
90
Session/Settings/Views/BlockedContactCell.swift
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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])
|
||||
"""
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue