Merge pull request #887 from oxen-io/dev

Release 2.4.0
This commit is contained in:
Morgan Pretty 2023-09-05 16:15:08 +10:00 committed by GitHub
commit 862a6a8898
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
328 changed files with 15629 additions and 12049 deletions

View file

@ -13,7 +13,9 @@ local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https:/
// 'LANG' env var so we need to work around the with https://github.com/CocoaPods/CocoaPods/issues/6333 // 'LANG' env var so we need to work around the with https://github.com/CocoaPods/CocoaPods/issues/6333
local install_cocoapods = { local install_cocoapods = {
name: 'Install CocoaPods', name: 'Install CocoaPods',
commands: ['LANG=en_US.UTF-8 pod install'] commands: ['
LANG=en_US.UTF-8 pod install || rm -rf ./Pods && LANG=en_US.UTF-8 pod install
']
}; };
// Load from the cached CocoaPods directory (to speed up the build) // Load from the cached CocoaPods directory (to speed up the build)
@ -21,8 +23,14 @@ local load_cocoapods_cache = {
name: 'Load CocoaPods Cache', name: 'Load CocoaPods Cache',
commands: [ commands: [
||| |||
LOOP_BREAK=0
while test -e /Users/drone/.cocoapods_cache.lock; do while test -e /Users/drone/.cocoapods_cache.lock; do
sleep 1 sleep 1
LOOP_BREAK=$((LOOP_BREAK + 1))
if [[ $LOOP_BREAK -ge 600 ]]; then
rm -f /Users/drone/.cocoapods_cache.lock
fi
done done
|||, |||,
'touch /Users/drone/.cocoapods_cache.lock', 'touch /Users/drone/.cocoapods_cache.lock',
@ -31,7 +39,7 @@ local load_cocoapods_cache = {
cp -r /Users/drone/.cocoapods_cache ./Pods cp -r /Users/drone/.cocoapods_cache ./Pods
fi fi
|||, |||,
'rm /Users/drone/.cocoapods_cache.lock' 'rm -f /Users/drone/.cocoapods_cache.lock'
] ]
}; };
@ -40,8 +48,14 @@ local update_cocoapods_cache = {
name: 'Update CocoaPods Cache', name: 'Update CocoaPods Cache',
commands: [ commands: [
||| |||
LOOP_BREAK=0
while test -e /Users/drone/.cocoapods_cache.lock; do while test -e /Users/drone/.cocoapods_cache.lock; do
sleep 1 sleep 1
LOOP_BREAK=$((LOOP_BREAK + 1))
if [[ $LOOP_BREAK -ge 600 ]]; then
rm -f /Users/drone/.cocoapods_cache.lock
fi
done done
|||, |||,
'touch /Users/drone/.cocoapods_cache.lock', 'touch /Users/drone/.cocoapods_cache.lock',
@ -51,7 +65,7 @@ local update_cocoapods_cache = {
cp -r ./Pods /Users/drone/.cocoapods_cache cp -r ./Pods /Users/drone/.cocoapods_cache
fi fi
|||, |||,
'rm /Users/drone/.cocoapods_cache.lock' 'rm -f /Users/drone/.cocoapods_cache.lock'
] ]
}; };
@ -71,7 +85,7 @@ local update_cocoapods_cache = {
name: 'Run Unit Tests', name: 'Run Unit Tests',
commands: [ commands: [
'mkdir build', 'mkdir build',
'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -workspace Session.xcworkspace -scheme Session -destination "platform=iOS Simulator,name=iPhone 14" -destination "platform=iOS Simulator,name=iPhone 14 Pro Max" -parallel-testing-enabled YES -test-timeouts-enabled YES -maximum-test-execution-time-allowance 2 -collect-test-diagnostics never 2>&1 | ./Pods/xcbeautify/xcbeautify --is-ci --report junit --report-path ./build/reports --junit-report-filename junit2.xml' 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -destination "platform=iOS Simulator,name=iPhone 14" -destination "platform=iOS Simulator,name=iPhone 14 Pro Max" -parallel-testing-enabled YES -test-timeouts-enabled YES -maximum-test-execution-time-allowance 2 -collect-test-diagnostics never 2>&1 | ./Pods/xcbeautify/xcbeautify --is-ci --report junit --report-path ./build/reports --junit-report-filename junit2.xml'
], ],
}, },
update_cocoapods_cache update_cocoapods_cache
@ -83,6 +97,7 @@ local update_cocoapods_cache = {
type: 'exec', type: 'exec',
name: 'Simulator Build', name: 'Simulator Build',
platform: { os: 'darwin', arch: 'amd64' }, platform: { os: 'darwin', arch: 'amd64' },
trigger: { event: { exclude: [ 'pull_request' ] } },
steps: [ steps: [
clone_submodules, clone_submodules,
load_cocoapods_cache, load_cocoapods_cache,
@ -91,7 +106,7 @@ local update_cocoapods_cache = {
name: 'Build', name: 'Build',
commands: [ commands: [
'mkdir build', 'mkdir build',
'xcodebuild archive -workspace Session.xcworkspace -scheme Session -configuration "App Store Release" -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator" | ./Pods/xcbeautify/xcbeautify --is-ci' 'xcodebuild archive -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -configuration "App Store Release" -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator" | ./Pods/xcbeautify/xcbeautify --is-ci'
], ],
}, },
update_cocoapods_cache, update_cocoapods_cache,
@ -110,6 +125,7 @@ local update_cocoapods_cache = {
type: 'exec', type: 'exec',
name: 'AppStore Build', name: 'AppStore Build',
platform: { os: 'darwin', arch: 'amd64' }, platform: { os: 'darwin', arch: 'amd64' },
trigger: { event: { exclude: [ 'pull_request' ] } },
steps: [ steps: [
clone_submodules, clone_submodules,
load_cocoapods_cache, load_cocoapods_cache,
@ -118,7 +134,7 @@ local update_cocoapods_cache = {
name: 'Build', name: 'Build',
commands: [ commands: [
'mkdir build', 'mkdir build',
'xcodebuild archive -workspace Session.xcworkspace -scheme Session -configuration "App Store Release" -sdk iphoneos -archivePath ./build/Session.xcarchive -destination "generic/platform=iOS" -allowProvisioningUpdates' 'xcodebuild archive -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -configuration "App Store Release" -sdk iphoneos -archivePath ./build/Session.xcarchive -destination "generic/platform=iOS" -allowProvisioningUpdates CODE_SIGNING_ALLOWED=NO | ./Pods/xcbeautify/xcbeautify --is-ci'
], ],
}, },
update_cocoapods_cache, update_cocoapods_cache,

@ -1 +1 @@
Subproject commit d8f07fa92c12c5c2409774e03e03395d7847d1c2 Subproject commit e3ccf29db08aaf0b9bb6bbe72ae5967cd183a78d

View file

@ -3,8 +3,6 @@
# Script used with Drone CI to upload build artifacts (because specifying all this in # Script used with Drone CI to upload build artifacts (because specifying all this in
# .drone.jsonnet is too painful). # .drone.jsonnet is too painful).
set -o errexit set -o errexit
if [ -z "$SSH_KEY" ]; then if [ -z "$SSH_KEY" ]; then
@ -19,33 +17,36 @@ set -o xtrace # Don't start tracing until *after* we write the ssh key
chmod 600 ssh_key chmod 600 ssh_key
if [ -n "$DRONE_TAG" ]; then # Define the output paths
# For a tag build use something like `session-ios-v1.2.3`
base="session-ios-$DRONE_TAG"
else
# Otherwise build a length name from the datetime and commit hash, such as:
# session-ios-20200522T212342Z-04d7dcc54
base="session-ios-$(date --date=@$DRONE_BUILD_CREATED +%Y%m%dT%H%M%SZ)-${DRONE_COMMIT:0:9}"
fi
mkdir -v "$base"
# Copy over the build products
prod_path="build/Session.xcarchive" prod_path="build/Session.xcarchive"
sim_path="build/Session_sim.xcarchive/Products/Applications/Session.app" sim_path="build/Session_sim.xcarchive/Products/Applications/Session.app"
mkdir build # Validate the paths exist
echo "Test" > "build/test.txt" if [ -d $prod_path ]; then
suffix="store"
if [ ! -d $prod_path ]; then target_path=$prod_path
cp -av $prod_path "$base" elif [ -d $sim_path ]; then
else if [ ! -d $sim_path ]; then suffix="sim"
cp -av $sim_path "$base" target_path=$sim_path
else else
echo "Expected a file to upload, found none" >&2 echo -e "\n\n\n\e[31;1mExpected a file to upload, found none\e[0m" >&2
exit 1 exit 1
fi fi
if [ -n "$DRONE_TAG" ]; then
# For a tag build use something like `session-ios-v1.2.3`
base="session-ios-$DRONE_TAG-$suffix"
else
# Otherwise build a length name from the datetime and commit hash, such as:
# session-ios-20200522T212342Z-04d7dcc54
base="session-ios-$(date --date=@$DRONE_BUILD_CREATED +%Y%m%dT%H%M%SZ)-${DRONE_COMMIT:0:9}-$suffix"
fi
# Copy over the build products
mkdir -vp "$base"
mkdir -p build
cp -av $target_path "$base"
# tar dat shiz up yo # tar dat shiz up yo
archive="$base.tar.xz" archive="$base.tar.xz"
tar cJvf "$archive" "$base" tar cJvf "$archive" "$base"

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,8 @@ import WebRTC
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit
import SessionSnodeKit
public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
@objc static let isEnabled = true @objc static let isEnabled = true

View file

@ -2,6 +2,7 @@
import UIKit import UIKit
import GRDB import GRDB
import SessionUtilitiesKit
extension SessionCallManager { extension SessionCallManager {
@discardableResult @discardableResult

View file

@ -6,6 +6,7 @@ import GRDB
import SessionMessagingKit import SessionMessagingKit
import SignalCoreKit import SignalCoreKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
public final class SessionCallManager: NSObject, CallManagerProtocol { public final class SessionCallManager: NSObject, CallManagerProtocol {
let provider: CXProvider? let provider: CXProvider?

View file

@ -4,6 +4,7 @@ import UIKit
import SessionUIKit import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
private static let swipeToOperateThreshold: CGFloat = 60 private static let swipeToOperateThreshold: CGFloat = 60

View file

@ -3,6 +3,7 @@
import UIKit import UIKit
import WebRTC import WebRTC
import SessionUIKit import SessionUIKit
import SessionUtilitiesKit
final class MiniCallView: UIView, RTCVideoViewDelegate { final class MiniCallView: UIView, RTCVideoViewDelegate {
var callVC: CallVC var callVC: CallVC

View file

@ -7,6 +7,7 @@ import DifferenceKit
import SessionUIKit import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate { final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate {
private struct GroupMemberDisplayInfo: FetchableRecord, Equatable, Hashable, Decodable, Differentiable { private struct GroupMemberDisplayInfo: FetchableRecord, Equatable, Hashable, Decodable, Differentiable {

View file

@ -2,6 +2,7 @@
import UIKit import UIKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit
extension ContextMenuVC { extension ContextMenuVC {
struct Action { struct Action {
@ -35,15 +36,15 @@ extension ContextMenuVC {
// MARK: - Actions // MARK: - Actions
static func info(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func info(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action( return Action(
icon: UIImage(named: "ic_info"), icon: UIImage(named: "ic_info"),
title: "context_menu_info".localized(), title: "context_menu_info".localized(),
accessibilityLabel: "Message info" accessibilityLabel: "Message info"
) { delegate?.info(cellViewModel) } ) { delegate?.info(cellViewModel, using: dependencies) }
} }
static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action( return Action(
icon: UIImage(systemName: "arrow.triangle.2.circlepath"), icon: UIImage(systemName: "arrow.triangle.2.circlepath"),
title: (cellViewModel.state == .failedToSync ? title: (cellViewModel.state == .failedToSync ?
@ -51,23 +52,23 @@ extension ContextMenuVC {
"context_menu_resend".localized() "context_menu_resend".localized()
), ),
accessibilityLabel: (cellViewModel.state == .failedToSync ? "Resync message" : "Resend message") accessibilityLabel: (cellViewModel.state == .failedToSync ? "Resync message" : "Resend message")
) { delegate?.retry(cellViewModel) } ) { delegate?.retry(cellViewModel, using: dependencies) }
} }
static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action( return Action(
icon: UIImage(named: "ic_reply"), icon: UIImage(named: "ic_reply"),
title: "context_menu_reply".localized(), title: "context_menu_reply".localized(),
accessibilityLabel: "Reply to message" accessibilityLabel: "Reply to message"
) { delegate?.reply(cellViewModel) } ) { delegate?.reply(cellViewModel, using: dependencies) }
} }
static func copy(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func copy(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action( return Action(
icon: UIImage(named: "ic_copy"), icon: UIImage(named: "ic_copy"),
title: "copy".localized(), title: "copy".localized(),
accessibilityLabel: "Copy text" accessibilityLabel: "Copy text"
) { delegate?.copy(cellViewModel) } ) { delegate?.copy(cellViewModel, using: dependencies) }
} }
static func copySessionID(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func copySessionID(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
@ -79,50 +80,50 @@ extension ContextMenuVC {
) { delegate?.copySessionID(cellViewModel) } ) { delegate?.copySessionID(cellViewModel) }
} }
static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action( return Action(
icon: UIImage(named: "ic_trash"), icon: UIImage(named: "ic_trash"),
title: "TXT_DELETE_TITLE".localized(), title: "TXT_DELETE_TITLE".localized(),
accessibilityLabel: "Delete message" accessibilityLabel: "Delete message"
) { delegate?.delete(cellViewModel) } ) { delegate?.delete(cellViewModel, using: dependencies) }
} }
static func save(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func save(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action( return Action(
icon: UIImage(named: "ic_download"), icon: UIImage(named: "ic_download"),
title: "context_menu_save".localized(), title: "context_menu_save".localized(),
accessibilityLabel: "Save attachment" accessibilityLabel: "Save attachment"
) { delegate?.save(cellViewModel) } ) { delegate?.save(cellViewModel, using: dependencies) }
} }
static func ban(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func ban(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action( return Action(
icon: UIImage(named: "ic_block"), icon: UIImage(named: "ic_block"),
title: "context_menu_ban_user".localized(), title: "context_menu_ban_user".localized(),
accessibilityLabel: "Ban user" accessibilityLabel: "Ban user"
) { delegate?.ban(cellViewModel) } ) { delegate?.ban(cellViewModel, using: dependencies) }
} }
static func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action( return Action(
icon: UIImage(named: "ic_block"), icon: UIImage(named: "ic_block"),
title: "context_menu_ban_and_delete_all".localized(), title: "context_menu_ban_and_delete_all".localized(),
accessibilityLabel: "Ban user and delete" accessibilityLabel: "Ban user and delete"
) { delegate?.banAndDeleteAllMessages(cellViewModel) } ) { delegate?.banAndDeleteAllMessages(cellViewModel, using: dependencies) }
} }
static func react(_ cellViewModel: MessageViewModel, _ emoji: EmojiWithSkinTones, _ delegate: ContextMenuActionDelegate?) -> Action { static func react(_ cellViewModel: MessageViewModel, _ emoji: EmojiWithSkinTones, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action( return Action(
title: emoji.rawValue, title: emoji.rawValue,
isEmojiAction: true isEmojiAction: true
) { delegate?.react(cellViewModel, with: emoji) } ) { delegate?.react(cellViewModel, with: emoji, using: dependencies) }
} }
static func emojiPlusButton(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { static func emojiPlusButton(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action( return Action(
isEmojiPlus: true, isEmojiPlus: true,
accessibilityLabel: "Add emoji" accessibilityLabel: "Add emoji"
) { delegate?.showFullEmojiKeyboard(cellViewModel) } ) { delegate?.showFullEmojiKeyboard(cellViewModel, using: dependencies) }
} }
static func dismiss(_ delegate: ContextMenuActionDelegate?) -> Action { static func dismiss(_ delegate: ContextMenuActionDelegate?) -> Action {
@ -150,7 +151,8 @@ extension ContextMenuVC {
currentUserBlinded25PublicKey: String?, currentUserBlinded25PublicKey: String?,
currentUserIsOpenGroupModerator: Bool, currentUserIsOpenGroupModerator: Bool,
currentThreadIsMessageRequest: Bool, currentThreadIsMessageRequest: Bool,
delegate: ContextMenuActionDelegate? delegate: ContextMenuActionDelegate?,
using dependencies: Dependencies = Dependencies()
) -> [Action]? { ) -> [Action]? {
switch cellViewModel.variant { switch cellViewModel.variant {
case .standardIncomingDeleted, .infoCall, case .standardIncomingDeleted, .infoCall,
@ -159,7 +161,7 @@ extension ContextMenuVC {
.infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving, .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving,
.infoMessageRequestAccepted, .infoDisappearingMessagesUpdate: .infoMessageRequestAccepted, .infoDisappearingMessagesUpdate:
// Let the user delete info messages and unsent messages // Let the user delete info messages and unsent messages
return [ Action.delete(cellViewModel, delegate) ] return [ Action.delete(cellViewModel, delegate, using: dependencies) ]
case .standardOutgoing, .standardIncoming: break case .standardOutgoing, .standardIncoming: break
} }
@ -227,18 +229,21 @@ extension ContextMenuVC {
let shouldShowInfo: Bool = (cellViewModel.attachments?.isEmpty == false) let shouldShowInfo: Bool = (cellViewModel.attachments?.isEmpty == false)
let generatedActions: [Action] = [ let generatedActions: [Action] = [
(canRetry ? Action.retry(cellViewModel, delegate) : nil), (canRetry ? Action.retry(cellViewModel, delegate, using: dependencies) : nil),
(viewModelCanReply(cellViewModel) ? Action.reply(cellViewModel, delegate) : nil), (viewModelCanReply(cellViewModel) ? Action.reply(cellViewModel, delegate, using: dependencies) : nil),
(canCopy ? Action.copy(cellViewModel, delegate) : nil), (canCopy ? Action.copy(cellViewModel, delegate, using: dependencies) : nil),
(canSave ? Action.save(cellViewModel, delegate) : nil), (canSave ? Action.save(cellViewModel, delegate, using: dependencies) : nil),
(canCopySessionId ? Action.copySessionID(cellViewModel, delegate) : nil), (canCopySessionId ? Action.copySessionID(cellViewModel, delegate) : nil),
(canDelete ? Action.delete(cellViewModel, delegate) : nil), (canDelete ? Action.delete(cellViewModel, delegate, using: dependencies) : nil),
(canBan ? Action.ban(cellViewModel, delegate) : nil), (canBan ? Action.ban(cellViewModel, delegate, using: dependencies) : nil),
(canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate) : nil), (canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate, using: dependencies) : nil),
(shouldShowInfo ? Action.info(cellViewModel, delegate) : nil), (shouldShowInfo ? Action.info(cellViewModel, delegate, using: dependencies) : nil),
] ]
.appending(contentsOf: (shouldShowEmojiActions ? recentEmojis : []).map { Action.react(cellViewModel, $0, delegate) }) .appending(
.appending(Action.emojiPlusButton(cellViewModel, delegate)) contentsOf: (shouldShowEmojiActions ? recentEmojis : [])
.map { Action.react(cellViewModel, $0, delegate, using: dependencies) }
)
.appending(Action.emojiPlusButton(cellViewModel, delegate, using: dependencies))
.compactMap { $0 } .compactMap { $0 }
guard !generatedActions.isEmpty else { return [] } guard !generatedActions.isEmpty else { return [] }
@ -250,16 +255,16 @@ extension ContextMenuVC {
// MARK: - Delegate // MARK: - Delegate
protocol ContextMenuActionDelegate { protocol ContextMenuActionDelegate {
func info(_ cellViewModel: MessageViewModel) func info(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func retry(_ cellViewModel: MessageViewModel) func retry(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func reply(_ cellViewModel: MessageViewModel) func reply(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func copy(_ cellViewModel: MessageViewModel) func copy(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func copySessionID(_ cellViewModel: MessageViewModel) func copySessionID(_ cellViewModel: MessageViewModel)
func delete(_ cellViewModel: MessageViewModel) func delete(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func save(_ cellViewModel: MessageViewModel) func save(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func ban(_ cellViewModel: MessageViewModel) func ban(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel) func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones) func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones, using dependencies: Dependencies)
func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel) func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func contextMenuDismissed() func contextMenuDismissed()
} }

View file

@ -5,6 +5,7 @@ import GRDB
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUIKit import SessionUIKit
import SessionUtilitiesKit
public class StyledSearchController: UISearchController { public class StyledSearchController: UISearchController {
public override var preferredStatusBarStyle: UIStatusBarStyle { public override var preferredStatusBarStyle: UIStatusBarStyle {

View file

@ -11,6 +11,7 @@ import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionSnodeKit
extension ConversationVC: extension ConversationVC:
InputViewDelegate, InputViewDelegate,
@ -149,8 +150,15 @@ extension ConversationVC:
dismiss(animated: true, completion: nil) dismiss(animated: true, completion: nil)
} }
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) { func sendMediaNav(
sendMessage(text: (messageText ?? ""), attachments: attachments) _ sendMediaNavigationController: SendMediaNavigationController,
didApproveAttachments attachments: [SignalAttachment],
forThreadId threadId: String,
threadVariant: SessionThread.Variant,
messageText: String?,
using dependencies: Dependencies
) {
sendMessage(text: (messageText ?? ""), attachments: attachments, using: dependencies)
resetMentions() resetMentions()
dismiss(animated: true) { [weak self] in dismiss(animated: true) { [weak self] in
@ -173,8 +181,15 @@ extension ConversationVC:
// MARK: - AttachmentApprovalViewControllerDelegate // MARK: - AttachmentApprovalViewControllerDelegate
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) { func attachmentApproval(
sendMessage(text: (messageText ?? ""), attachments: attachments) _ attachmentApproval: AttachmentApprovalViewController,
didApproveAttachments attachments: [SignalAttachment],
forThreadId threadId: String,
threadVariant: SessionThread.Variant,
messageText: String?,
using dependencies: Dependencies
) {
sendMessage(text: (messageText ?? ""), attachments: attachments, using: dependencies)
resetMentions() resetMentions()
dismiss(animated: true) { [weak self] in dismiss(animated: true) { [weak self] in
@ -248,11 +263,13 @@ extension ConversationVC:
func handleLibraryButtonTapped() { func handleLibraryButtonTapped() {
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
Permissions.requestLibraryPermissionIfNeeded { [weak self] in Permissions.requestLibraryPermissionIfNeeded { [weak self] in
DispatchQueue.main.async { DispatchQueue.main.async {
let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst( let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst(
threadId: threadId threadId: threadId,
threadVariant: threadVariant
) )
sendMediaNavController.sendMediaNavDelegate = self sendMediaNavController.sendMediaNavDelegate = self
sendMediaNavController.modalPresentationStyle = .fullScreen sendMediaNavController.modalPresentationStyle = .fullScreen
@ -270,7 +287,10 @@ extension ConversationVC:
SNLog("Proceeding without microphone access. Any recorded video will be silent.") SNLog("Proceeding without microphone access. Any recorded video will be silent.")
} }
let sendMediaNavController = SendMediaNavigationController.showingCameraFirst(threadId: self.viewModel.threadData.threadId) let sendMediaNavController = SendMediaNavigationController.showingCameraFirst(
threadId: self.viewModel.threadData.threadId,
threadVariant: self.viewModel.threadData.threadVariant
)
sendMediaNavController.sendMediaNavDelegate = self sendMediaNavController.sendMediaNavDelegate = self
sendMediaNavController.modalPresentationStyle = .fullScreen sendMediaNavController.modalPresentationStyle = .fullScreen
@ -356,6 +376,7 @@ extension ConversationVC:
func showAttachmentApprovalDialog(for attachments: [SignalAttachment]) { func showAttachmentApprovalDialog(for attachments: [SignalAttachment]) {
let navController = AttachmentApprovalViewController.wrappedInNavController( let navController = AttachmentApprovalViewController.wrappedInNavController(
threadId: self.viewModel.threadData.threadId, threadId: self.viewModel.threadData.threadId,
threadVariant: self.viewModel.threadData.threadVariant,
attachments: attachments, attachments: attachments,
approvalDelegate: self approvalDelegate: self
) )
@ -409,7 +430,8 @@ extension ConversationVC:
attachments: [SignalAttachment] = [], attachments: [SignalAttachment] = [],
linkPreviewDraft: LinkPreviewDraft? = nil, linkPreviewDraft: LinkPreviewDraft? = nil,
quoteModel: QuotedReplyModel? = nil, quoteModel: QuotedReplyModel? = nil,
hasPermissionToSendSeed: Bool = false hasPermissionToSendSeed: Bool = false,
using dependencies: Dependencies = Dependencies()
) { ) {
guard !showBlockedModalIfNeeded() else { return } guard !showBlockedModalIfNeeded() else { return }
@ -480,20 +502,23 @@ extension ConversationVC:
quoteModel: quoteModel quoteModel: quoteModel
) )
sendMessage(optimisticData: optimisticData) sendMessage(optimisticData: optimisticData, using: dependencies)
} }
private func sendMessage(optimisticData: ConversationViewModel.OptimisticMessageData) { private func sendMessage(
optimisticData: ConversationViewModel.OptimisticMessageData,
using dependencies: Dependencies
) {
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
DispatchQueue.global(qos:.userInitiated).async { DispatchQueue.global(qos:.userInitiated).async(using: dependencies) {
// Generate the quote thumbnail if needed (want this to happen outside of the DBWrite thread as // Generate the quote thumbnail if needed (want this to happen outside of the DBWrite thread as
// this can take up to 0.5s // this can take up to 0.5s
let quoteThumbnailAttachment: Attachment? = optimisticData.quoteModel?.attachment?.cloneAsQuoteThumbnail() let quoteThumbnailAttachment: Attachment? = optimisticData.quoteModel?.attachment?.cloneAsQuoteThumbnail()
// Actually send the message // Actually send the message
Storage.shared dependencies.storage
.writePublisher { [weak self] db in .writePublisher { [weak self] db in
// Update the thread to be visible (if it isn't already) // Update the thread to be visible (if it isn't already)
if self?.viewModel.threadData.threadShouldBeVisible == false { if self?.viewModel.threadData.threadShouldBeVisible == false {
@ -541,7 +566,8 @@ extension ConversationVC:
db, db,
interaction: insertedInteraction, interaction: insertedInteraction,
threadId: threadId, threadId: threadId,
threadVariant: threadVariant threadVariant: threadVariant,
using: dependencies
) )
} }
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) .subscribe(on: DispatchQueue.global(qos: .userInitiated))
@ -635,6 +661,7 @@ extension ConversationVC:
let approvalVC = AttachmentApprovalViewController.wrappedInNavController( let approvalVC = AttachmentApprovalViewController.wrappedInNavController(
threadId: self.viewModel.threadData.threadId, threadId: self.viewModel.threadData.threadId,
threadVariant: self.viewModel.threadData.threadVariant,
attachments: [ attachment ], attachments: [ attachment ],
approvalDelegate: self approvalDelegate: self
) )
@ -798,10 +825,14 @@ extension ConversationVC:
self.contextMenuWindow?.makeKeyAndVisible() self.contextMenuWindow?.makeKeyAndVisible()
} }
func handleItemTapped(_ cellViewModel: MessageViewModel, gestureRecognizer: UITapGestureRecognizer) { func handleItemTapped(
_ cellViewModel: MessageViewModel,
gestureRecognizer: UITapGestureRecognizer,
using dependencies: Dependencies = Dependencies()
) {
guard cellViewModel.variant != .standardOutgoing || (cellViewModel.state != .failed && cellViewModel.state != .failedToSync) else { guard cellViewModel.variant != .standardOutgoing || (cellViewModel.state != .failed && cellViewModel.state != .failedToSync) else {
// Show the failed message sheet // Show the failed message sheet
showFailedMessageSheet(for: cellViewModel) showFailedMessageSheet(for: cellViewModel, using: dependencies)
return return
} }
@ -875,8 +906,8 @@ extension ConversationVC:
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
// Retry downloading the failed attachment // Retry downloading the failed attachment
Storage.shared.writeAsync { db in dependencies.storage.writeAsync { db in
JobRunner.add( dependencies.jobRunner.add(
db, db,
job: Job( job: Job(
variant: .attachmentDownload, variant: .attachmentDownload,
@ -885,7 +916,9 @@ extension ConversationVC:
details: AttachmentDownloadJob.Details( details: AttachmentDownloadJob.Details(
attachmentId: mediaView.attachment.id attachmentId: mediaView.attachment.id
) )
) ),
canStartJob: true,
using: dependencies
) )
} }
break break
@ -1024,8 +1057,8 @@ extension ConversationVC:
self.present(actionSheet, animated: true) self.present(actionSheet, animated: true)
} }
func handleReplyButtonTapped(for cellViewModel: MessageViewModel) { func handleReplyButtonTapped(for cellViewModel: MessageViewModel, using dependencies: Dependencies) {
reply(cellViewModel) reply(cellViewModel, using: dependencies)
} }
func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?) { func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?) {
@ -1134,15 +1167,15 @@ extension ConversationVC:
UIView.setAnimationsEnabled(true) UIView.setAnimationsEnabled(true)
} }
func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones) { func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones, using dependencies: Dependencies) {
react(cellViewModel, with: emoji.rawValue, remove: false) react(cellViewModel, with: emoji.rawValue, remove: false, using: dependencies)
} }
func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones) { func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones, using dependencies: Dependencies) {
react(cellViewModel, with: emoji.rawValue, remove: true) react(cellViewModel, with: emoji.rawValue, remove: true, using: dependencies)
} }
func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) { func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String, using dependencies: Dependencies) {
guard cellViewModel.threadVariant == .community else { return } guard cellViewModel.threadVariant == .community else { return }
Storage.shared Storage.shared
@ -1219,7 +1252,7 @@ extension ConversationVC:
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
let openGroupRoom: String? = self.viewModel.threadData.openGroupRoomToken let openGroupRoom: String? = self.viewModel.threadData.openGroupRoomToken
let sentTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs() let sentTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs()
let recentReactionTimestamps: [Int64] = dependencies.generalCache.recentReactionTimestamps let recentReactionTimestamps: [Int64] = dependencies.caches[.general].recentReactionTimestamps
guard guard
recentReactionTimestamps.count < 20 || recentReactionTimestamps.count < 20 ||
@ -1237,7 +1270,7 @@ extension ConversationVC:
return return
} }
dependencies.mutableGeneralCache.mutate { dependencies.caches.mutate(cache: .general) {
$0.recentReactionTimestamps = Array($0.recentReactionTimestamps $0.recentReactionTimestamps = Array($0.recentReactionTimestamps
.suffix(19)) .suffix(19))
.appending(sentTimestamp) .appending(sentTimestamp)
@ -1272,9 +1305,9 @@ extension ConversationVC:
)) ))
} }
} }
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
.flatMap { pendingChange -> AnyPublisher<(MessageSender.PreparedSendData?, OpenGroupInfo?), Error> in .flatMap { pendingChange -> AnyPublisher<(MessageSender.PreparedSendData?, OpenGroupInfo?), Error> in
Storage.shared.writePublisher { [weak self] db -> (MessageSender.PreparedSendData?, OpenGroupInfo?) in dependencies.storage.writePublisher { [weak self] db -> (MessageSender.PreparedSendData?, OpenGroupInfo?) in
// Update the thread to be visible (if it isn't already) // Update the thread to be visible (if it isn't already)
if self?.viewModel.threadData.threadShouldBeVisible == false { if self?.viewModel.threadData.threadShouldBeVisible == false {
_ = try SessionThread _ = try SessionThread
@ -1383,7 +1416,8 @@ extension ConversationVC:
namespace: try Message.Destination namespace: try Message.Destination
.from(db, threadId: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant) .from(db, threadId: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant)
.defaultNamespace, .defaultNamespace,
interactionId: cellViewModel.id interactionId: cellViewModel.id,
using: dependencies
) )
return (sendData, nil) return (sendData, nil)
@ -1393,7 +1427,7 @@ extension ConversationVC:
.tryFlatMap { messageSendData, openGroupInfo -> AnyPublisher<Void, Error> in .tryFlatMap { messageSendData, openGroupInfo -> AnyPublisher<Void, Error> in
switch (messageSendData, openGroupInfo) { switch (messageSendData, openGroupInfo) {
case (.some(let sendData), _): case (.some(let sendData), _):
return MessageSender.sendImmediate(preparedSendData: sendData) return MessageSender.sendImmediate(data: sendData, using: dependencies)
case (_, .some(let info)): case (_, .some(let info)):
return OpenGroupAPI.send(data: info.sendData) return OpenGroupAPI.send(data: info.sendData)
@ -1444,14 +1478,14 @@ extension ConversationVC:
} }
} }
func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel) { func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
hideInputAccessoryView() hideInputAccessoryView()
let emojiPicker = EmojiPickerSheet( let emojiPicker = EmojiPickerSheet(
completionHandler: { [weak self] emoji in completionHandler: { [weak self] emoji in
guard let emoji: EmojiWithSkinTones = emoji else { return } guard let emoji: EmojiWithSkinTones = emoji else { return }
self?.react(cellViewModel, with: emoji) self?.react(cellViewModel, with: emoji, using: dependencies)
}, },
dismissHandler: { [weak self] in dismissHandler: { [weak self] in
self?.showInputAccessoryView() self?.showInputAccessoryView()
@ -1467,7 +1501,7 @@ extension ConversationVC:
// MARK: --action handling // MARK: --action handling
func showFailedMessageSheet(for cellViewModel: MessageViewModel) { private func showFailedMessageSheet(for cellViewModel: MessageViewModel, using dependencies: Dependencies) {
let sheet = UIAlertController( let sheet = UIAlertController(
title: (cellViewModel.state == .failedToSync ? title: (cellViewModel.state == .failedToSync ?
"MESSAGE_DELIVERY_FAILED_SYNC_TITLE".localized() : "MESSAGE_DELIVERY_FAILED_SYNC_TITLE".localized() :
@ -1494,7 +1528,7 @@ extension ConversationVC:
"context_menu_resend".localized() "context_menu_resend".localized()
), ),
style: .default, style: .default,
handler: { [weak self] _ in self?.retry(cellViewModel) } handler: { [weak self] _ in self?.retry(cellViewModel, using: dependencies) }
)) ))
// HACK: Extracting this info from the error string is pretty dodgy // HACK: Extracting this info from the error string is pretty dodgy
@ -1607,7 +1641,7 @@ extension ConversationVC:
// MARK: - ContextMenuActionDelegate // MARK: - ContextMenuActionDelegate
func info(_ cellViewModel: MessageViewModel) { func info(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
let mediaInfoVC = MediaInfoVC( let mediaInfoVC = MediaInfoVC(
attachments: (cellViewModel.attachments ?? []), attachments: (cellViewModel.attachments ?? []),
isOutgoing: (cellViewModel.variant == .standardOutgoing), isOutgoing: (cellViewModel.variant == .standardOutgoing),
@ -1618,8 +1652,7 @@ extension ConversationVC:
navigationController?.pushViewController(mediaInfoVC, animated: true) navigationController?.pushViewController(mediaInfoVC, animated: true)
} }
func retry(_ cellViewModel: MessageViewModel) { func retry(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
// If the failed message is an optimistic update then we need to do things differently
guard cellViewModel.id != MessageViewModel.optimisticUpdateId else { guard cellViewModel.id != MessageViewModel.optimisticUpdateId else {
guard guard
let optimisticMessageId: UUID = cellViewModel.optimisticMessageId, let optimisticMessageId: UUID = cellViewModel.optimisticMessageId,
@ -1640,11 +1673,11 @@ extension ConversationVC:
} }
// Try to send the optimistic message again // Try to send the optimistic message again
self.sendMessage(optimisticData: optimisticMessageData) sendMessage(optimisticData: optimisticMessageData, using: dependencies)
return return
} }
Storage.shared.writeAsync { [weak self] db in dependencies.storage.writeAsync { [weak self] db in
guard guard
let threadId: String = self?.viewModel.threadData.threadId, let threadId: String = self?.viewModel.threadData.threadId,
let threadVariant: SessionThread.Variant = self?.viewModel.threadData.threadVariant, let threadVariant: SessionThread.Variant = self?.viewModel.threadData.threadVariant,
@ -1685,12 +1718,13 @@ extension ConversationVC:
interaction: interaction, interaction: interaction,
threadId: threadId, threadId: threadId,
threadVariant: threadVariant, threadVariant: threadVariant,
isSyncMessage: (cellViewModel.state == .failedToSync) isSyncMessage: (cellViewModel.state == .failedToSync),
using: dependencies
) )
} }
} }
func reply(_ cellViewModel: MessageViewModel) { func reply(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
let maybeQuoteDraft: QuotedReplyModel? = QuotedReplyModel.quotedReplyForSending( let maybeQuoteDraft: QuotedReplyModel? = QuotedReplyModel.quotedReplyForSending(
threadId: self.viewModel.threadData.threadId, threadId: self.viewModel.threadData.threadId,
authorId: cellViewModel.authorId, authorId: cellViewModel.authorId,
@ -1713,7 +1747,7 @@ extension ConversationVC:
snInputView.becomeFirstResponder() snInputView.becomeFirstResponder()
} }
func copy(_ cellViewModel: MessageViewModel) { func copy(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
switch cellViewModel.cellType { switch cellViewModel.cellType {
case .typingIndicator, .dateHeader, .unreadMarker: break case .typingIndicator, .dateHeader, .unreadMarker: break
@ -1751,7 +1785,7 @@ extension ConversationVC:
UIPasteboard.general.string = cellViewModel.authorId UIPasteboard.general.string = cellViewModel.authorId
} }
func delete(_ cellViewModel: MessageViewModel) { func delete(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
switch cellViewModel.variant { switch cellViewModel.variant {
case .standardIncomingDeleted, .infoCall, case .standardIncomingDeleted, .infoCall,
.infoScreenshotNotification, .infoMediaSavedNotification, .infoScreenshotNotification, .infoMediaSavedNotification,
@ -1947,7 +1981,8 @@ extension ConversationVC:
message: unsendRequest, message: unsendRequest,
threadId: cellViewModel.threadId, threadId: cellViewModel.threadId,
interactionId: nil, interactionId: nil,
to: .contact(publicKey: userPublicKey) to: .contact(publicKey: userPublicKey),
using: dependencies
) )
} }
return return
@ -1970,7 +2005,8 @@ extension ConversationVC:
message: unsendRequest, message: unsendRequest,
threadId: cellViewModel.threadId, threadId: cellViewModel.threadId,
interactionId: nil, interactionId: nil,
to: .contact(publicKey: userPublicKey) to: .contact(publicKey: userPublicKey),
using: dependencies
) )
} }
self?.showInputAccessoryView() self?.showInputAccessoryView()
@ -1998,7 +2034,8 @@ extension ConversationVC:
message: unsendRequest, message: unsendRequest,
interactionId: nil, interactionId: nil,
threadId: cellViewModel.threadId, threadId: cellViewModel.threadId,
threadVariant: cellViewModel.threadVariant threadVariant: cellViewModel.threadVariant,
using: dependencies
) )
} }
@ -2032,7 +2069,7 @@ extension ConversationVC:
} }
} }
func save(_ cellViewModel: MessageViewModel) { func save(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
guard cellViewModel.cellType == .mediaMessage else { return } guard cellViewModel.cellType == .mediaMessage else { return }
let mediaAttachments: [(Attachment, String)] = (cellViewModel.attachments ?? []) let mediaAttachments: [(Attachment, String)] = (cellViewModel.attachments ?? [])
@ -2074,24 +2111,10 @@ extension ConversationVC:
return return
} }
let threadId: String = self.viewModel.threadData.threadId sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs)))
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
Storage.shared.writeAsync { db in
try MessageSender.send(
db,
message: DataExtractionNotification(
kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs)),
sentTimestamp: UInt64(SnodeAPI.currentOffsetTimestampMs())
),
interactionId: nil,
threadId: threadId,
threadVariant: threadVariant
)
}
} }
func ban(_ cellViewModel: MessageViewModel) { func ban(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
guard cellViewModel.threadVariant == .community else { return } guard cellViewModel.threadVariant == .community else { return }
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
@ -2147,7 +2170,7 @@ extension ConversationVC:
self.present(modal, animated: true) self.present(modal, animated: true)
} }
func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel) { func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
guard cellViewModel.threadVariant == .community else { return } guard cellViewModel.threadVariant == .community else { return }
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
@ -2205,7 +2228,7 @@ extension ConversationVC:
// MARK: - VoiceMessageRecordingViewDelegate // MARK: - VoiceMessageRecordingViewDelegate
func startVoiceMessageRecording() { func startVoiceMessageRecording(using dependencies: Dependencies) {
// Request permission if needed // Request permission if needed
Permissions.requestMicrophonePermissionIfNeeded() { [weak self] in Permissions.requestMicrophonePermissionIfNeeded() { [weak self] in
DispatchQueue.main.async { DispatchQueue.main.async {
@ -2254,7 +2277,7 @@ extension ConversationVC:
// Limit voice messages to a minute // Limit voice messages to a minute
audioTimer = Timer.scheduledTimer(withTimeInterval: 180, repeats: false, block: { [weak self] _ in audioTimer = Timer.scheduledTimer(withTimeInterval: 180, repeats: false, block: { [weak self] _ in
self?.snInputView.hideVoiceMessageUI() self?.snInputView.hideVoiceMessageUI()
self?.endVoiceMessageRecording() self?.endVoiceMessageRecording(using: dependencies)
}) })
// Prepare audio recorder // Prepare audio recorder
@ -2270,7 +2293,7 @@ extension ConversationVC:
} }
} }
func endVoiceMessageRecording() { func endVoiceMessageRecording(using dependencies: Dependencies) {
UIApplication.shared.isIdleTimerDisabled = true UIApplication.shared.isIdleTimerDisabled = true
// Hide the UI // Hide the UI
@ -2322,7 +2345,7 @@ extension ConversationVC:
} }
// Send attachment // Send attachment
sendMessage(text: "", attachments: [attachment]) sendMessage(text: "", attachments: [attachment], using: dependencies)
} }
func cancelVoiceMessageRecording() { func cancelVoiceMessageRecording() {
@ -2339,23 +2362,29 @@ extension ConversationVC:
// MARK: - Data Extraction Notifications // MARK: - Data Extraction Notifications
@objc func sendScreenshotNotification() { @objc func sendScreenshotNotification() { sendDataExtraction(kind: .screenshot) }
func sendDataExtraction(
kind: DataExtractionNotification.Kind,
using dependencies: Dependencies = Dependencies()
) {
// Only send screenshot notifications to one-to-one conversations // Only send screenshot notifications to one-to-one conversations
guard self.viewModel.threadData.threadVariant == .contact else { return } guard self.viewModel.threadData.threadVariant == .contact else { return }
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
Storage.shared.writeAsync { db in dependencies.storage.writeAsync { db in
try MessageSender.send( try MessageSender.send(
db, db,
message: DataExtractionNotification( message: DataExtractionNotification(
kind: .screenshot, kind: kind,
sentTimestamp: UInt64(SnodeAPI.currentOffsetTimestampMs()) sentTimestamp: UInt64(SnodeAPI.currentOffsetTimestampMs())
), ),
interactionId: nil, interactionId: nil,
threadId: threadId, threadId: threadId,
threadVariant: threadVariant threadVariant: threadVariant,
using: dependencies
) )
} }
} }
@ -2391,7 +2420,8 @@ extension ConversationVC {
for threadId: String, for threadId: String,
threadVariant: SessionThread.Variant, threadVariant: SessionThread.Variant,
isNewThread: Bool, isNewThread: Bool,
timestampMs: Int64 timestampMs: Int64,
using dependencies: Dependencies = Dependencies()
) { ) {
guard threadVariant == .contact else { return } guard threadVariant == .contact else { return }
@ -2432,7 +2462,8 @@ extension ConversationVC {
), ),
interactionId: nil, interactionId: nil,
threadId: threadId, threadId: threadId,
threadVariant: threadVariant threadVariant: threadVariant,
using: dependencies
) )
} }

View file

@ -208,17 +208,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
}() }()
private lazy var emptyStateLabel: UILabel = { private lazy var emptyStateLabel: UILabel = {
let text: String = String( let text: String = emptyStateText(for: viewModel.threadData)
format: {
switch (viewModel.threadData.threadIsNoteToSelf, viewModel.threadData.canWrite) {
case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized()
case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized()
default: return "CONVERSATION_EMPTY_STATE".localized()
}
}(),
viewModel.threadData.displayName
)
let result: UILabel = UILabel() let result: UILabel = UILabel()
result.accessibilityLabel = "Empty state label" result.accessibilityLabel = "Empty state label"
result.translatesAutoresizingMaskIntoConstraints = false result.translatesAutoresizingMaskIntoConstraints = false
@ -584,7 +574,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
!SessionUtil.conversationInConfig( !SessionUtil.conversationInConfig(
threadId: threadId, threadId: threadId,
threadVariant: viewModel.threadData.threadVariant, threadVariant: viewModel.threadData.threadVariant,
visibleOnly: true visibleOnly: false
) )
{ {
Storage.shared.writeAsync { db in Storage.shared.writeAsync { db in
@ -698,6 +688,24 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
self.viewModel.onInteractionChange = nil self.viewModel.onInteractionChange = nil
} }
private func emptyStateText(for threadData: SessionThreadViewModel) -> String {
return String(
format: {
switch (threadData.threadIsNoteToSelf, threadData.canWrite) {
case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized()
case (_, false):
return (threadData.profile?.blocksCommunityMessageRequests == true ?
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE".localized() :
"CONVERSATION_EMPTY_STATE_READ_ONLY".localized()
)
default: return "CONVERSATION_EMPTY_STATE".localized()
}
}(),
threadData.displayName
)
}
private func handleThreadUpdates(_ updatedThreadData: SessionThreadViewModel, initialLoad: Bool = false) { private func handleThreadUpdates(_ updatedThreadData: SessionThreadViewModel, initialLoad: Bool = false) {
// Ensure the first load or a load when returning from a child screen runs without animations (if // Ensure the first load or a load when returning from a child screen runs without animations (if
// we don't do this the cells will animate in from a frame of CGRect.zero or have a buggy transition) // we don't do this the cells will animate in from a frame of CGRect.zero or have a buggy transition)
@ -738,17 +746,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
) )
// Update the empty state // Update the empty state
let text: String = String( let text: String = emptyStateText(for: updatedThreadData)
format: {
switch (updatedThreadData.threadIsNoteToSelf, updatedThreadData.canWrite) {
case (true, _): return "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF".localized()
case (_, false): return "CONVERSATION_EMPTY_STATE_READ_ONLY".localized()
default: return "CONVERSATION_EMPTY_STATE".localized()
}
}(),
updatedThreadData.displayName
)
emptyStateLabel.attributedText = NSAttributedString(string: text) emptyStateLabel.attributedText = NSAttributedString(string: text)
.adding( .adding(
attributes: [.font: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize)], attributes: [.font: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize)],
@ -791,9 +789,11 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
updatedThreadData.threadRequiresApproval == true updatedThreadData.threadRequiresApproval == true
) )
self?.messageRequestStackView.isHidden = ( self?.messageRequestStackView.isHidden = (
!updatedThreadData.canWrite || (
updatedThreadData.threadIsMessageRequest == false && updatedThreadData.threadIsMessageRequest == false &&
updatedThreadData.threadRequiresApproval == false updatedThreadData.threadRequiresApproval == false
) )
)
self?.messageRequestBackgroundView.isHidden = (self?.messageRequestStackView.isHidden == true) self?.messageRequestBackgroundView.isHidden = (self?.messageRequestStackView.isHidden == true)
self?.messageRequestDescriptionLabelBottomConstraint?.constant = (updatedThreadData.threadRequiresApproval == true ? -4 : -20) self?.messageRequestDescriptionLabelBottomConstraint?.constant = (updatedThreadData.threadRequiresApproval == true ? -4 : -20)

View file

@ -416,14 +416,14 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
if inputViewButton == sendButton { delegate?.handleSendButtonTapped() } if inputViewButton == sendButton { delegate?.handleSendButtonTapped() }
} }
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?, using dependencies: Dependencies) {
guard inputViewButton == voiceMessageButton else { return } guard inputViewButton == voiceMessageButton else { return }
// Note: The 'showVoiceMessageUI' call MUST come before triggering 'startVoiceMessageRecording' // Note: The 'showVoiceMessageUI' call MUST come before triggering 'startVoiceMessageRecording'
// because if something goes wrong it'll trigger `hideVoiceMessageUI` and we don't want it to // because if something goes wrong it'll trigger `hideVoiceMessageUI` and we don't want it to
// end up in a state with the input content hidden // end up in a state with the input content hidden
showVoiceMessageUI() showVoiceMessageUI()
delegate?.startVoiceMessageRecording() delegate?.startVoiceMessageRecording(using: dependencies)
} }
func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) {

View file

@ -2,6 +2,7 @@
import UIKit import UIKit
import SessionUIKit import SessionUIKit
import SessionUtilitiesKit
final class InputViewButton: UIView { final class InputViewButton: UIView {
private let icon: UIImage? private let icon: UIImage?
@ -137,7 +138,9 @@ final class InputViewButton: UIView {
// We want to detect both taps and long presses // We want to detect both taps and long presses
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { onTouchesBegan() }
private func onTouchesBegan(using dependencies: Dependencies = Dependencies()) {
guard isUserInteractionEnabled else { return } guard isUserInteractionEnabled else { return }
UIImpactFeedbackGenerator(style: .heavy).impactOccurred() UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
@ -145,7 +148,7 @@ final class InputViewButton: UIView {
invalidateLongPressIfNeeded() invalidateLongPressIfNeeded()
longPressTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { [weak self] _ in longPressTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { [weak self] _ in
self?.isLongPress = true self?.isLongPress = true
self?.delegate?.handleInputViewButtonLongPressBegan(self) self?.delegate?.handleInputViewButtonLongPressBegan(self, using: dependencies)
}) })
} }
@ -185,13 +188,13 @@ final class InputViewButton: UIView {
protocol InputViewButtonDelegate: AnyObject { protocol InputViewButtonDelegate: AnyObject {
func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) func handleInputViewButtonTapped(_ inputViewButton: InputViewButton)
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?, using dependencies: Dependencies)
func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?)
func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?)
} }
extension InputViewButtonDelegate { extension InputViewButtonDelegate {
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { } func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?, using dependencies: Dependencies) { }
func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { } func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { }
func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { } func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { }
} }

View file

@ -310,12 +310,12 @@ final class VoiceMessageRecordingView: UIView {
} }
} }
func handleLongPressEnded(at location: CGPoint) { func handleLongPressEnded(at location: CGPoint, using dependencies: Dependencies = Dependencies()) {
if pulseView.frame.contains(location) { if pulseView.frame.contains(location) {
delegate?.endVoiceMessageRecording() delegate?.endVoiceMessageRecording(using: dependencies)
} }
else if isValidLockViewLocation(location) { else if isValidLockViewLocation(location) {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleCircleViewTap)) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onCircleViewTap))
circleView.addGestureRecognizer(tapGestureRecognizer) circleView.addGestureRecognizer(tapGestureRecognizer)
UIView.animate(withDuration: 0.25, delay: 0, options: .transitionCrossDissolve, animations: { UIView.animate(withDuration: 0.25, delay: 0, options: .transitionCrossDissolve, animations: {
@ -332,8 +332,10 @@ final class VoiceMessageRecordingView: UIView {
} }
} }
@objc private func handleCircleViewTap() { @objc private func onCircleViewTap() { handleCircleViewTap() }
delegate?.endVoiceMessageRecording()
private func handleCircleViewTap(using dependencies: Dependencies = Dependencies()) {
delegate?.endVoiceMessageRecording(using: dependencies)
} }
@objc private func handleCancelButtonTapped() { @objc private func handleCancelButtonTapped() {
@ -474,7 +476,7 @@ extension VoiceMessageRecordingView {
// MARK: - Delegate // MARK: - Delegate
protocol VoiceMessageRecordingViewDelegate: AnyObject { protocol VoiceMessageRecordingViewDelegate: AnyObject {
func startVoiceMessageRecording() func startVoiceMessageRecording(using dependencies: Dependencies)
func endVoiceMessageRecording() func endVoiceMessageRecording(using dependencies: Dependencies)
func cancelVoiceMessageRecording() func cancelVoiceMessageRecording()
} }

View file

@ -3,6 +3,7 @@
import UIKit import UIKit
import SessionUIKit import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit
final class CallMessageCell: MessageCell { final class CallMessageCell: MessageCell {
private static let iconSize: CGFloat = 16 private static let iconSize: CGFloat = 16

View file

@ -3,6 +3,7 @@
import UIKit import UIKit
import SessionMessagingKit import SessionMessagingKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
public class MediaAlbumView: UIStackView { public class MediaAlbumView: UIStackView {
private let items: [Attachment] private let items: [Attachment]

View file

@ -6,6 +6,7 @@ import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SignalCoreKit import SignalCoreKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
public class MediaView: UIView { public class MediaView: UIView {
static let contentMode: UIView.ContentMode = .scaleAspectFill static let contentMode: UIView.ContentMode = .scaleAspectFill

View file

@ -156,7 +156,7 @@ final class QuoteView: UIView {
if attachment.isVisualMedia { if attachment.isVisualMedia {
attachment.thumbnail( attachment.thumbnail(
size: .small, size: .small,
success: { image, _ in success: { [imageView] image, _ in
guard Thread.isMainThread else { guard Thread.isMainThread else {
DispatchQueue.main.async { DispatchQueue.main.async {
imageView.image = image imageView.image = image
@ -234,8 +234,6 @@ final class QuoteView: UIView {
} }
// Label stack view // Label stack view
let bodyLabelSize = bodyLabel.systemLayoutSizeFitting(availableSpace)
let isCurrentUser: Bool = [ let isCurrentUser: Bool = [
currentUserPublicKey, currentUserPublicKey,
currentUserBlinded15PublicKey, currentUserBlinded15PublicKey,
@ -288,9 +286,8 @@ final class QuoteView: UIView {
cancelButton.set(.height, to: cancelButtonSize) cancelButton.set(.height, to: cancelButtonSize)
cancelButton.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside) cancelButton.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside)
addSubview(cancelButton) mainStackView.addArrangedSubview(cancelButton)
cancelButton.center(.vertical, in: self) cancelButton.center(.vertical, in: self)
cancelButton.pin(.right, to: .right, of: self)
} }
} }

View file

@ -3,6 +3,7 @@
import UIKit import UIKit
import SessionUIKit import SessionUIKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
@objc class TypingIndicatorView: UIStackView { @objc class TypingIndicatorView: UIStackView {
// This represents the spacing between the dots // This represents the spacing between the dots

View file

@ -2,6 +2,7 @@
import UIKit import UIKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit
public enum SwipeState { public enum SwipeState {
case began case began
@ -87,12 +88,18 @@ public class MessageCell: UITableViewCell {
protocol MessageCellDelegate: ReactionDelegate { protocol MessageCellDelegate: ReactionDelegate {
func handleItemLongPressed(_ cellViewModel: MessageViewModel) func handleItemLongPressed(_ cellViewModel: MessageViewModel)
func handleItemTapped(_ cellViewModel: MessageViewModel, gestureRecognizer: UITapGestureRecognizer) func handleItemTapped(_ cellViewModel: MessageViewModel, gestureRecognizer: UITapGestureRecognizer, using dependencies: Dependencies)
func handleItemDoubleTapped(_ cellViewModel: MessageViewModel) func handleItemDoubleTapped(_ cellViewModel: MessageViewModel)
func handleItemSwiped(_ cellViewModel: MessageViewModel, state: SwipeState) func handleItemSwiped(_ cellViewModel: MessageViewModel, state: SwipeState)
func openUrl(_ urlString: String) func openUrl(_ urlString: String)
func handleReplyButtonTapped(for cellViewModel: MessageViewModel) func handleReplyButtonTapped(for cellViewModel: MessageViewModel, using dependencies: Dependencies)
func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?) func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?)
func showReactionList(_ cellViewModel: MessageViewModel, selectedReaction: EmojiWithSkinTones?) func showReactionList(_ cellViewModel: MessageViewModel, selectedReaction: EmojiWithSkinTones?)
func needsLayout(for cellViewModel: MessageViewModel, expandingReactions: Bool) func needsLayout(for cellViewModel: MessageViewModel, expandingReactions: Bool)
} }
extension MessageCellDelegate {
func handleItemTapped(_ cellViewModel: MessageViewModel, gestureRecognizer: UITapGestureRecognizer) {
handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer, using: Dependencies())
}
}

View file

@ -52,7 +52,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
profilePictureView, profilePictureView,
replyButton, replyButton,
timerView, timerView,
messageStatusImageView, messageStatusContainerView,
reactionContainerView reactionContainerView
] ]
@ -861,7 +861,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
isHandlingLongPress = true isHandlingLongPress = true
} }
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { onTap(gestureRecognizer) }
private func onTap(_ gestureRecognizer: UITapGestureRecognizer, using dependencies: Dependencies = Dependencies()) {
guard let cellViewModel: MessageViewModel = self.viewModel else { return } guard let cellViewModel: MessageViewModel = self.viewModel else { return }
let location = gestureRecognizer.location(in: self) let location = gestureRecognizer.location(in: self)
@ -897,10 +899,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
if reactionContainerView.convert(reactionView.frame, from: reactionView.superview).contains(convertedLocation) { if reactionContainerView.convert(reactionView.frame, from: reactionView.superview).contains(convertedLocation) {
if reactionView.viewModel.showBorder { if reactionView.viewModel.showBorder {
delegate?.removeReact(cellViewModel, for: reactionView.viewModel.emoji) delegate?.removeReact(cellViewModel, for: reactionView.viewModel.emoji, using: dependencies)
} }
else { else {
delegate?.react(cellViewModel, with: reactionView.viewModel.emoji) delegate?.react(cellViewModel, with: reactionView.viewModel.emoji, using: dependencies)
} }
return return
} }
@ -917,7 +919,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
} }
} }
else if snContentView.bounds.contains(snContentView.convert(location, from: self)) { else if snContentView.bounds.contains(snContentView.convert(location, from: self)) {
delegate?.handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer) delegate?.handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer, using: dependencies)
} }
} }
@ -985,11 +987,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
} }
} }
private func reply() { private func reply(using dependencies: Dependencies = Dependencies()) {
guard let cellViewModel: MessageViewModel = self.viewModel else { return } guard let cellViewModel: MessageViewModel = self.viewModel else { return }
resetReply() resetReply()
delegate?.handleReplyButtonTapped(for: cellViewModel) delegate?.handleReplyButtonTapped(for: cellViewModel, using: dependencies)
} }
// MARK: - Convenience // MARK: - Convenience

View file

@ -39,10 +39,10 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
// MARK: - Initialization // MARK: - Initialization
init( init(
dependencies: Dependencies = Dependencies(),
threadId: String, threadId: String,
threadVariant: SessionThread.Variant, threadVariant: SessionThread.Variant,
config: DisappearingMessagesConfiguration config: DisappearingMessagesConfiguration,
using dependencies: Dependencies = Dependencies()
) { ) {
self.dependencies = dependencies self.dependencies = dependencies
self.threadId = threadId self.threadId = threadId
@ -68,7 +68,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
currentSelection currentSelection
.removeDuplicates() .removeDuplicates()
.map { [weak self] currentSelection in (self?.storedSelection != currentSelection) } .map { [weak self] currentSelection in (self?.storedSelection != currentSelection) }
.map { isChanged in .map { [weak self, dependencies] isChanged in
guard isChanged else { return [] } guard isChanged else { return [] }
return [ return [
@ -76,8 +76,8 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
id: .save, id: .save,
systemItem: .save, systemItem: .save,
accessibilityIdentifier: "Save button" accessibilityIdentifier: "Save button"
) { [weak self] in ) {
self?.saveChanges() self?.saveChanges(using: dependencies)
self?.dismissScreen() self?.dismissScreen()
} }
] ]
@ -100,7 +100,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this /// 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 _observableTableData: ObservableData = ValueObservation private lazy var _observableTableData: ObservableData = ValueObservation
.trackingConstantRegion { [weak self, config, dependencies, threadId = self.threadId] db -> [SectionModel] in .trackingConstantRegion { [weak self, config, dependencies, threadId = self.threadId] db -> [SectionModel] in
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies) let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel
.conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey) .conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey)
.fetchOne(db) .fetchOne(db)
@ -156,7 +156,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
// MARK: - Functions // MARK: - Functions
private func saveChanges() { private func saveChanges(using dependencies: Dependencies = Dependencies()) {
let threadId: String = self.threadId let threadId: String = self.threadId
let threadVariant: SessionThread.Variant = self.threadVariant let threadVariant: SessionThread.Variant = self.threadVariant
let currentSelection: TimeInterval = self.currentSelection.value let currentSelection: TimeInterval = self.currentSelection.value
@ -195,7 +195,8 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
), ),
interactionId: interaction.id, interactionId: interaction.id,
threadId: threadId, threadId: threadId,
threadVariant: threadVariant threadVariant: threadVariant,
using: dependencies
) )
// Legacy closed groups // Legacy closed groups

View file

@ -9,6 +9,7 @@ import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SessionSnodeKit
class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.NavButton, ThreadSettingsViewModel.Section, ThreadSettingsViewModel.Setting> { class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.NavButton, ThreadSettingsViewModel.Section, ThreadSettingsViewModel.Setting> {
// MARK: - Config // MARK: - Config
@ -60,10 +61,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
// MARK: - Initialization // MARK: - Initialization
init( init(
dependencies: Dependencies = Dependencies(),
threadId: String, threadId: String,
threadVariant: SessionThread.Variant, threadVariant: SessionThread.Variant,
didTriggerSearch: @escaping () -> () didTriggerSearch: @escaping () -> (),
using dependencies: Dependencies = Dependencies()
) { ) {
self.dependencies = dependencies self.dependencies = dependencies
self.threadId = threadId self.threadId = threadId
@ -178,6 +179,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
// MARK: - Content // MARK: - Content
private var originalState: SessionThreadViewModel?
override var title: String { override var title: String {
switch threadVariant { switch threadVariant {
case .contact: return "vc_settings_title".localized() case .contact: return "vc_settings_title".localized()
@ -196,7 +198,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this /// 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 _observableTableData: ObservableData = ValueObservation private lazy var _observableTableData: ObservableData = ValueObservation
.trackingConstantRegion { [weak self, dependencies, threadId = self.threadId, threadVariant = self.threadVariant] db -> [SectionModel] in .trackingConstantRegion { [weak self, dependencies, threadId = self.threadId, threadVariant = self.threadVariant] db -> [SectionModel] in
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies) let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel
.conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey) .conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey)
.fetchOne(db) .fetchOne(db)
@ -235,6 +237,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
threadViewModel.currentUserIsClosedGroupAdmin == true threadViewModel.currentUserIsClosedGroupAdmin == true
) )
let editIcon: UIImage? = UIImage(named: "icon_edit") let editIcon: UIImage? = UIImage(named: "icon_edit")
let originalState: SessionThreadViewModel = (self?.originalState ?? threadViewModel)
self?.originalState = threadViewModel
return [ return [
SectionModel( SectionModel(
@ -577,7 +581,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
title: "vc_conversation_settings_notify_for_mentions_only_title".localized(), title: "vc_conversation_settings_notify_for_mentions_only_title".localized(),
subtitle: "vc_conversation_settings_notify_for_mentions_only_explanation".localized(), subtitle: "vc_conversation_settings_notify_for_mentions_only_explanation".localized(),
rightAccessory: .toggle( rightAccessory: .toggle(
.boolValue(threadViewModel.threadOnlyNotifyForMentions == true) .boolValue(
threadViewModel.threadOnlyNotifyForMentions == true,
oldValue: (originalState.threadOnlyNotifyForMentions == true)
)
), ),
isEnabled: ( isEnabled: (
( (
@ -615,7 +622,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
), ),
title: "CONVERSATION_SETTINGS_MUTE_LABEL".localized(), title: "CONVERSATION_SETTINGS_MUTE_LABEL".localized(),
rightAccessory: .toggle( rightAccessory: .toggle(
.boolValue(threadViewModel.threadMutedUntilTimestamp != nil) .boolValue(
threadViewModel.threadMutedUntilTimestamp != nil,
oldValue: (originalState.threadMutedUntilTimestamp != nil)
)
), ),
isEnabled: ( isEnabled: (
( (
@ -661,7 +671,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
), ),
title: "CONVERSATION_SETTINGS_BLOCK_THIS_USER".localized(), title: "CONVERSATION_SETTINGS_BLOCK_THIS_USER".localized(),
rightAccessory: .toggle( rightAccessory: .toggle(
.boolValue(threadViewModel.threadIsBlocked == true) .boolValue(
threadViewModel.threadIsBlocked == true,
oldValue: (originalState.threadIsBlocked == true)
)
), ),
accessibility: Accessibility( accessibility: Accessibility(
identifier: "\(ThreadSettingsViewModel.self).block", identifier: "\(ThreadSettingsViewModel.self).block",
@ -755,7 +768,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
publicKey: publicKey publicKey: publicKey
) )
dependencies.storage.writeAsync { db in dependencies.storage.writeAsync { [dependencies] db in
try selectedUsers.forEach { userId in try selectedUsers.forEach { userId in
let thread: SessionThread = try SessionThread let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: userId, variant: .contact, shouldBeVisible: nil) .fetchOrCreate(db, id: userId, variant: .contact, shouldBeVisible: nil)
@ -786,7 +799,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
db, db,
interaction: interaction, interaction: interaction,
threadId: thread.id, threadId: thread.id,
threadVariant: thread.variant threadVariant: thread.variant,
using: dependencies
) )
} }
} }

View file

@ -5,6 +5,7 @@ import DifferenceKit
import SessionUIKit import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
final class ReactionListSheet: BaseVC { final class ReactionListSheet: BaseVC {
public struct ReactionSummary: Hashable, Differentiable { public struct ReactionSummary: Hashable, Differentiable {
@ -368,10 +369,12 @@ final class ReactionListSheet: BaseVC {
dismiss(animated: true, completion: nil) dismiss(animated: true, completion: nil)
} }
@objc private func clearAllTapped() { @objc private func clearAllTapped() { clearAll() }
private func clearAll(using dependencies: Dependencies = Dependencies()) {
guard let selectedReaction: EmojiWithSkinTones = self.reactionSummaries.first(where: { $0.isSelected })?.emoji else { return } guard let selectedReaction: EmojiWithSkinTones = self.reactionSummaries.first(where: { $0.isSelected })?.emoji else { return }
delegate?.removeAllReactions(messageViewModel, for: selectedReaction.rawValue) delegate?.removeAllReactions(messageViewModel, for: selectedReaction.rawValue, using: dependencies)
} }
} }
@ -599,7 +602,13 @@ extension ReactionListSheet {
// MARK: - Delegate // MARK: - Delegate
protocol ReactionDelegate: AnyObject { protocol ReactionDelegate: AnyObject {
func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones) func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones, using dependencies: Dependencies)
func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones) func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones, using dependencies: Dependencies)
func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String, using dependencies: Dependencies)
}
extension ReactionDelegate {
func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones) {
removeReact(cellViewModel, for: emoji, using: Dependencies())
}
} }

View file

@ -1,5 +1,6 @@
import Foundation import Foundation
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
extension Emoji { extension Emoji {
private static let availableCache: Atomic<[Emoji:Bool]> = Atomic([:]) private static let availableCache: Atomic<[Emoji:Bool]> = Atomic([:])

View file

@ -203,6 +203,11 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U
]) ])
} }
catch { catch {
// Don't log the 'interrupt' error as that's just the user typing too fast
if (error as? DatabaseError)?.resultCode != DatabaseError.SQLITE_INTERRUPT {
SNLog("[GlobalSearch] Failed to find results due to error: \(error)")
}
return .failure(error) return .failure(error)
} }
} }

View file

@ -283,14 +283,6 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
// Start polling if needed (i.e. if the user just created or restored their Session ID) // Start polling if needed (i.e. if the user just created or restored their Session ID)
if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate { if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.startPollersIfNeeded() appDelegate.startPollersIfNeeded()
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
if !SessionUtil.userConfigsEnabled {
// Do this only if we created a new Session ID, or if we already received the initial configuration message
if UserDefaults.standard[.hasSyncedInitialConfiguration] {
appDelegate.syncConfigurationIfNeeded()
}
}
} }
// Onion request path countries cache // Onion request path countries cache

View file

@ -6,6 +6,7 @@ import DifferenceKit
import SessionUIKit import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
class MessageRequestsViewController: BaseVC, SessionUtilRespondingViewController, UITableViewDelegate, UITableViewDataSource { class MessageRequestsViewController: BaseVC, SessionUtilRespondingViewController, UITableViewDelegate, UITableViewDataSource {
private static let loadingHeaderHeight: CGFloat = 40 private static let loadingHeaderHeight: CGFloat = 40

View file

@ -4,6 +4,7 @@ import Foundation
import GRDB import GRDB
import DifferenceKit import DifferenceKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
public class MessageRequestsViewModel { public class MessageRequestsViewModel {
public typealias SectionModel = ArraySection<Section, SessionThreadViewModel> public typealias SectionModel = ArraySection<Section, SessionThreadViewModel>

View file

@ -7,6 +7,7 @@ import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionSnodeKit
final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, QRScannerDelegate { final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, QRScannerDelegate {
private var shouldShowBackButton: Bool = true private var shouldShowBackButton: Bool = true

View file

@ -5,6 +5,7 @@ import MediaPlayer
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
// This kind of view is tricky. I've tried to organize things in the // This kind of view is tricky. I've tried to organize things in the
// simplest possible way. // simplest possible way.
@ -359,8 +360,7 @@ import SignalCoreKit
@objc func handlePinch(sender: UIPinchGestureRecognizer) { @objc func handlePinch(sender: UIPinchGestureRecognizer) {
switch sender.state { switch sender.state {
case .possible: case .possible: break
break
case .began: case .began:
srcTranslationAtPinchStart = srcTranslation srcTranslationAtPinchStart = srcTranslation
imageScaleAtPinchStart = imageScale imageScaleAtPinchStart = imageScale
@ -368,7 +368,7 @@ import SignalCoreKit
lastPinchLocation = lastPinchLocation =
sender.location(in: sender.view) sender.location(in: sender.view)
lastPinchScale = sender.scale lastPinchScale = sender.scale
break
case .changed, .ended: case .changed, .ended:
if sender.numberOfTouches > 1 { if sender.numberOfTouches > 1 {
let location = let location =
@ -402,11 +402,12 @@ import SignalCoreKit
lastPinchLocation = location lastPinchLocation = location
lastPinchScale = sender.scale lastPinchScale = sender.scale
} }
break
case .cancelled, .failed: case .cancelled, .failed:
srcTranslation = srcTranslationAtPinchStart srcTranslation = srcTranslationAtPinchStart
imageScale = imageScaleAtPinchStart imageScale = imageScaleAtPinchStart
break
@unknown default: break
} }
updateImageLayout() updateImageLayout()
@ -416,11 +417,10 @@ import SignalCoreKit
@objc func handlePan(sender: UIPanGestureRecognizer) { @objc func handlePan(sender: UIPanGestureRecognizer) {
switch sender.state { switch sender.state {
case .possible: case .possible: break
break
case .began: case .began:
srcTranslationAtPanStart = srcTranslation srcTranslationAtPanStart = srcTranslation
break
case .changed, .ended: case .changed, .ended:
let viewSizePoints = imageView.frame.size let viewSizePoints = imageView.frame.size
let srcCropSizePoints = CGSize(width: srcDefaultCropSizePoints.width / imageScale, let srcCropSizePoints = CGSize(width: srcDefaultCropSizePoints.width / imageScale,
@ -434,11 +434,11 @@ import SignalCoreKit
// Update translation. // Update translation.
srcTranslation = CGPoint(x: srcTranslationAtPanStart.x + gestureTranslation.x * -viewToSrcRatio, srcTranslation = CGPoint(x: srcTranslationAtPanStart.x + gestureTranslation.x * -viewToSrcRatio,
y: srcTranslationAtPanStart.y + gestureTranslation.y * -viewToSrcRatio) y: srcTranslationAtPanStart.y + gestureTranslation.y * -viewToSrcRatio)
break
case .cancelled, .failed: case .cancelled, .failed:
srcTranslation srcTranslation = srcTranslationAtPanStart
= srcTranslationAtPanStart
break @unknown default: break
} }
updateImageLayout() updateImageLayout()

View file

@ -7,6 +7,7 @@ import DifferenceKit
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
public class DocumentTileViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { public class DocumentTileViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
@ -46,6 +47,10 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate,
// MARK: - UI // MARK: - UI
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.isIPad {
return .all
}
return .allButUpsideDown return .allButUpsideDown
} }

View file

@ -5,6 +5,7 @@ import Combine
import YYImage import YYImage
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
class GifPickerCell: UICollectionViewCell { class GifPickerCell: UICollectionViewCell {

View file

@ -6,6 +6,7 @@ import Reachability
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUIKit import SessionUIKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollectionViewDataSource, UICollectionViewDelegate, GifPickerLayoutDelegate { class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollectionViewDataSource, UICollectionViewDelegate, GifPickerLayoutDelegate {

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
public class GiphyDownloader: ProxiedContentDownloader { public class GiphyDownloader: ProxiedContentDownloader {

View file

@ -6,6 +6,7 @@ import Photos
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
protocol ImagePickerGridControllerDelegate: AnyObject { protocol ImagePickerGridControllerDelegate: AnyObject {
func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController)
@ -155,6 +156,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
case .cancelled, .ended, .failed: case .cancelled, .ended, .failed:
collectionView.isUserInteractionEnabled = true collectionView.isUserInteractionEnabled = true
collectionView.isScrollEnabled = true collectionView.isScrollEnabled = true
@unknown default: break
} }
} }

View file

@ -6,6 +6,7 @@ import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionMessagingKit import SessionMessagingKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
public enum MediaGalleryOption { public enum MediaGalleryOption {
case sliderEnabled case sliderEnabled

View file

@ -44,6 +44,10 @@ class MediaGalleryNavigationController: UINavigationController {
// MARK: - Orientation // MARK: - Orientation
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.isIPad {
return .all
}
return .allButUpsideDown return .allButUpsideDown
} }

View file

@ -199,16 +199,18 @@ public class MediaGalleryViewModel {
} }
} }
public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable { public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable, ColumnExpressible {
fileprivate static let interactionIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionId.stringValue) public typealias Columns = CodingKeys
fileprivate static let interactionVariantKey: SQL = SQL(stringLiteral: CodingKeys.interactionVariant.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
fileprivate static let interactionAuthorIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionAuthorId.stringValue) case interactionId
fileprivate static let interactionTimestampMsKey: SQL = SQL(stringLiteral: CodingKeys.interactionTimestampMs.stringValue) case interactionVariant
fileprivate static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) case interactionAuthorId
fileprivate static let attachmentKey: SQL = SQL(stringLiteral: CodingKeys.attachment.stringValue) case interactionTimestampMs
fileprivate static let attachmentAlbumIndexKey: SQL = SQL(stringLiteral: CodingKeys.attachmentAlbumIndex.stringValue)
fileprivate static let attachmentString: String = CodingKeys.attachment.stringValue case rowId
case attachmentAlbumIndex
case attachment
}
public var id: String { attachment.id } public var id: String { attachment.id }
public var differenceIdentifier: String { attachment.id } public var differenceIdentifier: String { attachment.id }
@ -306,7 +308,7 @@ public class MediaGalleryViewModel {
let finalFilterSQL: SQL = { let finalFilterSQL: SQL = {
guard let customFilters: SQL = customFilters else { guard let customFilters: SQL = customFilters else {
return """ return """
WHERE \(attachment.alias[Column.rowID]) IN \(rowIds) WHERE \(attachment[.rowId]) IN \(rowIds)
""" """
} }
@ -318,14 +320,14 @@ public class MediaGalleryViewModel {
}() }()
let request: SQLRequest<Item> = """ let request: SQLRequest<Item> = """
SELECT SELECT
\(interaction[.id]) AS \(Item.interactionIdKey), \(interaction[.id]) AS \(Item.Columns.interactionId),
\(interaction[.variant]) AS \(Item.interactionVariantKey), \(interaction[.variant]) AS \(Item.Columns.interactionVariant),
\(interaction[.authorId]) AS \(Item.interactionAuthorIdKey), \(interaction[.authorId]) AS \(Item.Columns.interactionAuthorId),
\(interaction[.timestampMs]) AS \(Item.interactionTimestampMsKey), \(interaction[.timestampMs]) AS \(Item.Columns.interactionTimestampMs),
\(attachment.alias[Column.rowID]) AS \(Item.rowIdKey), \(attachment[.rowId]) AS \(Item.Columns.rowId),
\(interactionAttachment[.albumIndex]) AS \(Item.attachmentAlbumIndexKey), \(interactionAttachment[.albumIndex]) AS \(Item.Columns.attachmentAlbumIndex),
\(Item.attachmentKey).* \(attachment.allColumns)
FROM \(Attachment.self) FROM \(Attachment.self)
\(joinSQL) \(joinSQL)
\(finalFilterSQL) \(finalFilterSQL)
@ -338,8 +340,8 @@ public class MediaGalleryViewModel {
Attachment.numberOfSelectedColumns(db) Attachment.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(Item.self, [
Item.attachmentString: adapters[1] .attachment: adapters[1]
]) ])
} }
} }

View file

@ -6,6 +6,8 @@ import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
import SessionSnodeKit
class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, MediaDetailViewControllerDelegate, InteractivelyDismissableViewController { class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, MediaDetailViewControllerDelegate, InteractivelyDismissableViewController {
class DynamicallySizedView: UIView { class DynamicallySizedView: UIView {
@ -505,8 +507,9 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
dismissSelf(animated: true) dismissSelf(animated: true)
} }
@objc @objc public func didPressShare(_ sender: Any) { share() }
public func didPressShare(_ sender: Any) {
public func share(using dependencies: Dependencies = Dependencies()) {
guard let currentViewController = self.viewControllers?[0] as? MediaDetailViewController else { guard let currentViewController = self.viewControllers?[0] as? MediaDetailViewController else {
owsFailDebug("currentViewController was unexpectedly nil") owsFailDebug("currentViewController was unexpectedly nil")
return return
@ -553,7 +556,8 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
), ),
interactionId: nil, // Show no interaction for the current user interactionId: nil, // Show no interaction for the current user
threadId: threadId, threadId: threadId,
threadVariant: threadVariant threadVariant: threadVariant,
using: dependencies
) )
} }
} }

View file

@ -7,6 +7,7 @@ import DifferenceKit
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
public class MediaTileViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { public class MediaTileViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
@ -54,6 +55,10 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
// MARK: - UI // MARK: - UI
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.isIPad {
return .all
}
return .allButUpsideDown return .allButUpsideDown
} }

View file

@ -6,6 +6,7 @@ import AVFoundation
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
protocol PhotoCaptureViewControllerDelegate: AnyObject { protocol PhotoCaptureViewControllerDelegate: AnyObject {
func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment) func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment)

View file

@ -6,6 +6,7 @@ import Photos
import CoreServices import CoreServices
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
protocol PhotoLibraryDelegate: AnyObject { protocol PhotoLibraryDelegate: AnyObject {
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) func photoLibraryDidChange(_ photoLibrary: PhotoLibrary)

View file

@ -6,6 +6,7 @@ import Photos
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUIKit import SessionUIKit
import SessionUtilitiesKit
class SendMediaNavigationController: UINavigationController { class SendMediaNavigationController: UINavigationController {
public override var preferredStatusBarStyle: UIStatusBarStyle { public override var preferredStatusBarStyle: UIStatusBarStyle {
@ -17,12 +18,14 @@ class SendMediaNavigationController: UINavigationController {
static let bottomButtonsCenterOffset: CGFloat = -50 static let bottomButtonsCenterOffset: CGFloat = -50
private let threadId: String private let threadId: String
private let threadVariant: SessionThread.Variant
private var disposables: Set<AnyCancellable> = Set() private var disposables: Set<AnyCancellable> = Set()
// MARK: - Initialization // MARK: - Initialization
init(threadId: String) { init(threadId: String, threadVariant: SessionThread.Variant) {
self.threadId = threadId self.threadId = threadId
self.threadVariant = threadVariant
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
@ -73,17 +76,15 @@ class SendMediaNavigationController: UINavigationController {
public weak var sendMediaNavDelegate: SendMediaNavDelegate? public weak var sendMediaNavDelegate: SendMediaNavDelegate?
@objc public class func showingCameraFirst(threadId: String, threadVariant: SessionThread.Variant) -> SendMediaNavigationController {
public class func showingCameraFirst(threadId: String) -> SendMediaNavigationController { let navController = SendMediaNavigationController(threadId: threadId, threadVariant: threadVariant)
let navController = SendMediaNavigationController(threadId: threadId)
navController.viewControllers = [navController.captureViewController] navController.viewControllers = [navController.captureViewController]
return navController return navController
} }
@objc public class func showingMediaLibraryFirst(threadId: String, threadVariant: SessionThread.Variant) -> SendMediaNavigationController {
public class func showingMediaLibraryFirst(threadId: String) -> SendMediaNavigationController { let navController = SendMediaNavigationController(threadId: threadId, threadVariant: threadVariant)
let navController = SendMediaNavigationController(threadId: threadId)
navController.viewControllers = [navController.mediaLibraryViewController] navController.viewControllers = [navController.mediaLibraryViewController]
return navController return navController
@ -232,6 +233,7 @@ class SendMediaNavigationController: UINavigationController {
let approvalViewController = AttachmentApprovalViewController( let approvalViewController = AttachmentApprovalViewController(
mode: .sharedNavigation, mode: .sharedNavigation,
threadId: self.threadId, threadId: self.threadId,
threadVariant: self.threadVariant,
attachments: self.attachments attachments: self.attachments
) )
approvalViewController.approvalDelegate = self approvalViewController.approvalDelegate = self
@ -430,8 +432,22 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat
attachmentDraftCollection.remove(attachment: attachment) attachmentDraftCollection.remove(attachment: attachment)
} }
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) { func attachmentApproval(
sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: messageText) _ attachmentApproval: AttachmentApprovalViewController,
didApproveAttachments attachments: [SignalAttachment],
forThreadId threadId: String,
threadVariant: SessionThread.Variant,
messageText: String?,
using dependencies: Dependencies
) {
sendMediaNavDelegate?.sendMediaNav(
self,
didApproveAttachments: attachments,
forThreadId: threadId,
threadVariant: threadVariant,
messageText: messageText,
using: dependencies
)
} }
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) { func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
@ -539,8 +555,8 @@ private struct MediaLibrarySelection: Hashable, Equatable {
let asset: PHAsset let asset: PHAsset
let signalAttachmentPublisher: AnyPublisher<SignalAttachment, Error> let signalAttachmentPublisher: AnyPublisher<SignalAttachment, Error>
var hashValue: Int { func hash(into hasher: inout Hasher) {
return asset.hashValue asset.hash(into: &hasher)
} }
var publisher: AnyPublisher<MediaLibraryAttachment, Error> { var publisher: AnyPublisher<MediaLibraryAttachment, Error> {
@ -559,8 +575,8 @@ private struct MediaLibraryAttachment: Hashable, Equatable {
let asset: PHAsset let asset: PHAsset
let signalAttachment: SignalAttachment let signalAttachment: SignalAttachment
public var hashValue: Int { func hash(into hasher: inout Hasher) {
return asset.hashValue asset.hash(into: &hasher)
} }
public static func == (lhs: MediaLibraryAttachment, rhs: MediaLibraryAttachment) -> Bool { public static func == (lhs: MediaLibraryAttachment, rhs: MediaLibraryAttachment) -> Bool {
@ -764,7 +780,7 @@ private class DoneButton: UIView {
protocol SendMediaNavDelegate: AnyObject { protocol SendMediaNavDelegate: AnyObject {
func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController?) func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController?)
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, threadVariant: SessionThread.Variant, messageText: String?, using dependencies: Dependencies)
func sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String? func sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String?
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeMessageText newMessageText: String?) func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeMessageText newMessageText: String?)

View file

@ -9,6 +9,7 @@ import SessionMessagingKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionSnodeKit
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
@ -92,7 +93,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
} }
// No point continuing if we are running tests // No point continuing if we are running tests
guard !CurrentAppContext().isRunningTests else { return true } guard !SNUtilitiesKit.isRunningTests else { return true }
self.window = mainWindow self.window = mainWindow
CurrentAppContext().mainWindow = mainWindow CurrentAppContext().mainWindow = mainWindow
@ -212,7 +213,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
} }
func applicationDidBecomeActive(_ application: UIApplication) { func applicationDidBecomeActive(_ application: UIApplication) {
guard !CurrentAppContext().isRunningTests else { return } guard !SNUtilitiesKit.isRunningTests else { return }
UserDefaults.sharedLokiProject?[.isMainAppActive] = true UserDefaults.sharedLokiProject?[.isMainAppActive] = true
@ -248,7 +249,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if UIDevice.current.isIPad { if UIDevice.current.isIPad {
return .allButUpsideDown return .all
} }
return .portrait return .portrait
@ -314,7 +315,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
private func completePostMigrationSetup(calledFrom lifecycleMethod: LifecycleMethod, needsConfigSync: Bool) { private func completePostMigrationSetup(calledFrom lifecycleMethod: LifecycleMethod, needsConfigSync: Bool) {
SNLog("Migrations completed, performing setup and ensuring rootViewController") SNLog("Migrations completed, performing setup and ensuring rootViewController")
Configuration.performMainSetup() Configuration.performMainSetup()
JobRunner.add(executor: SyncPushTokensJob.self, for: .syncPushTokens) JobRunner.setExecutor(SyncPushTokensJob.self, for: .syncPushTokens)
// Setup the UI if needed, then trigger any post-UI setup actions // Setup the UI if needed, then trigger any post-UI setup actions
self.ensureRootViewController(calledFrom: lifecycleMethod) { [weak self] success in self.ensureRootViewController(calledFrom: lifecycleMethod) { [weak self] success in
@ -522,7 +523,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
startPollersIfNeeded() startPollersIfNeeded()
if CurrentAppContext().isMainApp { if CurrentAppContext().isMainApp {
syncConfigurationIfNeeded()
handleAppActivatedWithOngoingCallIfNeeded() handleAppActivatedWithOngoingCallIfNeeded()
} }
} }
@ -868,36 +868,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
presentingVC.present(callVC, animated: true, completion: nil) presentingVC.present(callVC, animated: true, completion: nil)
} }
// MARK: - Config Sync
func syncConfigurationIfNeeded() {
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
guard !SessionUtil.userConfigsEnabled else { return }
let lastSync: Date = (UserDefaults.standard[.lastConfigurationSync] ?? .distantPast)
guard Date().timeIntervalSince(lastSync) > (7 * 24 * 60 * 60) else { return } // Sync every 2 days
Storage.shared
.writeAsync(
updates: { db in
ConfigurationSyncJob.enqueue(db, publicKey: getUserHexEncodedPublicKey(db))
},
completion: { _, result in
switch result {
case .failure: break
case .success:
// Only update the 'lastConfigurationSync' timestamp if we have done the
// first sync (Don't want a new device config sync to override config
// syncs from other devices)
if UserDefaults.standard[.hasSyncedInitialConfiguration] {
UserDefaults.standard[.lastConfigurationSync] = Date()
}
}
}
)
}
} }
// MARK: - LifecycleMethod // MARK: - LifecycleMethod

View file

@ -1,8 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import Foundation
import SessionUtilitiesKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionMessagingKit
public class AppEnvironment { public class AppEnvironment {
@ -11,7 +13,7 @@ public class AppEnvironment {
public class var shared: AppEnvironment { public class var shared: AppEnvironment {
get { return _shared } get { return _shared }
set { set {
guard CurrentAppContext().isRunningTests else { guard SNUtilitiesKit.isRunningTests else {
owsFailDebug("Can only switch environments in tests.") owsFailDebug("Can only switch environments in tests.")
return return
} }

View file

@ -1,17 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import <SessionUtilitiesKit/AppContext.h>
NS_ASSUME_NONNULL_BEGIN
extern NSString *const ReportedApplicationStateDidChangeNotification;
@interface MainAppContext : NSObject <AppContext>
- (instancetype)init;
@end
NS_ASSUME_NONNULL_END

View file

@ -1,321 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "MainAppContext.h"
#import "Session-Swift.h"
#import <SignalCoreKit/OWSAsserts.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplicationStateDidChangeNotification";
@interface MainAppContext ()
@property (atomic) UIApplicationState reportedApplicationState;
@property (nonatomic, nullable) NSMutableArray<AppActiveBlock> *appActiveBlocks;
@end
#pragma mark -
@implementation MainAppContext
@synthesize mainWindow = _mainWindow;
@synthesize appLaunchTime = _appLaunchTime;
@synthesize wasWokenUpByPushNotification = _wasWokenUpByPushNotification;
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
self.reportedApplicationState = UIApplicationStateInactive;
_appLaunchTime = [NSDate new];
_wasWokenUpByPushNotification = false;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillEnterForeground:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillTerminate:)
name:UIApplicationWillTerminateNotification
object:nil];
// We can't use OWSSingletonAssert() since it uses the app context.
self.appActiveBlocks = [NSMutableArray new];
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Notifications
- (void)setReportedApplicationState:(UIApplicationState)reportedApplicationState
{
OWSAssertIsOnMainThread();
if (_reportedApplicationState == reportedApplicationState) {
return;
}
_reportedApplicationState = reportedApplicationState;
[[NSNotificationCenter defaultCenter] postNotificationName:ReportedApplicationStateDidChangeNotification
object:nil
userInfo:nil];
}
- (void)applicationWillEnterForeground:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
self.reportedApplicationState = UIApplicationStateInactive;
OWSLogInfo(@"");
[NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationWillEnterForegroundNotification object:nil];
}
- (void)applicationDidEnterBackground:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
self.reportedApplicationState = UIApplicationStateBackground;
OWSLogInfo(@"");
[DDLog flushLog];
[NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidEnterBackgroundNotification object:nil];
}
- (void)applicationWillResignActive:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
self.reportedApplicationState = UIApplicationStateInactive;
OWSLogInfo(@"");
[DDLog flushLog];
[NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationWillResignActiveNotification object:nil];
}
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
self.reportedApplicationState = UIApplicationStateActive;
OWSLogInfo(@"");
[NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidBecomeActiveNotification object:nil];
[self runAppActiveBlocks];
}
- (void)applicationWillTerminate:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
[DDLog flushLog];
}
#pragma mark -
- (BOOL)isMainApp
{
return YES;
}
- (BOOL)isMainAppAndActive
{
return [UIApplication sharedApplication].applicationState == UIApplicationStateActive;
}
- (BOOL)isShareExtension {
return NO;
}
- (BOOL)isRTL
{
// FIXME: We should try to remove this as we've had to add a hack to ensure the first call to this runs on the main thread
static BOOL isRTL = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
isRTL = [[UIApplication sharedApplication] userInterfaceLayoutDirection]
== UIUserInterfaceLayoutDirectionRightToLeft;
});
return isRTL;
}
- (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated
{
[[UIApplication sharedApplication] setStatusBarHidden:isHidden animated:isAnimated];
}
- (CGFloat)statusBarHeight
{
return [UIApplication sharedApplication].statusBarFrame.size.height;
}
- (BOOL)isInBackground
{
return self.reportedApplicationState == UIApplicationStateBackground;
}
- (BOOL)isAppForegroundAndActive
{
return self.reportedApplicationState == UIApplicationStateActive;
}
- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:
(BackgroundTaskExpirationHandler)expirationHandler
{
return [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:expirationHandler];
}
- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)backgroundTaskIdentifier
{
[UIApplication.sharedApplication endBackgroundTask:backgroundTaskIdentifier];
}
- (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray<id> *)blockingObjects
{
if (UIApplication.sharedApplication.isIdleTimerDisabled != shouldBeBlocking) {
if (shouldBeBlocking) {
NSMutableString *logString =
[NSMutableString stringWithFormat:@"Blocking sleep because of: %@", blockingObjects.firstObject];
if (blockingObjects.count > 1) {
[logString appendString:[NSString stringWithFormat:@"(and %lu others)", blockingObjects.count - 1]];
}
OWSLogInfo(@"%@", logString);
} else {
OWSLogInfo(@"Unblocking Sleep.");
}
}
UIApplication.sharedApplication.idleTimerDisabled = shouldBeBlocking;
}
- (void)setMainAppBadgeNumber:(NSInteger)value
{
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:value];
[[NSUserDefaults sharedLokiProject] setInteger:value forKey:@"currentBadgeNumber"];
[[NSUserDefaults sharedLokiProject] synchronize];
}
- (nullable UIViewController *)frontmostViewController
{
return UIApplication.sharedApplication.frontmostViewControllerIgnoringAlerts;
}
- (nullable UIAlertAction *)openSystemSettingsAction
{
return [UIAlertAction actionWithTitle:CommonStrings.openSettingsButton
accessibilityIdentifier:[NSString stringWithFormat:@"%@.%@", self.class, @"system_settings"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
[UIApplication.sharedApplication openSystemSettings];
}];
}
- (BOOL)isRunningTests
{
return (NSProcessInfo.processInfo.environment[@"XCTestConfigurationFilePath"] != nil);
}
- (void)setNetworkActivityIndicatorVisible:(BOOL)value
{
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:value];
}
#pragma mark -
- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block
{
OWSAssertDebug(block);
[Threading dispatchMainThreadSafe:^{
if (self.isMainAppAndActive) {
// App active blocks typically will be used to safely access the
// shared data container, so use a background task to protect this
// work.
OWSBackgroundTask *_Nullable backgroundTask =
[OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
block();
OWSAssertDebug(backgroundTask);
backgroundTask = nil;
return;
}
[self.appActiveBlocks addObject:block];
}];
}
- (void)runAppActiveBlocks
{
OWSAssertIsOnMainThread();
OWSAssertDebug(self.isMainAppAndActive);
// App active blocks typically will be used to safely access the
// shared data container, so use a background task to protect this
// work.
OWSBackgroundTask *_Nullable backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
NSArray<AppActiveBlock> *appActiveBlocks = [self.appActiveBlocks copy];
[self.appActiveBlocks removeAllObjects];
for (AppActiveBlock block in appActiveBlocks) {
block();
}
OWSAssertDebug(backgroundTask);
backgroundTask = nil;
}
- (NSString *)appDocumentDirectoryPath
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentDirectoryURL =
[[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
return [documentDirectoryURL path];
}
- (NSString *)appSharedDataDirectoryPath
{
NSURL *groupContainerDirectoryURL =
[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:SignalApplicationGroup];
return [groupContainerDirectoryURL path];
}
- (NSUserDefaults *)appUserDefaults
{
return [[NSUserDefaults alloc] initWithSuiteName:SignalApplicationGroup];
}
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,253 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SignalCoreKit
import SessionUtilitiesKit
final class MainAppContext: NSObject, AppContext {
var reportedApplicationState: UIApplication.State
let appLaunchTime = Date()
let isMainApp: Bool = true
var isMainAppAndActive: Bool { UIApplication.shared.applicationState == .active }
var isShareExtension: Bool = false
var appActiveBlocks: [AppActiveBlock] = []
var mainWindow: UIWindow?
var wasWokenUpByPushNotification: Bool = false
private static var _isRTL: Bool = {
return (UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft)
}()
var isRTL: Bool { return MainAppContext._isRTL }
var statusBarHeight: CGFloat { UIApplication.shared.statusBarFrame.size.height }
var openSystemSettingsAction: UIAlertAction? {
let result = UIAlertAction(
title: "OPEN_SETTINGS_BUTTON".localized(),
style: .default
) { _ in UIApplication.shared.openSystemSettings() }
result.accessibilityIdentifier = "\(type(of: self)).system_settings"
return result
}
// MARK: - Initialization
override init() {
self.reportedApplicationState = .inactive
super.init()
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillEnterForeground(notification:)),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidEnterBackground(notification:)),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillResignActive(notification:)),
name: UIApplication.willResignActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidBecomeActive(notification:)),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillTerminate(notification:)),
name: UIApplication.willTerminateNotification,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Notifications
@objc private func applicationWillEnterForeground(notification: NSNotification) {
AssertIsOnMainThread()
self.reportedApplicationState = .inactive
OWSLogger.info("")
NotificationCenter.default.post(
name: .OWSApplicationWillEnterForeground,
object: nil
)
}
@objc private func applicationDidEnterBackground(notification: NSNotification) {
AssertIsOnMainThread()
self.reportedApplicationState = .background
OWSLogger.info("")
DDLog.flushLog()
NotificationCenter.default.post(
name: .OWSApplicationDidEnterBackground,
object: nil
)
}
@objc private func applicationWillResignActive(notification: NSNotification) {
AssertIsOnMainThread()
self.reportedApplicationState = .inactive
OWSLogger.info("")
DDLog.flushLog()
NotificationCenter.default.post(
name: .OWSApplicationWillResignActive,
object: nil
)
}
@objc private func applicationDidBecomeActive(notification: NSNotification) {
AssertIsOnMainThread()
self.reportedApplicationState = .active
OWSLogger.info("")
NotificationCenter.default.post(
name: .OWSApplicationDidBecomeActive,
object: nil
)
self.runAppActiveBlocks()
}
@objc private func applicationWillTerminate(notification: NSNotification) {
AssertIsOnMainThread()
OWSLogger.info("")
DDLog.flushLog()
}
// MARK: - AppContext Functions
func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) {
UIApplication.shared.setStatusBarHidden(isHidden, with: (isAnimated ? .slide : .none))
}
func isAppForegroundAndActive() -> Bool {
return (reportedApplicationState == .active)
}
func isInBackground() -> Bool {
return (reportedApplicationState == .background)
}
func beginBackgroundTask(expirationHandler: @escaping BackgroundTaskExpirationHandler) -> UIBackgroundTaskIdentifier {
return UIApplication.shared.beginBackgroundTask(expirationHandler: expirationHandler)
}
func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) {
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjects: [Any]) {
if UIApplication.shared.isIdleTimerDisabled != shouldBeBlocking {
if shouldBeBlocking {
var logString: String = "Blocking sleep because of: \(String(describing: blockingObjects.first))"
if blockingObjects.count > 1 {
logString = "\(logString) (and \(blockingObjects.count - 1) others)"
}
OWSLogger.info(logString)
}
else {
OWSLogger.info("Unblocking Sleep.")
}
}
UIApplication.shared.isIdleTimerDisabled = shouldBeBlocking
}
func setMainAppBadgeNumber(_ value: Int) {
UIApplication.shared.applicationIconBadgeNumber = value
UserDefaults.sharedLokiProject?.setValue(value, forKey: "currentBadgeNumber")
}
func frontmostViewController() -> UIViewController? {
UIApplication.shared.frontmostViewControllerIgnoringAlerts
}
func setNetworkActivityIndicatorVisible(_ value: Bool) {
UIApplication.shared.isNetworkActivityIndicatorVisible = value
}
// MARK: -
func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) {
Threading.dispatchMainThreadSafe { [weak self] in
if self?.isMainAppAndActive == true {
// App active blocks typically will be used to safely access the
// shared data container, so use a background task to protect this
// work.
var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: #function)
block()
if backgroundTask != nil { backgroundTask = nil }
return
}
self?.appActiveBlocks.append(block)
}
}
func runAppActiveBlocks() {
// App active blocks typically will be used to safely access the
// shared data container, so use a background task to protect this
// work.
var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: #function)
let appActiveBlocks: [AppActiveBlock] = self.appActiveBlocks
self.appActiveBlocks.removeAll()
appActiveBlocks.forEach { $0() }
if backgroundTask != nil { backgroundTask = nil }
}
func appDocumentDirectoryPath() -> String {
let targetPath: String? = FileManager.default
.urls(
for: .documentDirectory,
in: .userDomainMask
)
.last?
.path
owsAssertDebug(targetPath != nil)
return (targetPath ?? "")
}
func appSharedDataDirectoryPath() -> String {
let targetPath: String? = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: UserDefaults.applicationGroup)?
.path
owsAssertDebug(targetPath != nil)
return (targetPath ?? "")
}
func appUserDefaults() -> UserDefaults {
owsAssertDebug(UserDefaults.sharedLokiProject != nil)
return (UserDefaults.sharedLokiProject ?? UserDefaults.standard)
}
}

View file

@ -140,6 +140,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<true/> <true/>

View file

@ -7,54 +7,12 @@
<key>PreferenceSpecifiers</key> <key>PreferenceSpecifiers</key>
<array> <array>
<dict> <dict>
<key>Type</key> <key>File</key>
<string>PSGroupSpecifier</string> <string>Pods-GlobalDependencies-Session-settings-metadata</string>
<key>Title</key> <key>Title</key>
<string>Group</string> <string>Acknowledgements</string>
</dict>
<dict>
<key>Type</key> <key>Type</key>
<string>PSTextFieldSpecifier</string> <string>PSChildPaneSpecifier</string>
<key>Title</key>
<string>Name</string>
<key>Key</key>
<string>name_preference</string>
<key>DefaultValue</key>
<string></string>
<key>IsSecure</key>
<false/>
<key>KeyboardType</key>
<string>Alphabet</string>
<key>AutocapitalizationType</key>
<string>None</string>
<key>AutocorrectionType</key>
<string>No</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Enabled</string>
<key>Key</key>
<string>enabled_preference</string>
<key>DefaultValue</key>
<true/>
</dict>
<dict>
<key>Type</key>
<string>PSSliderSpecifier</string>
<key>Key</key>
<string>slider_preference</string>
<key>DefaultValue</key>
<real>0.5</real>
<key>MinimumValue</key>
<integer>0</integer>
<key>MaximumValue</key>
<integer>1</integer>
<key>MinimumValueImage</key>
<string></string>
<key>MaximumValueImage</key>
<string></string>
</dict> </dict>
</array> </array>
</dict> </dict>

View file

@ -7,4 +7,3 @@
#import "OWSBezierPathView.h" #import "OWSBezierPathView.h"
#import "OWSMessageTimerView.h" #import "OWSMessageTimerView.h"
#import "OWSWindowManager.h" #import "OWSWindowManager.h"
#import "MainAppContext.h"

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Bildschirmschutz"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Bildschirmschutz";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Lesebestätigungen"; "PRIVACY_SECTION_READ_RECEIPTS" = "Lesebestätigungen";
"PRIVACY_READ_RECEIPTS_TITLE" = "Lesebestätigungen"; "PRIVACY_READ_RECEIPTS_TITLE" = "Lesebestätigungen";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Protección de pantalla"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Protección de pantalla";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Notificaciones de lectura"; "PRIVACY_SECTION_READ_RECEIPTS" = "Notificaciones de lectura";
"PRIVACY_READ_RECEIPTS_TITLE" = "Notificaciones de lectura"; "PRIVACY_READ_RECEIPTS_TITLE" = "Notificaciones de lectura";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "امنیت صفحه"; "PRIVACY_SECTION_SCREEN_SECURITY" = "امنیت صفحه";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "قفل Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "قفل Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = " برای باز کردن قفل Session به شناسه لمسی، شناسه صورت و یا رمز عبوری ضرورت است."; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = " برای باز کردن قفل Session به شناسه لمسی، شناسه صورت و یا رمز عبوری ضرورت است.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "رسیدهای خواندن"; "PRIVACY_SECTION_READ_RECEIPTS" = "رسیدهای خواندن";
"PRIVACY_READ_RECEIPTS_TITLE" = "رسیدهای خواندن"; "PRIVACY_READ_RECEIPTS_TITLE" = "رسیدهای خواندن";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "رسیدهای خواندن در چت‌های یک به یک روان شود."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "رسیدهای خواندن در چت‌های یک به یک روان شود.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Näytön suojaus"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Näytön suojaus";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Lukukuittaukset"; "PRIVACY_SECTION_READ_RECEIPTS" = "Lukukuittaukset";
"PRIVACY_READ_RECEIPTS_TITLE" = "Lukukuittaukset"; "PRIVACY_READ_RECEIPTS_TITLE" = "Lukukuittaukset";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Sécurité de lécran"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Sécurité de lécran";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Verrouiller Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Verrouiller Session";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Requiert Touch ID, Face ID ou votre code pour déverrouiller Session."; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Requiert Touch ID, Face ID ou votre code pour déverrouiller Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Accusés de lecture"; "PRIVACY_SECTION_READ_RECEIPTS" = "Accusés de lecture";
"PRIVACY_READ_RECEIPTS_TITLE" = "Accusés de lecture"; "PRIVACY_READ_RECEIPTS_TITLE" = "Accusés de lecture";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Envoyer un accusé réception dans les conversations 1 à 1."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Envoyer un accusé réception dans les conversations 1 à 1.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Sigurnost zaslona"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Sigurnost zaslona";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Potvrda o čitanju"; "PRIVACY_SECTION_READ_RECEIPTS" = "Potvrda o čitanju";
"PRIVACY_READ_RECEIPTS_TITLE" = "Potvrda o čitanju"; "PRIVACY_READ_RECEIPTS_TITLE" = "Potvrda o čitanju";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Layar Aman"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Layar Aman";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Pesan terbaca diterima"; "PRIVACY_SECTION_READ_RECEIPTS" = "Pesan terbaca diterima";
"PRIVACY_READ_RECEIPTS_TITLE" = "Pesan terbaca diterima"; "PRIVACY_READ_RECEIPTS_TITLE" = "Pesan terbaca diterima";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Sicurezza schermo"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Sicurezza schermo";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Ricevute di lettura"; "PRIVACY_SECTION_READ_RECEIPTS" = "Ricevute di lettura";
"PRIVACY_READ_RECEIPTS_TITLE" = "Ricevute di lettura"; "PRIVACY_READ_RECEIPTS_TITLE" = "Ricevute di lettura";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "スクリーン・セキュリティ"; "PRIVACY_SECTION_SCREEN_SECURITY" = "スクリーン・セキュリティ";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "既読確認"; "PRIVACY_SECTION_READ_RECEIPTS" = "既読確認";
"PRIVACY_READ_RECEIPTS_TITLE" = "既読確認"; "PRIVACY_READ_RECEIPTS_TITLE" = "既読確認";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Scherm beveiliging"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Scherm beveiliging";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Leesbevestigingen"; "PRIVACY_SECTION_READ_RECEIPTS" = "Leesbevestigingen";
"PRIVACY_READ_RECEIPTS_TITLE" = "Leesbevestigingen"; "PRIVACY_READ_RECEIPTS_TITLE" = "Leesbevestigingen";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Ochrona ekranu"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Ochrona ekranu";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Potwierdzenia odczytania"; "PRIVACY_SECTION_READ_RECEIPTS" = "Potwierdzenia odczytania";
"PRIVACY_READ_RECEIPTS_TITLE" = "Potwierdzenia odczytania"; "PRIVACY_READ_RECEIPTS_TITLE" = "Potwierdzenia odczytania";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Segurança de Tela"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Segurança de Tela";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Confirmações de Leitura"; "PRIVACY_SECTION_READ_RECEIPTS" = "Confirmações de Leitura";
"PRIVACY_READ_RECEIPTS_TITLE" = "Confirmações de Leitura"; "PRIVACY_READ_RECEIPTS_TITLE" = "Confirmações de Leitura";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Защита экрана"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Защита экрана";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Уведомления о прочтении"; "PRIVACY_SECTION_READ_RECEIPTS" = "Уведомления о прочтении";
"PRIVACY_READ_RECEIPTS_TITLE" = "Уведомления о прочтении"; "PRIVACY_READ_RECEIPTS_TITLE" = "Уведомления о прочтении";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "තිරයේ ආරක්ෂාව"; "PRIVACY_SECTION_SCREEN_SECURITY" = "තිරයේ ආරක්ෂාව";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "කියවූ බවට ලදුපත්"; "PRIVACY_SECTION_READ_RECEIPTS" = "කියවූ බවට ලදුපත්";
"PRIVACY_READ_RECEIPTS_TITLE" = "කියවූ බවට ලදුපත්"; "PRIVACY_READ_RECEIPTS_TITLE" = "කියවූ බවට ලදුපත්";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Zabezpečenie obrazovky"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Zabezpečenie obrazovky";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Potvrdenia o prečítaní"; "PRIVACY_SECTION_READ_RECEIPTS" = "Potvrdenia o prečítaní";
"PRIVACY_READ_RECEIPTS_TITLE" = "Potvrdenia o prečítaní"; "PRIVACY_READ_RECEIPTS_TITLE" = "Potvrdenia o prečítaní";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Skärmsäkerhet"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Skärmsäkerhet";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Läskvittenser"; "PRIVACY_SECTION_READ_RECEIPTS" = "Läskvittenser";
"PRIVACY_READ_RECEIPTS_TITLE" = "Läskvittenser"; "PRIVACY_READ_RECEIPTS_TITLE" = "Läskvittenser";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "ความปลอดภัยหน้าจอ"; "PRIVACY_SECTION_SCREEN_SECURITY" = "ความปลอดภัยหน้าจอ";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "แจ้งการอ่านข้อความ"; "PRIVACY_SECTION_READ_RECEIPTS" = "แจ้งการอ่านข้อความ";
"PRIVACY_READ_RECEIPTS_TITLE" = "แจ้งการอ่านข้อความ"; "PRIVACY_READ_RECEIPTS_TITLE" = "แจ้งการอ่านข้อความ";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "螢幕顯示安全"; "PRIVACY_SECTION_SCREEN_SECURITY" = "螢幕顯示安全";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "已讀回條"; "PRIVACY_SECTION_READ_RECEIPTS" = "已讀回條";
"PRIVACY_READ_RECEIPTS_TITLE" = "已讀回條"; "PRIVACY_READ_RECEIPTS_TITLE" = "已讀回條";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -483,6 +483,9 @@
"PRIVACY_SECTION_SCREEN_SECURITY" = "屏幕安全"; "PRIVACY_SECTION_SCREEN_SECURITY" = "屏幕安全";
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "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_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session.";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SECTION_READ_RECEIPTS" = "已读回执"; "PRIVACY_SECTION_READ_RECEIPTS" = "已读回执";
"PRIVACY_READ_RECEIPTS_TITLE" = "已读回执"; "PRIVACY_READ_RECEIPTS_TITLE" = "已读回执";
"PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats."; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "Send read receipts in one-to-one chats.";
@ -645,6 +648,8 @@
"CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!";
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
"COMMUNITY_MESSAGE_REQUEST_DISABLED_EMPTY_STATE" = "%@ has message requests from Community conversations turned off, so you cannot send them a message.";
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated."; "USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
"LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue."; "LOAD_RECOVERY_PASSWORD_ERROR" = "An error occurred when trying to load your recovery password.\n\nPlease export your logs, then upload the file though Session's Help Desk to help resolve this issue.";
"FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages."; "FAILED_TO_STORE_OUTGOING_MESSAGE" = "An error occurred when trying to store the outgoing message for sending, you may need to restart the app before you can send messages.";
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";

View file

@ -6,6 +6,8 @@ import GRDB
import SessionMessagingKit import SessionMessagingKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
import SessionSnodeKit
/// There are two primary components in our system notification integration: /// There are two primary components in our system notification integration:
/// ///
@ -553,7 +555,8 @@ class NotificationActionHandler {
func reply( func reply(
userInfo: [AnyHashable: Any], userInfo: [AnyHashable: Any],
replyText: String, replyText: String,
applicationState: UIApplication.State applicationState: UIApplication.State,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> { ) -> AnyPublisher<Void, Error> {
guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else { guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else {
return Fail<Void, Error>(error: NotificationError.failDebug("threadId was unexpectedly nil")) return Fail<Void, Error>(error: NotificationError.failDebug("threadId was unexpectedly nil"))
@ -599,10 +602,11 @@ class NotificationActionHandler {
db, db,
interaction: interaction, interaction: interaction,
threadId: threadId, threadId: threadId,
threadVariant: thread.variant threadVariant: thread.variant,
using: dependencies
) )
} }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) } .flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.handleEvents( .handleEvents(
receiveCompletion: { result in receiveCompletion: { result in
switch result { switch result {

View file

@ -4,8 +4,10 @@ import Foundation
import Combine import Combine
import PushKit import PushKit
import GRDB import GRDB
import SessionMessagingKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SignalCoreKit import SignalCoreKit
import SessionUtilitiesKit
public enum PushRegistrationError: Error { public enum PushRegistrationError: Error {
case assertionError(description: String) case assertionError(description: String)
@ -53,8 +55,6 @@ public enum PushRegistrationError: Error {
Logger.info("") Logger.info("")
return registerUserNotificationSettings() return registerUserNotificationSettings()
.subscribe(on: DispatchQueue.global(qos: .default))
.receive(on: DispatchQueue.main) // MUST be on main thread
.setFailureType(to: Error.self) .setFailureType(to: Error.self)
.tryFlatMap { _ -> AnyPublisher<(pushToken: String, voipToken: String), Error> in .tryFlatMap { _ -> AnyPublisher<(pushToken: String, voipToken: String), Error> in
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
@ -75,25 +75,28 @@ public enum PushRegistrationError: Error {
// MARK: Vanilla push token // MARK: Vanilla push token
// Vanilla push token is obtained from the system via AppDelegate // Vanilla push token is obtained from the system via AppDelegate
public func didReceiveVanillaPushToken(_ tokenData: Data) { public func didReceiveVanillaPushToken(_ tokenData: Data, using dependencies: Dependencies = Dependencies()) {
guard let vanillaTokenResolver = self.vanillaTokenResolver else { guard let vanillaTokenResolver = self.vanillaTokenResolver else {
owsFailDebug("publisher completion in \(#function) unexpectedly nil") owsFailDebug("publisher completion in \(#function) unexpectedly nil")
return return
} }
DispatchQueue.global(qos: .default).async(using: dependencies) {
vanillaTokenResolver(Result.success(tokenData)) vanillaTokenResolver(Result.success(tokenData))
} }
}
// Vanilla push token is obtained from the system via AppDelegate // Vanilla push token is obtained from the system via AppDelegate
@objc public func didFailToReceiveVanillaPushToken(error: Error, using dependencies: Dependencies = Dependencies()) {
public func didFailToReceiveVanillaPushToken(error: Error) {
guard let vanillaTokenResolver = self.vanillaTokenResolver else { guard let vanillaTokenResolver = self.vanillaTokenResolver else {
owsFailDebug("publisher completion in \(#function) unexpectedly nil") owsFailDebug("publisher completion in \(#function) unexpectedly nil")
return return
} }
DispatchQueue.global(qos: .default).async(using: dependencies) {
vanillaTokenResolver(Result.failure(error)) vanillaTokenResolver(Result.failure(error))
} }
}
// MARK: helpers // MARK: helpers
@ -111,9 +114,8 @@ public enum PushRegistrationError: Error {
* in this case we've verified that we *have* properly registered notification settings. * in this case we've verified that we *have* properly registered notification settings.
*/ */
private var isSusceptibleToFailedPushRegistration: Bool { private var isSusceptibleToFailedPushRegistration: Bool {
// Only affects users who have disabled both: background refresh *and* notifications // Only affects users who have disabled both: background refresh *and* notifications
guard UIApplication.shared.backgroundRefreshStatus == .denied else { guard DispatchQueue.main.sync(execute: { UIApplication.shared.backgroundRefreshStatus }) == .denied else {
return false return false
} }
@ -128,10 +130,7 @@ public enum PushRegistrationError: Error {
return true return true
} }
// FIXME: Might be nice to try to avoid having this required to run on the main thread (follow a similar approach to the 'SyncPushTokensJob' & `Atomic<T>`?)
private func registerForVanillaPushToken() -> AnyPublisher<String, Error> { private func registerForVanillaPushToken() -> AnyPublisher<String, Error> {
AssertIsOnMainThread()
// Use the existing publisher if it exists // Use the existing publisher if it exists
if let vanillaTokenPublisher: AnyPublisher<Data, Error> = self.vanillaTokenPublisher { if let vanillaTokenPublisher: AnyPublisher<Data, Error> = self.vanillaTokenPublisher {
return vanillaTokenPublisher return vanillaTokenPublisher
@ -139,19 +138,23 @@ public enum PushRegistrationError: Error {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
UIApplication.shared.registerForRemoteNotifications()
// No pending vanilla token yet; create a new publisher // No pending vanilla token yet; create a new publisher
let publisher: AnyPublisher<Data, Error> = Deferred { let publisher: AnyPublisher<Data, Error> = Deferred {
Future<Data, Error> { self.vanillaTokenResolver = $0 } Future<Data, Error> {
self.vanillaTokenResolver = $0
// Tell the device to register for remote notifications
DispatchQueue.main.sync { UIApplication.shared.registerForRemoteNotifications() }
} }
}
.shareReplay(1)
.eraseToAnyPublisher() .eraseToAnyPublisher()
self.vanillaTokenPublisher = publisher self.vanillaTokenPublisher = publisher
return publisher return publisher
.timeout( .timeout(
.seconds(10), .seconds(10),
scheduler: DispatchQueue.main, scheduler: DispatchQueue.global(qos: .default),
customError: { PushRegistrationError.timeout } customError: { PushRegistrationError.timeout }
) )
.catch { error -> AnyPublisher<Data, Error> in .catch { error -> AnyPublisher<Data, Error> in
@ -200,9 +203,8 @@ public enum PushRegistrationError: Error {
} }
public func createVoipRegistryIfNecessary() { public func createVoipRegistryIfNecessary() {
AssertIsOnMainThread()
guard voipRegistry == nil else { return } guard voipRegistry == nil else { return }
let voipRegistry = PKPushRegistry(queue: nil) let voipRegistry = PKPushRegistry(queue: nil)
self.voipRegistry = voipRegistry self.voipRegistry = voipRegistry
voipRegistry.desiredPushTypes = [.voIP] voipRegistry.desiredPushTypes = [.voIP]
@ -210,8 +212,6 @@ public enum PushRegistrationError: Error {
} }
private func registerForVoipPushToken() -> AnyPublisher<String?, Error> { private func registerForVoipPushToken() -> AnyPublisher<String?, Error> {
AssertIsOnMainThread()
// Use the existing publisher if it exists // Use the existing publisher if it exists
if let voipTokenPublisher: AnyPublisher<Data?, Error> = self.voipTokenPublisher { if let voipTokenPublisher: AnyPublisher<Data?, Error> = self.voipTokenPublisher {
return voipTokenPublisher return voipTokenPublisher

View file

@ -17,37 +17,18 @@ public enum SyncPushTokensJob: JobExecutor {
public static func run( public static func run(
_ job: Job, _ job: Job,
queue: DispatchQueue, queue: DispatchQueue,
success: @escaping (Job, Bool) -> (), success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool) -> (), failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job) -> () deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies = Dependencies()
) { ) {
// Don't run when inactive or not in main app or if the user doesn't exist yet // Don't run when inactive or not in main app or if the user doesn't exist yet
guard (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else { guard (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else {
return deferred(job) // Don't need to do anything if it's not the main app return deferred(job, dependencies) // Don't need to do anything if it's not the main app
} }
guard Identity.userCompletedRequiredOnboarding() else { guard Identity.userCompletedRequiredOnboarding() else {
SNLog("[SyncPushTokensJob] Deferred due to incomplete registration") SNLog("[SyncPushTokensJob] Deferred due to incomplete registration")
return deferred(job) return deferred(job, dependencies)
}
// We need to check a UIApplication setting which needs to run on the main thread so synchronously
// retrieve the value so we can continue
let isRegisteredForRemoteNotifications: Bool = {
guard !Thread.isMainThread else {
return UIApplication.shared.isRegisteredForRemoteNotifications
}
return DispatchQueue.main.sync {
return UIApplication.shared.isRegisteredForRemoteNotifications
}
}()
// Apple's documentation states that we should re-register for notifications on every launch:
// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1
guard job.behaviour == .runOnce || !isRegisteredForRemoteNotifications else {
SNLog("[SyncPushTokensJob] Deferred due to Fast Mode disabled")
deferred(job) // Don't need to do anything if push notifications are already registered
return
} }
// Determine if the device has 'Fast Mode' (APNS) enabled // Determine if the device has 'Fast Mode' (APNS) enabled
@ -56,33 +37,33 @@ public enum SyncPushTokensJob: JobExecutor {
// If the job is running and 'Fast Mode' is disabled then we should try to unregister the existing // If the job is running and 'Fast Mode' is disabled then we should try to unregister the existing
// token // token
guard isUsingFullAPNs else { guard isUsingFullAPNs else {
Just(Storage.shared[.lastRecordedPushToken]) Just(dependencies.storage[.lastRecordedPushToken])
.setFailureType(to: Error.self) .setFailureType(to: Error.self)
.flatMap { lastRecordedPushToken in .flatMap { lastRecordedPushToken -> AnyPublisher<Void, Error> in
if let existingToken: String = lastRecordedPushToken {
SNLog("[SyncPushTokensJob] Unregister using last recorded push token: \(redact(existingToken))")
return Just(existingToken)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
SNLog("[SyncPushTokensJob] Unregister using live token provided from device")
return PushRegistrationManager.shared.requestPushTokens()
.map { token, _ in token }
.eraseToAnyPublisher()
}
.flatMap { pushToken in PushNotificationAPI.unregister(Data(hex: pushToken)) }
.map {
// Tell the device to unregister for remote notifications (essentially try to invalidate // Tell the device to unregister for remote notifications (essentially try to invalidate
// the token if needed // the token if needed - we do this first to avoid wrid race conditions which could be
// triggered by the user immediately re-registering)
DispatchQueue.main.sync { UIApplication.shared.unregisterForRemoteNotifications() } DispatchQueue.main.sync { UIApplication.shared.unregisterForRemoteNotifications() }
Storage.shared.write { db in // Clear the old token
dependencies.storage.write(using: dependencies) { db in
db[.lastRecordedPushToken] = nil db[.lastRecordedPushToken] = nil
} }
return ()
// Unregister from our server
if let existingToken: String = lastRecordedPushToken {
SNLog("[SyncPushTokensJob] Unregister using last recorded push token: \(redact(existingToken))")
return PushNotificationAPI.unsubscribe(token: Data(hex: existingToken))
.map { _ in () }
.eraseToAnyPublisher()
} }
.subscribe(on: queue)
SNLog("[SyncPushTokensJob] No previous token stored just triggering device unregister")
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.subscribe(on: queue, using: dependencies)
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in
switch result { switch result {
@ -91,23 +72,26 @@ public enum SyncPushTokensJob: JobExecutor {
} }
// We want to complete this job regardless of success or failure // We want to complete this job regardless of success or failure
success(job, false) success(job, false, dependencies)
} }
) )
return return
} }
// Perform device registration /// Perform device registration
///
/// **Note:** Apple's documentation states that we should re-register for notifications on every launch:
/// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1
Logger.info("Re-registering for remote notifications.") Logger.info("Re-registering for remote notifications.")
PushRegistrationManager.shared.requestPushTokens() PushRegistrationManager.shared.requestPushTokens()
.flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<Void, Error> in .flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<Void, Error> in
PushNotificationAPI PushNotificationAPI
.register( .subscribe(
with: Data(hex: pushToken), token: Data(hex: pushToken),
publicKey: getUserHexEncodedPublicKey(), isForcedUpdate: true,
isForcedUpdate: true using: dependencies
) )
.retry(3) .retry(3, using: dependencies)
.handleEvents( .handleEvents(
receiveCompletion: { result in receiveCompletion: { result in
switch result { switch result {
@ -117,9 +101,9 @@ public enum SyncPushTokensJob: JobExecutor {
case .finished: case .finished:
Logger.warn("Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))") Logger.warn("Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))")
SNLog("[SyncPushTokensJob] Completed") SNLog("[SyncPushTokensJob] Completed")
UserDefaults.standard[.lastPushNotificationSync] = Date() dependencies.standardUserDefaults[.lastPushNotificationSync] = dependencies.dateNow
Storage.shared.write { db in dependencies.storage.write(using: dependencies) { db in
db[.lastRecordedPushToken] = pushToken db[.lastRecordedPushToken] = pushToken
db[.lastRecordedVoipToken] = voipToken db[.lastRecordedVoipToken] = voipToken
} }
@ -129,10 +113,10 @@ public enum SyncPushTokensJob: JobExecutor {
.map { _ in () } .map { _ in () }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
.subscribe(on: queue) .subscribe(on: queue, using: dependencies)
.sinkUntilComplete( .sinkUntilComplete(
// We want to complete this job regardless of success or failure // We want to complete this job regardless of success or failure
receiveCompletion: { _ in success(job, false) } receiveCompletion: { _ in success(job, false, dependencies) }
) )
} }
@ -149,9 +133,9 @@ public enum SyncPushTokensJob: JobExecutor {
SyncPushTokensJob.run( SyncPushTokensJob.run(
job, job,
queue: DispatchQueue.global(qos: .default), queue: DispatchQueue.global(qos: .default),
success: { _, _ in }, success: { _, _, _ in },
failure: { _, _, _ in }, failure: { _, _, _, _ in },
deferred: { _ in } deferred: { _, _ in }
) )
} }
} }
@ -167,5 +151,9 @@ extension SyncPushTokensJob {
// MARK: - Convenience // MARK: - Convenience
private func redact(_ string: String) -> String { private func redact(_ string: String) -> String {
return OWSIsDebugBuild() ? string : "[ READACTED \(string.prefix(2))...\(string.suffix(2)) ]" #if DEBUG
return string
#else
return "[ READACTED \(string.prefix(2))...\(string.suffix(2)) ]"
#endif
} }

View file

@ -6,6 +6,7 @@ import UserNotifications
import SessionMessagingKit import SessionMessagingKit
import SignalCoreKit import SignalCoreKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
class UserNotificationConfig { class UserNotificationConfig {

View file

@ -26,14 +26,10 @@ enum Onboarding {
return existingPublisher return existingPublisher
} }
private static func createProfileNameRetrievalPublisher(_ requestId: UUID) -> AnyPublisher<String?, Error> { private static func createProfileNameRetrievalPublisher(
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent _ requestId: UUID,
guard SessionUtil.userConfigsEnabled else { using dependencies: Dependencies = Dependencies()
return Just(nil) ) -> AnyPublisher<String?, Error> {
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
let userPublicKey: String = getUserHexEncodedPublicKey() let userPublicKey: String = getUserHexEncodedPublicKey()
return SnodeAPI.getSwarm(for: userPublicKey) return SnodeAPI.getSwarm(for: userPublicKey)
@ -99,7 +95,8 @@ enum Onboarding {
) )
}(), }(),
sentTimestamp: TimeInterval((message.sentTimestamp ?? 0) / 1000), sentTimestamp: TimeInterval((message.sentTimestamp ?? 0) / 1000),
calledFromConfigHandling: false calledFromConfigHandling: false,
using: dependencies
) )
} }
return () return ()
@ -254,9 +251,9 @@ enum Onboarding {
// Notify the app that registration is complete // Notify the app that registration is complete
Identity.didRegister() Identity.didRegister()
// Now that we have registered get the Snode pool and sync push tokens // Now that we have registered get the Snode pool (just in case) - other non-blocking
// launch jobs will automatically be run because the app activation was triggered
GetSnodePoolJob.run() GetSnodePoolJob.run()
SyncPushTokensJob.run(uploadOnlyIfStale: false)
} }
} }
} }

View file

@ -6,6 +6,7 @@ import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SessionSnodeKit import SessionSnodeKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
final class PNModeVC: BaseVC, OptionViewDelegate { final class PNModeVC: BaseVC, OptionViewDelegate {
private let flow: Onboarding.Flow private let flow: Onboarding.Flow

View file

@ -4,6 +4,7 @@ import UIKit
import Sodium import Sodium
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
final class RegisterVC : BaseVC { final class RegisterVC : BaseVC {
private var seed: Data! { didSet { updateKeyPair() } } private var seed: Data! { didSet { updateKeyPair() } }

View file

@ -3,6 +3,7 @@
import UIKit import UIKit
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
final class AppearanceViewController: BaseVC { final class AppearanceViewController: BaseVC {
// MARK: - Components // MARK: - Components

View file

@ -6,6 +6,7 @@ import GRDB
import DifferenceKit import DifferenceKit
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsViewModel.Section, Profile> { class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsViewModel.Section, Profile> {
// MARK: - Section // MARK: - Section
@ -257,11 +258,12 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
// MARK: - DataModel // MARK: - DataModel
public struct DataModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { public struct DataModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible {
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) public typealias Columns = CodingKeys
public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case rowId
public static let profileString: String = CodingKeys.profile.stringValue case profile
}
public var differenceIdentifier: String { profile.id } public var differenceIdentifier: String { profile.id }
public var id: String { profile.id } public var id: String { profile.id }
@ -285,11 +287,11 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
let request: SQLRequest<DataModel> = """ let request: SQLRequest<DataModel> = """
SELECT SELECT
\(profile.alias[Column.rowID]) AS \(DataModel.rowIdKey), \(profile[.rowId]) AS \(DataModel.Columns.rowId),
\(DataModel.profileKey).* \(profile.allColumns)
FROM \(Profile.self) FROM \(Profile.self)
WHERE \(profile.alias[Column.rowID]) IN \(rowIds) WHERE \(profile[.rowId]) IN \(rowIds)
ORDER BY \(orderSQL) ORDER BY \(orderSQL)
""" """
@ -299,8 +301,8 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
Profile.numberOfSelectedColumns(db) Profile.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(DataModel.self, [
DataModel.profileString: adapters[1] .profile: adapters[1]
]) ])
} }
} }

View file

@ -33,6 +33,11 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
// MARK: - Content // MARK: - Content
private struct State: Equatable {
let trimOpenGroupMessagesOlderThanSixMonths: Bool
let shouldAutoPlayConsecutiveAudioMessages: Bool
}
override var title: String { "CONVERSATION_SETTINGS_TITLE".localized() } override var title: String { "CONVERSATION_SETTINGS_TITLE".localized() }
public override var observableTableData: ObservableData { _observableTableData } public override var observableTableData: ObservableData { _observableTableData }
@ -45,7 +50,17 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) /// 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 /// 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 _observableTableData: ObservableData = ValueObservation private lazy var _observableTableData: ObservableData = ValueObservation
.trackingConstantRegion { db -> [SectionModel] in .trackingConstantRegion { [weak self] db -> State in
State(
trimOpenGroupMessagesOlderThanSixMonths: db[.trimOpenGroupMessagesOlderThanSixMonths],
shouldAutoPlayConsecutiveAudioMessages: db[.shouldAutoPlayConsecutiveAudioMessages]
)
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[ConversationSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.withPrevious()
.map { (previous: State?, current: State) -> [SectionModel] in
return [ return [
SectionModel( SectionModel(
model: .messageTrimming, model: .messageTrimming,
@ -55,7 +70,11 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
title: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE".localized(), title: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE".localized(),
subtitle: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION".localized(), subtitle: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION".localized(),
rightAccessory: .toggle( rightAccessory: .toggle(
.settingBool(key: .trimOpenGroupMessagesOlderThanSixMonths) .boolValue(
key: .trimOpenGroupMessagesOlderThanSixMonths,
value: current.trimOpenGroupMessagesOlderThanSixMonths,
oldValue: (previous ?? current).trimOpenGroupMessagesOlderThanSixMonths
)
), ),
onTap: { onTap: {
Storage.shared.write { db in Storage.shared.write { db in
@ -73,7 +92,11 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
title: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE".localized(), title: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE".localized(),
subtitle: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION".localized(), subtitle: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION".localized(),
rightAccessory: .toggle( rightAccessory: .toggle(
.settingBool(key: .shouldAutoPlayConsecutiveAudioMessages) .boolValue(
key: .shouldAutoPlayConsecutiveAudioMessages,
value: current.shouldAutoPlayConsecutiveAudioMessages,
oldValue: (previous ?? current).shouldAutoPlayConsecutiveAudioMessages
)
), ),
onTap: { onTap: {
Storage.shared.write { db in Storage.shared.write { db in
@ -103,8 +126,5 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
) )
] ]
} }
.removeDuplicates()
.handleEvents(didFail: { SNLog("[ConversationSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)
} }

View file

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import Foundation
import SessionUtilitiesKit
class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigationControllerDelegate { class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigationControllerDelegate {
private let onTransition: (UIViewController, TransitionType) -> Void private let onTransition: (UIViewController, TransitionType) -> Void

View file

@ -7,7 +7,7 @@ import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit import SessionUtilitiesKit
class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSettingsViewModel.Section, NotificationSettingsViewModel.Setting> { class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSettingsViewModel.Section, NotificationSettingsViewModel.Item> {
// MARK: - Config // MARK: - Config
public enum Section: SessionTableSection { public enum Section: SessionTableSection {
@ -31,7 +31,7 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
} }
} }
public enum Setting: Differentiable { public enum Item: Differentiable {
case strategyUseFastMode case strategyUseFastMode
case strategyDeviceSettings case strategyDeviceSettings
case styleSound case styleSound
@ -41,6 +41,13 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
// MARK: - Content // MARK: - Content
private struct State: Equatable {
let isUsingFullAPNs: Bool
let notificationSound: Preferences.Sound
let playNotificationSoundInForeground: Bool
let previewType: Preferences.NotificationPreviewType
}
override var title: String { "NOTIFICATIONS_TITLE".localized() } override var title: String { "NOTIFICATIONS_TITLE".localized() }
public override var observableTableData: ObservableData { _observableTableData } public override var observableTableData: ObservableData { _observableTableData }
@ -53,12 +60,30 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) /// 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 /// 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 _observableTableData: ObservableData = ValueObservation private lazy var _observableTableData: ObservableData = ValueObservation
.trackingConstantRegion { db -> [SectionModel] in .trackingConstantRegion { db -> State in
let notificationSound: Preferences.Sound = db[.defaultNotificationSound] State(
.defaulting(to: Preferences.Sound.defaultNotificationSound) isUsingFullAPNs: false, // Set later the the data flow
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] notificationSound: db[.defaultNotificationSound]
.defaulting(to: Preferences.Sound.defaultNotificationSound),
playNotificationSoundInForeground: db[.playNotificationSoundInForeground],
previewType: db[.preferencesNotificationPreviewType]
.defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType) .defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType)
)
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[NotificationSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.manualRefreshFrom(forcedRefresh)
.map { dbState -> State in
State(
isUsingFullAPNs: UserDefaults.standard[.isUsingFullAPNs],
notificationSound: dbState.notificationSound,
playNotificationSoundInForeground: dbState.playNotificationSoundInForeground,
previewType: dbState.previewType
)
}
.withPrevious()
.map { (previous: State?, current: State) -> [SectionModel] in
return [ return [
SectionModel( SectionModel(
model: .strategy, model: .strategy,
@ -68,13 +93,16 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
title: "NOTIFICATIONS_STRATEGY_FAST_MODE_TITLE".localized(), title: "NOTIFICATIONS_STRATEGY_FAST_MODE_TITLE".localized(),
subtitle: "NOTIFICATIONS_STRATEGY_FAST_MODE_DESCRIPTION".localized(), subtitle: "NOTIFICATIONS_STRATEGY_FAST_MODE_DESCRIPTION".localized(),
rightAccessory: .toggle( rightAccessory: .toggle(
.userDefaults(UserDefaults.standard, key: "isUsingFullAPNs") .boolValue(
current.isUsingFullAPNs,
oldValue: (previous ?? current).isUsingFullAPNs
)
), ),
styling: SessionCell.StyleInfo( styling: SessionCell.StyleInfo(
allowedSeparators: [.top], allowedSeparators: [.top],
customPadding: SessionCell.Padding(bottom: Values.verySmallSpacing) customPadding: SessionCell.Padding(bottom: Values.verySmallSpacing)
), ),
onTap: { onTap: { [weak self] in
UserDefaults.standard.set( UserDefaults.standard.set(
!UserDefaults.standard.bool(forKey: "isUsingFullAPNs"), !UserDefaults.standard.bool(forKey: "isUsingFullAPNs"),
forKey: "isUsingFullAPNs" forKey: "isUsingFullAPNs"
@ -82,6 +110,7 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
// Force sync the push tokens on change // Force sync the push tokens on change
SyncPushTokensJob.run(uploadOnlyIfStale: false) SyncPushTokensJob.run(uploadOnlyIfStale: false)
self?.forceRefresh()
} }
), ),
SessionCell.Info( SessionCell.Info(
@ -106,7 +135,7 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
id: .styleSound, id: .styleSound,
title: "NOTIFICATIONS_STYLE_SOUND_TITLE".localized(), title: "NOTIFICATIONS_STYLE_SOUND_TITLE".localized(),
rightAccessory: .dropDown( rightAccessory: .dropDown(
.dynamicString { notificationSound.displayName } .dynamicString { current.notificationSound.displayName }
), ),
onTap: { [weak self] in onTap: { [weak self] in
self?.transitionToScreen( self?.transitionToScreen(
@ -117,7 +146,13 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
SessionCell.Info( SessionCell.Info(
id: .styleSoundWhenAppIsOpen, id: .styleSoundWhenAppIsOpen,
title: "NOTIFICATIONS_STYLE_SOUND_WHEN_OPEN_TITLE".localized(), title: "NOTIFICATIONS_STYLE_SOUND_WHEN_OPEN_TITLE".localized(),
rightAccessory: .toggle(.settingBool(key: .playNotificationSoundInForeground)), rightAccessory: .toggle(
.boolValue(
key: .playNotificationSoundInForeground,
value: current.playNotificationSoundInForeground,
oldValue: (previous ?? current).playNotificationSoundInForeground
)
),
onTap: { onTap: {
Storage.shared.write { db in Storage.shared.write { db in
db[.playNotificationSoundInForeground] = !db[.playNotificationSoundInForeground] db[.playNotificationSoundInForeground] = !db[.playNotificationSoundInForeground]
@ -134,7 +169,7 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
title: "NOTIFICATIONS_STYLE_CONTENT_TITLE".localized(), title: "NOTIFICATIONS_STYLE_CONTENT_TITLE".localized(),
subtitle: "NOTIFICATIONS_STYLE_CONTENT_DESCRIPTION".localized(), subtitle: "NOTIFICATIONS_STYLE_CONTENT_DESCRIPTION".localized(),
rightAccessory: .dropDown( rightAccessory: .dropDown(
.dynamicString { previewType.name } .dynamicString { current.previewType.name }
), ),
onTap: { [weak self] in onTap: { [weak self] in
self?.transitionToScreen( self?.transitionToScreen(
@ -146,8 +181,5 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
) )
] ]
} }
.removeDuplicates()
.handleEvents(didFail: { SNLog("[NotificationSettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)
} }

View file

@ -5,6 +5,7 @@ import SessionUIKit
import SessionSnodeKit import SessionSnodeKit
import SessionMessagingKit import SessionMessagingKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionUtilitiesKit
final class NukeDataModal: Modal { final class NukeDataModal: Modal {
// MARK: - Initialization // MARK: - Initialization
@ -226,8 +227,9 @@ final class NukeDataModal: Modal {
let maybeDeviceToken: String? = UserDefaults.standard[.deviceToken] let maybeDeviceToken: String? = UserDefaults.standard[.deviceToken]
if isUsingFullAPNs, let deviceToken: String = maybeDeviceToken { if isUsingFullAPNs, let deviceToken: String = maybeDeviceToken {
let data: Data = Data(hex: deviceToken) PushNotificationAPI
PushNotificationAPI.unregister(data).sinkUntilComplete() .unsubscribe(token: Data(hex: deviceToken))
.sinkUntilComplete()
} }
/// Stop and cancel all current jobs (don't want to inadvertantly have a job store data after it's table has already been cleared) /// Stop and cancel all current jobs (don't want to inadvertantly have a job store data after it's table has already been cleared)
@ -244,7 +246,7 @@ final class NukeDataModal: Modal {
UserDefaults.removeAll() UserDefaults.removeAll()
// Remove the cached key so it gets re-cached on next access // Remove the cached key so it gets re-cached on next access
dependencies.mutableGeneralCache.mutate { dependencies.caches.mutate(cache: .general) {
$0.encodedPublicKey = nil $0.encodedPublicKey = nil
$0.recentReactionTimestamps = [] $0.recentReactionTimestamps = []
} }

View file

@ -28,6 +28,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
public enum Section: SessionTableSection { public enum Section: SessionTableSection {
case screenSecurity case screenSecurity
case messageRequests
case readReceipts case readReceipts
case typingIndicators case typingIndicators
case linkPreviews case linkPreviews
@ -36,6 +37,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
var title: String? { var title: String? {
switch self { switch self {
case .screenSecurity: return "PRIVACY_SECTION_SCREEN_SECURITY".localized() case .screenSecurity: return "PRIVACY_SECTION_SCREEN_SECURITY".localized()
case .messageRequests: return "PRIVACY_SECTION_MESSAGE_REQUESTS".localized()
case .readReceipts: return "PRIVACY_SECTION_READ_RECEIPTS".localized() case .readReceipts: return "PRIVACY_SECTION_READ_RECEIPTS".localized()
case .typingIndicators: return "PRIVACY_SECTION_TYPING_INDICATORS".localized() case .typingIndicators: return "PRIVACY_SECTION_TYPING_INDICATORS".localized()
case .linkPreviews: return "PRIVACY_SECTION_LINK_PREVIEWS".localized() case .linkPreviews: return "PRIVACY_SECTION_LINK_PREVIEWS".localized()
@ -48,6 +50,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
public enum Item: Differentiable { public enum Item: Differentiable {
case screenLock case screenLock
case communityMessageRequests
case screenshotNotifications case screenshotNotifications
case readReceipts case readReceipts
case typingIndicators case typingIndicators
@ -75,6 +78,15 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
// MARK: - Content // MARK: - Content
private struct State: Equatable {
let isScreenLockEnabled: Bool
let checkForCommunityMessageRequests: Bool
let areReadReceiptsEnabled: Bool
let typingIndicatorsEnabled: Bool
let areLinkPreviewsEnabled: Bool
let areCallsEnabled: Bool
}
override var title: String { "PRIVACY_TITLE".localized() } override var title: String { "PRIVACY_TITLE".localized() }
public override var observableTableData: ObservableData { _observableTableData } public override var observableTableData: ObservableData { _observableTableData }
@ -87,7 +99,21 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) /// 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 /// 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 _observableTableData: ObservableData = ValueObservation private lazy var _observableTableData: ObservableData = ValueObservation
.trackingConstantRegion { db -> [SectionModel] in .trackingConstantRegion { [weak self] db -> State in
State(
isScreenLockEnabled: db[.isScreenLockEnabled],
checkForCommunityMessageRequests: db[.checkForCommunityMessageRequests],
areReadReceiptsEnabled: db[.areReadReceiptsEnabled],
typingIndicatorsEnabled: db[.typingIndicatorsEnabled],
areLinkPreviewsEnabled: db[.areLinkPreviewsEnabled],
areCallsEnabled: db[.areCallsEnabled]
)
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[PrivacySettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.withPrevious()
.map { (previous: State?, current: State) -> [SectionModel] in
return [ return [
SectionModel( SectionModel(
model: .screenSecurity, model: .screenSecurity,
@ -96,7 +122,13 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
id: .screenLock, id: .screenLock,
title: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE".localized(), title: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE".localized(),
subtitle: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION".localized(), subtitle: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION".localized(),
rightAccessory: .toggle(.settingBool(key: .isScreenLockEnabled)), rightAccessory: .toggle(
.boolValue(
key: .isScreenLockEnabled,
value: current.isScreenLockEnabled,
oldValue: (previous ?? current).isScreenLockEnabled
)
),
onTap: { [weak self] in onTap: { [weak self] in
// Make sure the device has a passcode set before allowing screen lock to // Make sure the device has a passcode set before allowing screen lock to
// be enabled (Note: This will always return true on a simulator) // be enabled (Note: This will always return true on a simulator)
@ -115,7 +147,32 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
} }
Storage.shared.write { db in Storage.shared.write { db in
db[.isScreenLockEnabled] = !db[.isScreenLockEnabled] try db.setAndUpdateConfig(.isScreenLockEnabled, to: !db[.isScreenLockEnabled])
}
}
)
]
),
SectionModel(
model: .messageRequests,
elements: [
SessionCell.Info(
id: .communityMessageRequests,
title: "PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE".localized(),
subtitle: "PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION".localized(),
rightAccessory: .toggle(
.boolValue(
key: .checkForCommunityMessageRequests,
value: current.checkForCommunityMessageRequests,
oldValue: (previous ?? current).checkForCommunityMessageRequests
)
),
onTap: { [weak self] in
Storage.shared.write { db in
try db.setAndUpdateConfig(
.checkForCommunityMessageRequests,
to: !db[.checkForCommunityMessageRequests]
)
} }
} }
) )
@ -128,10 +185,16 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
id: .readReceipts, id: .readReceipts,
title: "PRIVACY_READ_RECEIPTS_TITLE".localized(), title: "PRIVACY_READ_RECEIPTS_TITLE".localized(),
subtitle: "PRIVACY_READ_RECEIPTS_DESCRIPTION".localized(), subtitle: "PRIVACY_READ_RECEIPTS_DESCRIPTION".localized(),
rightAccessory: .toggle(.settingBool(key: .areReadReceiptsEnabled)), rightAccessory: .toggle(
.boolValue(
key: .areReadReceiptsEnabled,
value: current.areReadReceiptsEnabled,
oldValue: (previous ?? current).areReadReceiptsEnabled
)
),
onTap: { onTap: {
Storage.shared.write { db in Storage.shared.write { db in
db[.areReadReceiptsEnabled] = !db[.areReadReceiptsEnabled] try db.setAndUpdateConfig(.areReadReceiptsEnabled, to: !db[.areReadReceiptsEnabled])
} }
} }
) )
@ -176,10 +239,16 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
return result return result
} }
), ),
rightAccessory: .toggle(.settingBool(key: .typingIndicatorsEnabled)), rightAccessory: .toggle(
.boolValue(
key: .typingIndicatorsEnabled,
value: current.typingIndicatorsEnabled,
oldValue: (previous ?? current).typingIndicatorsEnabled
)
),
onTap: { onTap: {
Storage.shared.write { db in Storage.shared.write { db in
db[.typingIndicatorsEnabled] = !db[.typingIndicatorsEnabled] try db.setAndUpdateConfig(.typingIndicatorsEnabled, to: !db[.typingIndicatorsEnabled])
} }
} }
) )
@ -192,10 +261,16 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
id: .linkPreviews, id: .linkPreviews,
title: "PRIVACY_LINK_PREVIEWS_TITLE".localized(), title: "PRIVACY_LINK_PREVIEWS_TITLE".localized(),
subtitle: "PRIVACY_LINK_PREVIEWS_DESCRIPTION".localized(), subtitle: "PRIVACY_LINK_PREVIEWS_DESCRIPTION".localized(),
rightAccessory: .toggle(.settingBool(key: .areLinkPreviewsEnabled)), rightAccessory: .toggle(
.boolValue(
key: .areLinkPreviewsEnabled,
value: current.areLinkPreviewsEnabled,
oldValue: (previous ?? current).areLinkPreviewsEnabled
)
),
onTap: { onTap: {
Storage.shared.write { db in Storage.shared.write { db in
db[.areLinkPreviewsEnabled] = !db[.areLinkPreviewsEnabled] try db.setAndUpdateConfig(.areLinkPreviewsEnabled, to: !db[.areLinkPreviewsEnabled])
} }
} }
) )
@ -208,7 +283,13 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
id: .calls, id: .calls,
title: "PRIVACY_CALLS_TITLE".localized(), title: "PRIVACY_CALLS_TITLE".localized(),
subtitle: "PRIVACY_CALLS_DESCRIPTION".localized(), subtitle: "PRIVACY_CALLS_DESCRIPTION".localized(),
rightAccessory: .toggle(.settingBool(key: .areCallsEnabled)), rightAccessory: .toggle(
.boolValue(
key: .areCallsEnabled,
value: current.areCallsEnabled,
oldValue: (previous ?? current).areCallsEnabled
)
),
accessibility: Accessibility( accessibility: Accessibility(
label: "Allow voice and video calls" label: "Allow voice and video calls"
), ),
@ -223,7 +304,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
), ),
onTap: { onTap: {
Storage.shared.write { db in Storage.shared.write { db in
db[.areCallsEnabled] = !db[.areCallsEnabled] try db.setAndUpdateConfig(.areCallsEnabled, to: !db[.areCallsEnabled])
} }
} }
) )
@ -231,8 +312,5 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
) )
] ]
} }
.removeDuplicates()
.handleEvents(didFail: { SNLog("[PrivacySettingsViewModel] Observation failed with error: \($0)") })
.publisher(in: Storage.shared)
.mapToSessionTableViewData(for: self) .mapToSessionTableViewData(for: self)
} }

View file

@ -2,6 +2,7 @@
import UIKit import UIKit
import SessionUIKit import SessionUIKit
import SessionUtilitiesKit
public protocol CaptionContainerViewDelegate: AnyObject { public protocol CaptionContainerViewDelegate: AnyObject {
func captionContainerViewDidUpdateText(_ captionContainerView: CaptionContainerView) func captionContainerViewDidUpdateText(_ captionContainerView: CaptionContainerView)

View file

@ -4,6 +4,7 @@ import UIKit
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit
public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticCell { public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticCell {
public static let mutePrefix: String = "\u{e067} " public static let mutePrefix: String = "\u{e067} "

View file

@ -262,7 +262,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
reloadSectionsAnimation: .none, reloadSectionsAnimation: .none,
deleteRowsAnimation: .fade, deleteRowsAnimation: .fade,
insertRowsAnimation: .fade, insertRowsAnimation: .fade,
reloadRowsAnimation: .fade, reloadRowsAnimation: .none,
interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues
) { [weak self] updatedData in ) { [weak self] updatedData in
self?.viewModel.updateTableData(updatedData) self?.viewModel.updateTableData(updatedData)
@ -339,6 +339,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
.store(in: &disposables) .store(in: &disposables)
viewModel.leftNavItems viewModel.leftNavItems
.receive(on: DispatchQueue.main)
.sink { [weak self] maybeItems in .sink { [weak self] maybeItems in
self?.navigationItem.setLeftBarButtonItems( self?.navigationItem.setLeftBarButtonItems(
maybeItems.map { items in maybeItems.map { items in
@ -360,6 +361,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
.store(in: &disposables) .store(in: &disposables)
viewModel.rightNavItems viewModel.rightNavItems
.receive(on: DispatchQueue.main)
.sink { [weak self] maybeItems in .sink { [weak self] maybeItems in
self?.navigationItem.setRightBarButtonItems( self?.navigationItem.setRightBarButtonItems(
maybeItems.map { items in maybeItems.map { items in
@ -381,18 +383,21 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
.store(in: &disposables) .store(in: &disposables)
viewModel.emptyStateTextPublisher viewModel.emptyStateTextPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] text in .sink { [weak self] text in
self?.emptyStateLabel.text = text self?.emptyStateLabel.text = text
} }
.store(in: &disposables) .store(in: &disposables)
viewModel.footerView viewModel.footerView
.receive(on: DispatchQueue.main)
.sink { [weak self] footerView in .sink { [weak self] footerView in
self?.tableView.tableFooterView = footerView self?.tableView.tableFooterView = footerView
} }
.store(in: &disposables) .store(in: &disposables)
viewModel.footerButtonInfo viewModel.footerButtonInfo
.receive(on: DispatchQueue.main)
.sink { [weak self] buttonInfo in .sink { [weak self] buttonInfo in
if let buttonInfo: SessionButton.Info = buttonInfo { if let buttonInfo: SessionButton.Info = buttonInfo {
self?.footerButton.setTitle(buttonInfo.title, for: .normal) self?.footerButton.setTitle(buttonInfo.title, for: .normal)
@ -627,7 +632,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
) { ) {
// Try update the existing cell to have a nice animation instead of reloading the cell // Try update the existing cell to have a nice animation instead of reloading the cell
if let existingCell: SessionCell = tableView.cellForRow(at: indexPath) as? SessionCell { if let existingCell: SessionCell = tableView.cellForRow(at: indexPath) as? SessionCell {
existingCell.update(with: info) existingCell.update(with: info, isManualReload: true)
} }
else { else {
tableView.reloadRows(at: [indexPath], with: .none) tableView.reloadRows(at: [indexPath], with: .none)

View file

@ -27,6 +27,9 @@ class SessionTableViewModel<NavItemId: Equatable, Section: SessionTableSection,
open var leftNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() } open var leftNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() }
open var rightNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() } open var rightNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() }
private let _forcedRefresh: PassthroughSubject<Void, Never> = PassthroughSubject()
lazy var forcedRefresh: AnyPublisher<Void, Never> = _forcedRefresh
.shareReplay(0)
private let _showToast: PassthroughSubject<(String, ThemeValue), Never> = PassthroughSubject() private let _showToast: PassthroughSubject<(String, ThemeValue), Never> = PassthroughSubject()
lazy var showToast: AnyPublisher<(String, ThemeValue), Never> = _showToast lazy var showToast: AnyPublisher<(String, ThemeValue), Never> = _showToast
.shareReplay(0) .shareReplay(0)
@ -62,6 +65,10 @@ class SessionTableViewModel<NavItemId: Equatable, Section: SessionTableSection,
// MARK: - Functions // MARK: - Functions
func forceRefresh() {
_forcedRefresh.send(())
}
func setIsEditing(_ isEditing: Bool) { func setIsEditing(_ isEditing: Bool) {
_isEditing.send(isEditing) _isEditing.send(isEditing)
} }
@ -101,7 +108,7 @@ extension Array {
} }
} }
extension AnyPublisher { extension Publisher {
func mapToSessionTableViewData<Nav, Section, Item>( func mapToSessionTableViewData<Nav, Section, Item>(
for viewModel: SessionTableViewModel<Nav, Section, Item> for viewModel: SessionTableViewModel<Nav, Section, Item>
) -> AnyPublisher<(Output, StagedChangeset<Output>), Failure> where Output == [ArraySection<Section, SessionCell.Info<Item>>] { ) -> AnyPublisher<(Output, StagedChangeset<Output>), Failure> where Output == [ArraySection<Section, SessionCell.Info<Item>>] {

View file

@ -394,19 +394,30 @@ extension SessionCell.Accessory {
extension SessionCell.Accessory { extension SessionCell.Accessory {
public enum DataSource: Hashable, Equatable { public enum DataSource: Hashable, Equatable {
case boolValue(Bool) case boolValue(key: String, value: Bool, oldValue: Bool)
case dynamicString(() -> String?) case dynamicString(() -> String?)
case userDefaults(UserDefaults, key: String)
case settingBool(key: Setting.BoolKey) static func boolValue(_ value: Bool, oldValue: Bool) -> DataSource {
return .boolValue(key: "", value: value, oldValue: oldValue)
}
static func boolValue(key: Setting.BoolKey, value: Bool, oldValue: Bool) -> DataSource {
return .boolValue(key: key.rawValue, value: value, oldValue: oldValue)
}
// MARK: - Convenience // MARK: - Convenience
public var currentBoolValue: Bool { public var currentBoolValue: Bool {
switch self { switch self {
case .boolValue(let value): return value case .boolValue(_, let value, _): return value
case .dynamicString: return false case .dynamicString: return false
case .userDefaults(let defaults, let key): return defaults.bool(forKey: key) }
case .settingBool(let key): return Storage.shared[key] }
public var oldBoolValue: Bool {
switch self {
case .boolValue(_, _, let oldValue): return oldValue
default: return false
} }
} }
@ -421,27 +432,27 @@ extension SessionCell.Accessory {
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
switch self { switch self {
case .boolValue(let value): value.hash(into: &hasher) case .boolValue(let key, let value, let oldValue):
key.hash(into: &hasher)
value.hash(into: &hasher)
oldValue.hash(into: &hasher)
case .dynamicString(let generator): generator().hash(into: &hasher) case .dynamicString(let generator): generator().hash(into: &hasher)
case .userDefaults(_, let key): key.hash(into: &hasher)
case .settingBool(let key): key.hash(into: &hasher)
} }
} }
public static func == (lhs: DataSource, rhs: DataSource) -> Bool { public static func == (lhs: DataSource, rhs: DataSource) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.boolValue(let lhsValue), .boolValue(let rhsValue)): case (.boolValue(let lhsKey, let lhsValue, let lhsOldValue), .boolValue(let rhsKey, let rhsValue, let rhsOldValue)):
return (lhsValue == rhsValue) return (
lhsKey == rhsKey &&
lhsValue == rhsValue &&
lhsOldValue == rhsOldValue
)
case (.dynamicString(let lhsGenerator), .dynamicString(let rhsGenerator)): case (.dynamicString(let lhsGenerator), .dynamicString(let rhsGenerator)):
return (lhsGenerator() == rhsGenerator()) return (lhsGenerator() == rhsGenerator())
case (.userDefaults(_, let lhsKey), .userDefaults(_, let rhsKey)):
return (lhsKey == rhsKey)
case (.settingBool(let lhsKey), .settingBool(let rhsKey)):
return (lhsKey == rhsKey)
default: return false default: return false
} }
} }

View file

@ -1,6 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import UIKit
import SessionUIKit import SessionUIKit
// MARK: - Main Types // MARK: - Main Types

View file

@ -277,7 +277,8 @@ extension SessionCell {
public func update( public func update(
with accessory: Accessory?, with accessory: Accessory?,
tintColor: ThemeValue, tintColor: ThemeValue,
isEnabled: Bool isEnabled: Bool,
isManualReload: Bool
) { ) {
guard let accessory: Accessory = accessory else { return } guard let accessory: Accessory = accessory else { return }
@ -356,10 +357,15 @@ extension SessionCell {
fixedWidthConstraint.isActive = true fixedWidthConstraint.isActive = true
toggleSwitchConstraints.forEach { $0.isActive = true } toggleSwitchConstraints.forEach { $0.isActive = true }
let newValue: Bool = dataSource.currentBoolValue if !isManualReload {
toggleSwitch.setOn(dataSource.oldBoolValue, animated: false)
if newValue != toggleSwitch.isOn { // Dispatch so the cell reload doesn't conflict with the setting change animation
toggleSwitch.setOn(newValue, animated: true) if dataSource.oldBoolValue != dataSource.currentBoolValue {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) { [weak toggleSwitch] in
toggleSwitch?.setOn(dataSource.currentBoolValue, animated: true)
}
}
} }
case .dropDown(let dataSource, let accessibility): case .dropDown(let dataSource, let accessibility):

Some files were not shown because too many files have changed in this diff Show more