mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
commit
862a6a8898
|
@ -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
|
|
@ -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
|
@ -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
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import GRDB
|
import GRDB
|
||||||
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
extension SessionCallManager {
|
extension SessionCallManager {
|
||||||
@discardableResult
|
@discardableResult
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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?) {
|
||||||
|
|
|
@ -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?) { }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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([:])
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SignalUtilitiesKit
|
import SignalUtilitiesKit
|
||||||
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public class GiphyDownloader: ProxiedContentDownloader {
|
public class GiphyDownloader: ProxiedContentDownloader {
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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?)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
253
Session/Meta/MainAppContext.swift
Normal file
253
Session/Meta/MainAppContext.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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/>
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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>
|
||||||
|
|
Binary file not shown.
|
@ -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"
|
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import UserNotifications
|
||||||
import SessionMessagingKit
|
import SessionMessagingKit
|
||||||
import SignalCoreKit
|
import SignalCoreKit
|
||||||
import SignalUtilitiesKit
|
import SignalUtilitiesKit
|
||||||
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
class UserNotificationConfig {
|
class UserNotificationConfig {
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() } }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = []
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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} "
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>>] {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue