mirror of https://github.com/oxen-io/session-ios
Merge branch 'dev' of https://github.com/oxen-io/session-ios into preformance-improvement
commit
f22672ccd7
@ -0,0 +1,447 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
@objc
|
||||
class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
|
||||
private var threads: YapDatabaseViewMappings!
|
||||
private var threadViewModelCache: [String: ThreadViewModel] = [:] // Thread ID to ThreadViewModel
|
||||
private var tableViewTopConstraint: NSLayoutConstraint!
|
||||
|
||||
private var messageRequestCount: UInt {
|
||||
threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
||||
}
|
||||
|
||||
private lazy var dbConnection: YapDatabaseConnection = {
|
||||
let result = OWSPrimaryStorage.shared().newDatabaseConnection()
|
||||
result.objectCacheLimit = 500
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let result: UITableView = UITableView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.backgroundColor = .clear
|
||||
result.separatorStyle = .none
|
||||
result.register(MessageRequestsCell.self, forCellReuseIdentifier: MessageRequestsCell.reuseIdentifier)
|
||||
result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
|
||||
let bottomInset = Values.newConversationButtonBottomOffset + NewConversationButtonSet.expandedButtonSize + Values.largeSpacing + NewConversationButtonSet.collapsedButtonSize
|
||||
result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
|
||||
result.showsVerticalScrollIndicator = false
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var emptyStateLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isUserInteractionEnabled = false
|
||||
result.font = UIFont.systemFont(ofSize: Values.smallFontSize)
|
||||
result.text = NSLocalizedString("MESSAGE_REQUESTS_EMPTY_TEXT", comment: "")
|
||||
result.textColor = Colors.text
|
||||
result.textAlignment = .center
|
||||
result.numberOfLines = 0
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var fadeView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isUserInteractionEnabled = false
|
||||
result.setGradient(Gradients.homeVCFade)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var clearAllButton: UIButton = {
|
||||
let result: UIButton = UIButton()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
|
||||
result.setTitle(NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL", comment: ""), for: .normal)
|
||||
result.setTitleColor(Colors.destructive, for: .normal)
|
||||
result.setBackgroundImage(
|
||||
Colors.destructive
|
||||
.withAlphaComponent(isDarkMode ? 0.2 : 0.06)
|
||||
.toImage(isDarkMode: isDarkMode),
|
||||
for: .highlighted
|
||||
)
|
||||
result.isHidden = true
|
||||
result.layer.cornerRadius = (NewConversationButtonSet.collapsedButtonSize / 2)
|
||||
result.layer.borderColor = Colors.destructive.cgColor
|
||||
result.layer.borderWidth = 1.5
|
||||
result.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
ViewControllerUtilities.setUpDefaultSessionStyle(for: self, title: NSLocalizedString("MESSAGE_REQUESTS_TITLE", comment: ""), hasCustomBackButton: false)
|
||||
|
||||
// Threads (part 1)
|
||||
// Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
|
||||
dbConnection.beginLongLivedReadTransaction()
|
||||
|
||||
// Add the UI (MUST be done after the thread freeze so the 'tableView' creation and setting
|
||||
// the dataSource has the correct data)
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(emptyStateLabel)
|
||||
view.addSubview(fadeView)
|
||||
view.addSubview(clearAllButton)
|
||||
|
||||
// Notifications
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleYapDatabaseModifiedNotification(_:)),
|
||||
name: .YapDatabaseModified,
|
||||
object: OWSPrimaryStorage.shared().dbNotificationObject
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleProfileDidChangeNotification(_:)),
|
||||
name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange),
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleBlockedContactsUpdatedNotification(_:)),
|
||||
name: .blockedContactsUpdated,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// Threads (part 2)
|
||||
threads = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
|
||||
dbConnection.read { transaction in
|
||||
self.threads.update(with: transaction) // Perform the initial update
|
||||
}
|
||||
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
reload()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private func setupLayout() {
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.smallSpacing),
|
||||
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
|
||||
tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
|
||||
emptyStateLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.massiveSpacing),
|
||||
emptyStateLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.mediumSpacing),
|
||||
emptyStateLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Values.mediumSpacing),
|
||||
emptyStateLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
|
||||
fadeView.topAnchor.constraint(equalTo: view.topAnchor, constant: (0.15 * view.bounds.height)),
|
||||
fadeView.leftAnchor.constraint(equalTo: view.leftAnchor),
|
||||
fadeView.rightAnchor.constraint(equalTo: view.rightAnchor),
|
||||
fadeView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
|
||||
clearAllButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
clearAllButton.bottomAnchor.constraint(
|
||||
equalTo: view.bottomAnchor,
|
||||
constant: -Values.newConversationButtonBottomOffset // Negative due to how the constraint is set up
|
||||
),
|
||||
clearAllButton.widthAnchor.constraint(equalToConstant: 155),
|
||||
clearAllButton.heightAnchor.constraint(equalToConstant: NewConversationButtonSet.collapsedButtonSize)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return Int(messageRequestCount)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
|
||||
cell.threadViewModel = threadViewModel(at: indexPath.row)
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: - Updating
|
||||
|
||||
private func reload() {
|
||||
AssertIsOnMainThread()
|
||||
dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
|
||||
dbConnection.read { transaction in
|
||||
self.threads.update(with: transaction)
|
||||
}
|
||||
threadViewModelCache.removeAll()
|
||||
tableView.reloadData()
|
||||
clearAllButton.isHidden = (messageRequestCount == 0)
|
||||
emptyStateLabel.isHidden = (messageRequestCount != 0)
|
||||
emptyStateLabel.isHidden = (messageRequestCount != 0)
|
||||
}
|
||||
|
||||
@objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) {
|
||||
// NOTE: This code is very finicky and crashes easily. Modify with care.
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// If we don't capture `threads` here, a race condition can occur where the
|
||||
// `thread.snapshotOfLastUpdate != firstSnapshot - 1` check below evaluates to
|
||||
// `false`, but `threads` then changes between that check and the
|
||||
// `ext.getSectionChanges(§ionChanges, rowChanges: &rowChanges, for: notifications, with: threads)`
|
||||
// line. This causes `tableView.endUpdates()` to crash with an `NSInternalInconsistencyException`.
|
||||
let threads = threads!
|
||||
|
||||
// Create a stable state for the connection and jump to the latest commit
|
||||
let notifications = dbConnection.beginLongLivedReadTransaction()
|
||||
|
||||
guard !notifications.isEmpty else { return }
|
||||
|
||||
let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection
|
||||
let hasChanges = ext.hasChanges(forGroup: TSMessageRequestGroup, in: notifications)
|
||||
|
||||
guard hasChanges else { return }
|
||||
|
||||
if let firstChangeSet = notifications[0].userInfo {
|
||||
let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64
|
||||
|
||||
if threads.snapshotOfLastUpdate != firstSnapshot - 1 {
|
||||
return reload() // The code below will crash if we try to process multiple commits at once
|
||||
}
|
||||
}
|
||||
|
||||
var sectionChanges = NSArray()
|
||||
var rowChanges = NSArray()
|
||||
ext.getSectionChanges(§ionChanges, rowChanges: &rowChanges, for: notifications, with: threads)
|
||||
|
||||
guard sectionChanges.count > 0 || rowChanges.count > 0 else { return }
|
||||
|
||||
tableView.beginUpdates()
|
||||
|
||||
rowChanges.forEach { rowChange in
|
||||
let rowChange = rowChange as! YapDatabaseViewRowChange
|
||||
let key = rowChange.collectionKey.key
|
||||
threadViewModelCache[key] = nil
|
||||
switch rowChange.type {
|
||||
case .delete: tableView.deleteRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
|
||||
case .insert: tableView.insertRows(at: [ rowChange.newIndexPath! ], with: UITableView.RowAnimation.automatic)
|
||||
case .update: tableView.reloadRows(at: [ rowChange.indexPath! ], with: UITableView.RowAnimation.automatic)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
tableView.endUpdates()
|
||||
|
||||
// HACK: Moves can have conflicts with the other 3 types of change.
|
||||
// Just batch perform all the moves separately to prevent crashing.
|
||||
// Since all the changes are from the original state to the final state,
|
||||
// it will still be correct if we pick the moves out.
|
||||
|
||||
tableView.beginUpdates()
|
||||
|
||||
rowChanges.forEach { rowChange in
|
||||
let rowChange = rowChange as! YapDatabaseViewRowChange
|
||||
let key = rowChange.collectionKey.key
|
||||
threadViewModelCache[key] = nil
|
||||
|
||||
switch rowChange.type {
|
||||
case .move: tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
tableView.endUpdates()
|
||||
clearAllButton.isHidden = (messageRequestCount == 0)
|
||||
emptyStateLabel.isHidden = (messageRequestCount != 0)
|
||||
}
|
||||
|
||||
@objc private func handleProfileDidChangeNotification(_ notification: Notification) {
|
||||
tableView.reloadData() // TODO: Just reload the affected cell
|
||||
}
|
||||
|
||||
@objc private func handleBlockedContactsUpdatedNotification(_ notification: Notification) {
|
||||
tableView.reloadData() // TODO: Just reload the affected cell
|
||||
}
|
||||
|
||||
@objc override internal func handleAppModeChangedNotification(_ notification: Notification) {
|
||||
super.handleAppModeChangedNotification(notification)
|
||||
|
||||
let gradient = Gradients.homeVCFade
|
||||
fadeView.setGradient(gradient) // Re-do the gradient
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
guard let thread = self.thread(at: indexPath.row) else { return }
|
||||
|
||||
let conversationVC = ConversationVC(thread: thread)
|
||||
self.navigationController?.pushViewController(conversationVC, animated: true)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
||||
guard let thread = self.thread(at: indexPath.row) else { return [] }
|
||||
|
||||
let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_DELETE_TITLE", comment: "")) { [weak self] _, _ in
|
||||
self?.delete(thread)
|
||||
}
|
||||
delete.backgroundColor = Colors.destructive
|
||||
|
||||
return [ delete ]
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
private func updateContactAndThread(thread: TSThread, with transaction: YapDatabaseReadWriteTransaction, onComplete: ((Bool) -> ())? = nil) {
|
||||
guard let contactThread: TSContactThread = thread as? TSContactThread else {
|
||||
onComplete?(false)
|
||||
return
|
||||
}
|
||||
|
||||
var needsSync: Bool = false
|
||||
|
||||
// Update the contact
|
||||
let sessionId: String = contactThread.contactSessionID()
|
||||
|
||||
if let contact: Contact = Storage.shared.getContact(with: sessionId), (contact.isApproved || !contact.isBlocked) {
|
||||
contact.isApproved = false
|
||||
contact.isBlocked = true
|
||||
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
needsSync = true
|
||||
}
|
||||
|
||||
// Delete all thread content
|
||||
thread.removeAllThreadInteractions(with: transaction)
|
||||
thread.remove(with: transaction)
|
||||
|
||||
onComplete?(needsSync)
|
||||
}
|
||||
|
||||
@objc private func clearAllTapped() {
|
||||
let threadCount: Int = Int(messageRequestCount)
|
||||
let threads: [TSThread] = (0..<threadCount).compactMap { self.thread(at: $0) }
|
||||
var needsSync: Bool = false
|
||||
|
||||
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON", comment: ""), style: .destructive) { _ in
|
||||
// Clear the requests
|
||||
Storage.write(
|
||||
with: { [weak self] transaction in
|
||||
threads.forEach { thread in
|
||||
if let uniqueId: String = thread.uniqueId {
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
}
|
||||
|
||||
self?.updateContactAndThread(thread: thread, with: transaction) { threadNeedsSync in
|
||||
if threadNeedsSync {
|
||||
needsSync = true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
completion: {
|
||||
// Block all the contacts
|
||||
threads.forEach { thread in
|
||||
if let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), !OWSBlockingManager.shared().isRecipientIdBlocked(sessionId) {
|
||||
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
// Force a config sync (must run on the main thread)
|
||||
if needsSync {
|
||||
DispatchQueue.main.async {
|
||||
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil))
|
||||
self.present(alertVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func delete(_ thread: TSThread) {
|
||||
guard let uniqueId: String = thread.uniqueId else { return }
|
||||
|
||||
let alertVC: UIAlertController = UIAlertController(title: NSLocalizedString("MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { _ in
|
||||
Storage.write(
|
||||
with: { [weak self] transaction in
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: uniqueId, using: transaction)
|
||||
self?.updateContactAndThread(thread: thread, with: transaction)
|
||||
},
|
||||
completion: {
|
||||
// Block the contact
|
||||
if let sessionId: String = (thread as? TSContactThread)?.contactSessionID(), !OWSBlockingManager.shared().isRecipientIdBlocked(sessionId) {
|
||||
OWSBlockingManager.shared().addBlockedPhoneNumber(sessionId)
|
||||
}
|
||||
|
||||
// Force a config sync (must run on the main thread)
|
||||
DispatchQueue.main.async {
|
||||
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
alertVC.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .cancel, handler: nil))
|
||||
self.present(alertVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
private func thread(at index: Int) -> TSThread? {
|
||||
var thread: TSThread? = nil
|
||||
|
||||
dbConnection.read { transaction in
|
||||
let ext: YapDatabaseViewTransaction? = transaction.ext(TSThreadDatabaseViewExtensionName) as? YapDatabaseViewTransaction
|
||||
thread = ext?.object(atRow: UInt(index), inSection: 0, with: self.threads) as? TSThread
|
||||
}
|
||||
|
||||
return thread
|
||||
}
|
||||
|
||||
private func threadViewModel(at index: Int) -> ThreadViewModel? {
|
||||
guard let thread = thread(at: index), let uniqueId: String = thread.uniqueId else { return nil }
|
||||
|
||||
if let cachedThreadViewModel = threadViewModelCache[uniqueId] {
|
||||
return cachedThreadViewModel
|
||||
}
|
||||
else {
|
||||
var threadViewModel: ThreadViewModel? = nil
|
||||
dbConnection.read { transaction in
|
||||
threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
|
||||
}
|
||||
threadViewModelCache[uniqueId] = threadViewModel
|
||||
|
||||
return threadViewModel
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
class MessageRequestsCell: UITableViewCell {
|
||||
static let reuseIdentifier = "MessageRequestsCell"
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
setUpViewHierarchy()
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
|
||||
setUpViewHierarchy()
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private let iconContainerView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.backgroundColor = Colors.sessionMessageRequestsBubble
|
||||
result.layer.cornerRadius = (Values.mediumProfilePictureSize / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let iconImageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView(image: #imageLiteral(resourceName: "message_requests").withRenderingMode(.alwaysTemplate))
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.tintColor = Colors.sessionMessageRequestsIcon
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let titleLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
result.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.text = NSLocalizedString("MESSAGE_REQUESTS_TITLE", comment: "")
|
||||
result.textColor = Colors.sessionMessageRequestsTitle
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let unreadCountView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity)
|
||||
result.layer.cornerRadius = (ConversationCell.unreadCountViewSize / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let unreadCountLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
backgroundColor = Colors.cellBackground
|
||||
selectedBackgroundView = UIView()
|
||||
selectedBackgroundView?.backgroundColor = Colors.cellSelected
|
||||
|
||||
|
||||
contentView.addSubview(iconContainerView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(unreadCountView)
|
||||
|
||||
iconContainerView.addSubview(iconImageView)
|
||||
unreadCountView.addSubview(unreadCountLabel)
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private func setupLayout() {
|
||||
NSLayoutConstraint.activate([
|
||||
contentView.heightAnchor.constraint(equalToConstant: 68),
|
||||
|
||||
iconContainerView.leftAnchor.constraint(
|
||||
equalTo: contentView.leftAnchor,
|
||||
// Need 'accentLineThickness' to line up correctly with the 'ConversationCell'
|
||||
constant: (Values.accentLineThickness + Values.mediumSpacing)
|
||||
),
|
||||
iconContainerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
iconContainerView.widthAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize),
|
||||
iconContainerView.heightAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize),
|
||||
|
||||
iconImageView.centerXAnchor.constraint(equalTo: iconContainerView.centerXAnchor),
|
||||
iconImageView.centerYAnchor.constraint(equalTo: iconContainerView.centerYAnchor),
|
||||
iconImageView.widthAnchor.constraint(equalToConstant: 25),
|
||||
iconImageView.heightAnchor.constraint(equalToConstant: 22),
|
||||
|
||||
titleLabel.leftAnchor.constraint(equalTo: iconContainerView.rightAnchor, constant: Values.mediumSpacing),
|
||||
titleLabel.rightAnchor.constraint(lessThanOrEqualTo: contentView.rightAnchor, constant: -Values.mediumSpacing),
|
||||
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
|
||||
unreadCountView.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: (Values.smallSpacing / 2)),
|
||||
unreadCountView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
|
||||
unreadCountView.widthAnchor.constraint(equalToConstant: ConversationCell.unreadCountViewSize),
|
||||
unreadCountView.heightAnchor.constraint(equalToConstant: ConversationCell.unreadCountViewSize),
|
||||
|
||||
unreadCountLabel.topAnchor.constraint(equalTo: unreadCountView.topAnchor),
|
||||
unreadCountLabel.leftAnchor.constraint(equalTo: unreadCountView.leftAnchor),
|
||||
unreadCountLabel.rightAnchor.constraint(equalTo: unreadCountView.rightAnchor),
|
||||
unreadCountLabel.bottomAnchor.constraint(equalTo: unreadCountView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
func update(with count: Int) {
|
||||
unreadCountLabel.text = "\(count)"
|
||||
unreadCountView.isHidden = (count <= 0)
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "message_requests.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.001099 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
21.607576 22.001099 m
|
||||
3.393768 22.001099 l
|
||||
1.522707 22.001099 0.000053 20.440092 0.000053 18.520739 c
|
||||
0.000053 1.402527 l
|
||||
-0.002093 1.156628 0.061090 0.914558 0.183163 0.701015 c
|
||||
0.305237 0.487473 0.481828 0.310095 0.694923 0.186979 c
|
||||
0.900043 0.066381 1.133498 0.002249 1.371506 0.001116 c
|
||||
1.609514 -0.000017 1.843569 0.061890 2.049829 0.180531 c
|
||||
6.724900 2.833986 l
|
||||
7.053913 3.022442 7.426339 3.122168 7.805599 3.123371 c
|
||||
21.606285 3.123371 l
|
||||
23.477345 3.123371 25.000000 4.684376 25.000000 6.603730 c
|
||||
25.000000 18.514294 l
|
||||
25.001291 20.436871 23.478638 22.001099 21.607576 22.001099 c
|
||||
h
|
||||
23.775423 6.603730 m
|
||||
23.775423 5.359823 22.803120 4.347942 21.607576 4.347942 c
|
||||
7.806890 4.347942 l
|
||||
7.216724 4.345131 6.637146 4.191124 6.123580 3.900650 c
|
||||
1.445283 1.244619 l
|
||||
1.425066 1.232325 1.401854 1.225824 1.378185 1.225824 c
|
||||
1.354515 1.225824 1.331300 1.232325 1.311082 1.244619 c
|
||||
1.283574 1.260406 1.261027 1.283550 1.245980 1.311449 c
|
||||
1.230933 1.339348 1.223987 1.370895 1.225920 1.402527 c
|
||||
1.225920 18.520739 l
|
||||
1.225920 19.764645 2.198225 20.776527 3.393768 20.776527 c
|
||||
21.607576 20.776527 l
|
||||
22.803120 20.776527 23.775423 19.761423 23.775423 18.514294 c
|
||||
23.775423 6.603730 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 15.369263 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
12.584548 1.987679 m
|
||||
10.633485 1.987679 9.374066 1.053783 9.333419 -1.165269 c
|
||||
11.107699 -1.165269 l
|
||||
11.202542 0.052213 11.785154 0.377046 12.584548 0.377046 c
|
||||
13.383942 0.377046 13.789767 -0.123739 13.789767 -0.718623 c
|
||||
13.789767 -1.733728 13.467169 -1.936749 11.730955 -3.073668 c
|
||||
11.730955 -4.643053 l
|
||||
13.519432 -4.643053 l
|
||||
13.519432 -3.303759 l
|
||||
14.521417 -2.802974 15.648563 -1.990891 15.648563 -0.516248 c
|
||||
15.648563 0.958394 14.589163 1.987679 12.584548 1.987679 c
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 20.065186 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
13.627180 -10.381148 m
|
||||
11.568368 -10.381148 l
|
||||
11.568368 -12.315970 l
|
||||
13.627180 -12.315970 l
|
||||
13.627180 -10.381148 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
2088
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 25.000000 22.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000002178 00000 n
|
||||
0000002201 00000 n
|
||||
0000002374 00000 n
|
||||
0000002448 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2507
|
||||
%%EOF
|
@ -0,0 +1,255 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Sodium
|
||||
import SessionMessagingKit
|
||||
|
||||
enum MockDataGenerator {
|
||||
// Note: This was taken from TensorFlow's Random (https://github.com/apple/swift/blob/bc8f9e61d333b8f7a625f74d48ef0b554726e349/stdlib/public/TensorFlow/Random.swift)
|
||||
// the complex approach is needed due to an issue with Swift's randomElement(using:)
|
||||
// generation (see https://stackoverflow.com/a/64897775 for more info)
|
||||
struct ARC4RandomNumberGenerator: RandomNumberGenerator {
|
||||
var state: [UInt8] = Array(0...255)
|
||||
var iPos: UInt8 = 0
|
||||
var jPos: UInt8 = 0
|
||||
|
||||
init<T: BinaryInteger>(seed: T) {
|
||||
self.init(
|
||||
seed: (0..<(UInt64.bitWidth / UInt64.bitWidth)).map { index in
|
||||
UInt8(truncatingIfNeeded: seed >> (UInt8.bitWidth * index))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
init(seed: [UInt8]) {
|
||||
precondition(seed.count > 0, "Length of seed must be positive")
|
||||
precondition(seed.count <= 256, "Length of seed must be at most 256")
|
||||
|
||||
// Note: Have to use a for loop instead of a 'forEach' otherwise
|
||||
// it doesn't work properly (not sure why...)
|
||||
var j: UInt8 = 0
|
||||
for i: UInt8 in 0...255 {
|
||||
j &+= S(i) &+ seed[Int(i) % seed.count]
|
||||
swapAt(i, j)
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce the next random UInt64 from the stream, and advance the internal state
|
||||
mutating func next() -> UInt64 {
|
||||
// Note: Have to use a for loop instead of a 'forEach' otherwise
|
||||
// it doesn't work properly (not sure why...)
|
||||
var result: UInt64 = 0
|
||||
for _ in 0..<UInt64.bitWidth / UInt8.bitWidth {
|
||||
result <<= UInt8.bitWidth
|
||||
result += UInt64(nextByte())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Helper to access the state
|
||||
private func S(_ index: UInt8) -> UInt8 {
|
||||
return state[Int(index)]
|
||||
}
|
||||
|
||||
/// Helper to swap elements of the state
|
||||
private mutating func swapAt(_ i: UInt8, _ j: UInt8) {
|
||||
state.swapAt(Int(i), Int(j))
|
||||
}
|
||||
|
||||
/// Generates the next byte in the keystream.
|
||||
private mutating func nextByte() -> UInt8 {
|
||||
iPos &+= 1
|
||||
jPos &+= S(iPos)
|
||||
swapAt(iPos, jPos)
|
||||
return S(S(iPos) &+ S(jPos))
|
||||
}
|
||||
}
|
||||
|
||||
static func generateMockData() {
|
||||
// Don't re-generate the mock data if it already exists
|
||||
var existingMockDataThread: TSContactThread?
|
||||
|
||||
Storage.read { transaction in
|
||||
existingMockDataThread = TSContactThread.getWithContactSessionID("MockDatabaseThread", transaction: transaction)
|
||||
}
|
||||
|
||||
guard existingMockDataThread == nil else { return }
|
||||
|
||||
/// The mock data generation is quite slow, there are 3 parts which take a decent amount of time (deleting the account afterwards will also take a long time):
|
||||
/// Generating the threads & content - ~3s per 100
|
||||
/// Writing to the database - ~10s per 1000
|
||||
/// Updating the UI - ~10s per 1000
|
||||
let dmThreadCount: Int = 100
|
||||
let closedGroupThreadCount: Int = 0
|
||||
let openGroupThreadCount: Int = 0
|
||||
let maxMessagesPerThread: Int = 50
|
||||
let dmRandomSeed: Int = 1111
|
||||
let cgRandomSeed: Int = 2222
|
||||
let ogRandomSeed: Int = 3333
|
||||
|
||||
// FIXME: Make sure this data doesn't go off device somehow?
|
||||
Storage.shared.write { anyTransaction in
|
||||
guard let transaction: YapDatabaseReadWriteTransaction = anyTransaction as? YapDatabaseReadWriteTransaction else { return }
|
||||
|
||||
// First create the thread used to indicate that the mock data has been generated
|
||||
_ = TSContactThread.getOrCreateThread(withContactSessionID: "MockDatabaseThread", transaction: transaction)
|
||||
|
||||
// Multiple spaces to make it look more like words
|
||||
let stringContent: [String] = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ".map { String($0) }
|
||||
let timestampNow: TimeInterval = Date().timeIntervalSince1970
|
||||
let userSessionId: String = getUserHexEncodedPublicKey()
|
||||
|
||||
// MARK: - -- DM Thread
|
||||
var dmThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: dmRandomSeed)
|
||||
|
||||
(0..<dmThreadCount).forEach { threadIndex in
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &dmThreadRandomGenerator) })
|
||||
|
||||
let randomSessionId: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
let isMessageRequest: Bool = Bool.random(using: &dmThreadRandomGenerator)
|
||||
let contactNameLength: Int = ((5..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
let numMessages: Int = ((0..<maxMessagesPerThread).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
|
||||
// Generate the thread
|
||||
let thread: TSContactThread = TSContactThread.getOrCreateThread(withContactSessionID: randomSessionId, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
|
||||
// Generate the contact
|
||||
let contact = Contact(sessionID: randomSessionId)
|
||||
contact.name = (0..<contactNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
contact.isApproved = (!isMessageRequest || Bool.random(using: &dmThreadRandomGenerator))
|
||||
contact.didApproveMe = (!isMessageRequest && Bool.random(using: &dmThreadRandomGenerator))
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
(0..<numMessages).forEach { index in
|
||||
let isIncoming: Bool = (
|
||||
Bool.random(using: &dmThreadRandomGenerator) &&
|
||||
(!isMessageRequest || contact.isApproved)
|
||||
)
|
||||
let messageLength: Int = ((3..<40).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
|
||||
let message: VisibleMessage = VisibleMessage()
|
||||
message.sender = (isIncoming ? randomSessionId : userSessionId)
|
||||
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)))
|
||||
message.text = (0..<messageLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
|
||||
if isIncoming {
|
||||
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
else {
|
||||
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the thread
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
|
||||
// MARK: - -- Closed Group
|
||||
var cgThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: cgRandomSeed)
|
||||
|
||||
(0..<closedGroupThreadCount).forEach { threadIndex in
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
|
||||
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
let groupNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let groupName: String = (0..<groupNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
|
||||
.joined()
|
||||
let numGroupMembers: Int = ((0..<5).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let numMessages: Int = ((0..<maxMessagesPerThread).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
|
||||
// Generate the Contacts in the group
|
||||
var members: [String] = [userSessionId]
|
||||
|
||||
(0..<numGroupMembers).forEach { _ in
|
||||
let contactData = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
|
||||
let randomSessionId: String = KeyPairUtilities.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
|
||||
let contactNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let contact = Contact(sessionID: randomSessionId)
|
||||
contact.name = (0..<contactNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
|
||||
.joined()
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
|
||||
members.append(randomSessionId)
|
||||
}
|
||||
|
||||
let groupId: Data = LKGroupUtilities.getEncodedClosedGroupIDAsData(randomGroupPublicKey)
|
||||
let group: TSGroupModel = TSGroupModel(
|
||||
title: groupName,
|
||||
memberIds: members,
|
||||
image: nil,
|
||||
groupId: groupId,
|
||||
groupType: .closedGroup,
|
||||
adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId]
|
||||
)
|
||||
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
thread.save(with: transaction)
|
||||
|
||||
// Add the group to the user's set of public keys to poll for and store the key pair
|
||||
let encryptionKeyPair = Curve25519.generateKeyPair()
|
||||
Storage.shared.addClosedGroupPublicKey(randomGroupPublicKey, using: transaction)
|
||||
Storage.shared.addClosedGroupEncryptionKeyPair(encryptionKeyPair, for: randomGroupPublicKey, using: transaction)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
(0..<numMessages).forEach { index in
|
||||
let messageLength: Int = ((3..<40).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
let message: VisibleMessage = VisibleMessage()
|
||||
message.sender = (members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId)
|
||||
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)))
|
||||
message.text = (0..<messageLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
|
||||
if message.sender != userSessionId {
|
||||
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
else {
|
||||
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the thread
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
|
||||
// MARK: - --Open Group
|
||||
var ogThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: ogRandomSeed)
|
||||
|
||||
(0..<openGroupThreadCount).forEach { threadIndex in
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &ogThreadRandomGenerator) })
|
||||
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
let serverNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
let roomNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
let serverName: String = (0..<serverNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||
.joined()
|
||||
let roomName: String = (0..<roomNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||
.joined()
|
||||
|
||||
// Create the open group model and the thread
|
||||
let openGroup: OpenGroupV2 = OpenGroupV2(server: serverName, room: roomName, name: roomName, publicKey: randomGroupPublicKey, imageID: nil)
|
||||
let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
|
||||
let model = TSGroupModel(title: openGroup.name, memberIds: [ userSessionId ], image: nil, groupId: groupId, groupType: .openGroup, adminIds: [])
|
||||
|
||||
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
thread.save(with: transaction)
|
||||
|
||||
Storage.shared.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
// Created by Michael Kirk on 12/23/16.
|
||||
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Platform {
|
||||
static let isSimulator: Bool = {
|
||||
var isSim = false
|
||||
#if arch(i386) || arch(x86_64)
|
||||
isSim = true
|
||||
#endif
|
||||
return isSim
|
||||
}()
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
|
||||
extension ConfigurationMessage {
|
||||
|
||||
public static func getCurrent(with transaction: YapDatabaseReadWriteTransaction? = nil) -> ConfigurationMessage? {
|
||||
let storage = Storage.shared
|
||||
guard let user = storage.getUser() else { return nil }
|
||||
|
||||
let displayName = user.name
|
||||
let profilePictureURL = user.profilePictureURL
|
||||
let profileKey = user.profileEncryptionKey?.keyData
|
||||
var closedGroups: Set<ClosedGroup> = []
|
||||
var openGroups: Set<String> = []
|
||||
var contacts: Set<Contact> = []
|
||||
var contactCount = 0
|
||||
|
||||
let populateDataClosure: (YapDatabaseReadTransaction) -> () = { transaction in
|
||||
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
||||
guard let thread = object as? TSGroupThread else { return }
|
||||
|
||||
switch thread.groupModel.groupType {
|
||||
case .closedGroup:
|
||||
guard thread.isCurrentUserMemberInGroup() else { return }
|
||||
|
||||
let groupID = thread.groupModel.groupId
|
||||
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
|
||||
|
||||
guard storage.isClosedGroup(groupPublicKey), let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else {
|
||||
return
|
||||
}
|
||||
|
||||
let closedGroup = ClosedGroup(
|
||||
publicKey: groupPublicKey,
|
||||
name: thread.groupModel.groupName!,
|
||||
encryptionKeyPair: encryptionKeyPair,
|
||||
members: Set(thread.groupModel.groupMemberIds),
|
||||
admins: Set(thread.groupModel.groupAdminIds),
|
||||
expirationTimer: thread.disappearingMessagesDuration(with: transaction)
|
||||
)
|
||||
closedGroups.insert(closedGroup)
|
||||
|
||||
case .openGroup:
|
||||
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
|
||||
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey()
|
||||
var truncatedContacts = storage.getAllContacts(with: transaction)
|
||||
|
||||
if truncatedContacts.count > 200 {
|
||||
truncatedContacts = Set(Array(truncatedContacts)[0..<200])
|
||||
}
|
||||
|
||||
truncatedContacts.forEach { contact in
|
||||
let publicKey = contact.sessionID
|
||||
let threadID = TSContactThread.threadID(fromContactSessionID: publicKey)
|
||||
|
||||
// Want to sync contacts for visible threads and blocked contacts between devices
|
||||
guard
|
||||
publicKey != currentUserPublicKey && (
|
||||
TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true ||
|
||||
SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(publicKey)
|
||||
)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
// Can just default the 'hasX' values to true as they will be set to this
|
||||
// when converting to proto anyway
|
||||
let profilePictureURL = contact.profilePictureURL
|
||||
let profileKey = contact.profileEncryptionKey?.keyData
|
||||
let contact = ConfigurationMessage.Contact(
|
||||
publicKey: publicKey,
|
||||
displayName: (contact.name ?? publicKey),
|
||||
profilePictureURL: profilePictureURL,
|
||||
profileKey: profileKey,
|
||||
hasIsApproved: true,
|
||||
isApproved: contact.isApproved,
|
||||
hasIsBlocked: true,
|
||||
isBlocked: contact.isBlocked,
|
||||
hasDidApproveMe: true,
|
||||
didApproveMe: contact.didApproveMe
|
||||
)
|
||||
|
||||
contacts.insert(contact)
|
||||
contactCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
// If we are provided with a transaction then read the data based on the state of the database
|
||||
// from within the transaction rather than the state in disk
|
||||
if let transaction: YapDatabaseReadWriteTransaction = transaction {
|
||||
populateDataClosure(transaction)
|
||||
}
|
||||
else {
|
||||
Storage.read { transaction in populateDataClosure(transaction) }
|
||||
}
|
||||
|
||||
return ConfigurationMessage(
|
||||
displayName: displayName,
|
||||
profilePictureURL: profilePictureURL,
|
||||
profileKey: profileKey,
|
||||
closedGroups: closedGroups,
|
||||
openGroups: openGroups,
|
||||
contacts: contacts
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNMessageRequestResponse)
|
||||
public final class MessageRequestResponse: ControlMessage {
|
||||
public var isApproved: Bool
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(isApproved: Bool) {
|
||||
self.isApproved = isApproved
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Coding
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
guard let isApproved: Bool = coder.decodeObject(forKey: "isApproved") as? Bool else { return nil }
|
||||
|
||||
self.isApproved = isApproved
|
||||
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
public override func encode(with coder: NSCoder) {
|
||||
super.encode(with: coder)
|
||||
|
||||
coder.encode(isApproved, forKey: "isApproved")
|
||||
}
|
||||
|
||||
// MARK: - Proto Conversion
|
||||
|
||||
public override class func fromProto(_ proto: SNProtoContent) -> MessageRequestResponse? {
|
||||
guard let messageRequestResponseProto = proto.messageRequestResponse else { return nil }
|
||||
|
||||
let isApproved = messageRequestResponseProto.isApproved
|
||||
|
||||
return MessageRequestResponse(isApproved: isApproved)
|
||||
}
|
||||
|
||||
public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? {
|
||||
let messageRequestResponseProto = SNProtoMessageRequestResponse.builder(isApproved: isApproved)
|
||||
let contentProto = SNProtoContent.builder()
|
||||
|
||||
do {
|
||||
contentProto.setMessageRequestResponse(try messageRequestResponseProto.build())
|
||||
return try contentProto.build()
|
||||
} catch {
|
||||
SNLog("Couldn't construct unsend request proto from: \(self).")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Description
|
||||
|
||||
public override var description: String {
|
||||
"""
|
||||
MessageRequestResponse(
|
||||
isApproved: \(isApproved)
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x58",
|
||||
"green" : "0x58",
|
||||
"red" : "0x58"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x43",
|
||||
"green" : "0x43",
|
||||
"red" : "0x43"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xAD",
|
||||
"green" : "0xAD",
|
||||
"red" : "0xAD"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x9F",
|
||||
"green" : "0x9F",
|
||||
"red" : "0x9F"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0x00",
|
||||
"red" : "0x00"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xAD",
|
||||
"green" : "0xAD",
|
||||
"red" : "0xAD"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
@objc(SNMessageRequestsMigration)
|
||||
public class MessageRequestsMigration : OWSDatabaseMigration {
|
||||
|
||||
@objc
|
||||
class func migrationId() -> String {
|
||||
return "002"
|
||||
}
|
||||
|
||||
override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) {
|
||||
self.doMigrationAsync(completion: completion)
|
||||
}
|
||||
|
||||
private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) {
|
||||
var contacts: Set<Contact> = Set()
|
||||
var threads: [TSThread] = []
|
||||
|
||||
TSThread.enumerateCollectionObjects { object, _ in
|
||||
guard let thread: TSThread = object as? TSThread else { return }
|
||||
|
||||
if let contactThread: TSContactThread = thread as? TSContactThread {
|
||||
let sessionId: String = contactThread.contactSessionID()
|
||||
|
||||
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
|
||||
contact.isApproved = true
|
||||
contact.didApproveMe = true
|
||||
contacts.insert(contact)
|
||||
}
|
||||
}
|
||||
else if let groupThread: TSGroupThread = thread as? TSGroupThread, groupThread.isClosedGroup {
|
||||
let groupAdmins: [String] = groupThread.groupModel.groupAdminIds
|
||||
|
||||
groupAdmins.forEach { sessionId in
|
||||
if let contact: Contact = Storage.shared.getContact(with: sessionId) {
|
||||
contact.isApproved = true
|
||||
contact.didApproveMe = true
|
||||
contacts.insert(contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
threads.append(thread)
|
||||
}
|
||||
|
||||
Storage.write(with: { transaction in
|
||||
contacts.forEach { contact in
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
}
|
||||
threads.forEach { thread in
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
self.save(with: transaction) // Intentionally capture self
|
||||
}, completion: {
|
||||
completion()
|
||||
})
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
|
||||
extension ConfigurationMessage {
|
||||
|
||||
public static func getCurrent() -> ConfigurationMessage? {
|
||||
let storage = Storage.shared
|
||||
guard let user = storage.getUser() else { return nil }
|
||||
let displayName = user.name
|
||||
let profilePictureURL = user.profilePictureURL
|
||||
let profileKey = user.profileEncryptionKey?.keyData
|
||||
var closedGroups: Set<ClosedGroup> = []
|
||||
var openGroups: Set<String> = []
|
||||
var contacts: Set<Contact> = []
|
||||
var contactCount = 0
|
||||
Storage.read { transaction in
|
||||
TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
||||
guard let thread = object as? TSGroupThread else { return }
|
||||
switch thread.groupModel.groupType {
|
||||
case .closedGroup:
|
||||
guard thread.isCurrentUserMemberInGroup() else { return }
|
||||
let groupID = thread.groupModel.groupId
|
||||
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
|
||||
guard storage.isClosedGroup(groupPublicKey),
|
||||
let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { return }
|
||||
let closedGroup = ClosedGroup(publicKey: groupPublicKey, name: thread.groupModel.groupName!, encryptionKeyPair: encryptionKeyPair,
|
||||
members: Set(thread.groupModel.groupMemberIds), admins: Set(thread.groupModel.groupAdminIds), expirationTimer: thread.disappearingMessagesDuration(with: transaction))
|
||||
closedGroups.insert(closedGroup)
|
||||
case .openGroup:
|
||||
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
|
||||
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
var truncatedContacts = storage.getAllContacts()
|
||||
if truncatedContacts.count > 200 { truncatedContacts = Set(Array(truncatedContacts)[0..<200]) }
|
||||
truncatedContacts.forEach { contact in
|
||||
let publicKey = contact.sessionID
|
||||
let threadID = TSContactThread.threadID(fromContactSessionID: publicKey)
|
||||
guard let thread = TSContactThread.fetch(uniqueId: threadID, transaction: transaction), thread.shouldBeVisible
|
||||
&& !SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(publicKey) else { return }
|
||||
let profilePictureURL = contact.profilePictureURL
|
||||
let profileKey = contact.profileEncryptionKey?.keyData
|
||||
let contact = ConfigurationMessage.Contact(publicKey: publicKey, displayName: contact.name ?? publicKey,
|
||||
profilePictureURL: profilePictureURL, profileKey: profileKey)
|
||||
contacts.insert(contact)
|
||||
contactCount += 1
|
||||
}
|
||||
}
|
||||
return ConfigurationMessage(displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey,
|
||||
closedGroups: closedGroups, openGroups: openGroups, contacts: contacts)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue