Fixed the QA issues and a few other bugs
Updated the convoInfoVolatile to only ever set `last_read` to the maximum between the current and updated values Fixed an issue where deleting the Note to Self and One-to-one conversations wouldn't reset the 'pinnedPriority' value Fixed an issue with updating legacy group members and losing admin status Fixed an issue where receiving a 'NEW' legacy group control message could revert legacy group changes Fixed a bug where the open group suggestion grid could have broken positioning depending on the number of items Fixed a bug where the UI wouldn't update correctly when network access was lost Fixed a fun bug where one-to-one conversations could reappear after deletion because a new snode was polled and the latest (locally deleted) message was received again Fixed some incorrect accessibility values
This commit is contained in:
parent
fde34a6c45
commit
c80b6c720e
|
@ -555,7 +555,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.writeAsync { db in
|
||||||
_ = try SessionThread
|
_ = try SessionThread // Intentionally use `deleteAll` here instead of `deleteOrLeave`
|
||||||
.filter(id: threadId)
|
.filter(id: threadId)
|
||||||
.deleteAll(db)
|
.deleteAll(db)
|
||||||
}
|
}
|
||||||
|
@ -601,8 +601,18 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
}),
|
}),
|
||||||
let unblindedId: String = blindedLookup.sessionId
|
let unblindedId: String = blindedLookup.sessionId
|
||||||
else {
|
else {
|
||||||
// If we don't have an unblinded id then something has gone very wrong so pop to the HomeVC
|
// If we don't have an unblinded id then something has gone very wrong so pop to the
|
||||||
self?.navigationController?.popToRootViewController(animated: true)
|
// nearest conversation list
|
||||||
|
let maybeTargetViewController: UIViewController? = self?.navigationController?
|
||||||
|
.viewControllers
|
||||||
|
.last(where: { ($0 as? SessionUtilRespondingViewController)?.isConversationList == true })
|
||||||
|
|
||||||
|
if let targetViewController: UIViewController = maybeTargetViewController {
|
||||||
|
self?.navigationController?.popToViewController(targetViewController, animated: true)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self?.navigationController?.popToRootViewController(animated: true)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1246,7 +1256,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
target: self,
|
target: self,
|
||||||
action: #selector(startCall)
|
action: #selector(startCall)
|
||||||
)
|
)
|
||||||
callButton.accessibilityLabel = "Call button"
|
callButton.accessibilityLabel = "Call"
|
||||||
callButton.isAccessibilityElement = true
|
callButton.isAccessibilityElement = true
|
||||||
|
|
||||||
navigationItem.rightBarButtonItems = [settingsButtonItem, callButton]
|
navigationItem.rightBarButtonItems = [settingsButtonItem, callButton]
|
||||||
|
|
|
@ -301,9 +301,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
||||||
lastSearchText: String?
|
lastSearchText: String?
|
||||||
) {
|
) {
|
||||||
self.viewModel = cellViewModel
|
self.viewModel = cellViewModel
|
||||||
self.bubbleView.accessibilityIdentifier = "Message Body"
|
|
||||||
self.bubbleView.isAccessibilityElement = true
|
|
||||||
self.bubbleView.accessibilityLabel = cellViewModel.body
|
|
||||||
// We want to add spacing between "clusters" of messages to indicate that time has
|
// We want to add spacing between "clusters" of messages to indicate that time has
|
||||||
// passed (even if there wasn't enough time to warrant showing a date header)
|
// passed (even if there wasn't enough time to warrant showing a date header)
|
||||||
let shouldAddTopInset: Bool = (
|
let shouldAddTopInset: Bool = (
|
||||||
|
@ -362,6 +360,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
||||||
lastSearchText: lastSearchText
|
lastSearchText: lastSearchText
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bubbleView.accessibilityIdentifier = "Message Body"
|
||||||
|
bubbleView.accessibilityLabel = bodyTappableLabel?.attributedText?.string
|
||||||
|
bubbleView.isAccessibilityElement = true
|
||||||
|
|
||||||
// Author label
|
// Author label
|
||||||
authorLabelTopConstraint.constant = (shouldAddTopInset ? Values.mediumSpacing : 0)
|
authorLabelTopConstraint.constant = (shouldAddTopInset ? Values.mediumSpacing : 0)
|
||||||
authorLabel.isHidden = (cellViewModel.senderName == nil)
|
authorLabel.isHidden = (cellViewModel.senderName == nil)
|
||||||
|
|
|
@ -285,6 +285,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
||||||
),
|
),
|
||||||
backgroundStyle: .noBackground
|
backgroundStyle: .noBackground
|
||||||
),
|
),
|
||||||
|
accessibility: SessionCell.Accessibility(
|
||||||
|
identifier: "Username",
|
||||||
|
label: threadViewModel.displayName
|
||||||
|
),
|
||||||
onTap: {
|
onTap: {
|
||||||
self?.textChanged(self?.oldDisplayName, for: .nickname)
|
self?.textChanged(self?.oldDisplayName, for: .nickname)
|
||||||
self?.setIsEditing(true)
|
self?.setIsEditing(true)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import SessionUtilitiesKit
|
||||||
import SignalUtilitiesKit
|
import SignalUtilitiesKit
|
||||||
import SignalCoreKit
|
import SignalCoreKit
|
||||||
|
|
||||||
class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
|
class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, UITableViewDelegate, UITableViewDataSource {
|
||||||
fileprivate typealias SectionModel = ArraySection<SearchSection, SessionThreadViewModel>
|
fileprivate typealias SectionModel = ArraySection<SearchSection, SessionThreadViewModel>
|
||||||
|
|
||||||
// MARK: - SearchSection
|
// MARK: - SearchSection
|
||||||
|
@ -20,6 +20,15 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
||||||
case messages
|
case messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - SessionUtilRespondingViewController
|
||||||
|
|
||||||
|
let isConversationList: Bool = true
|
||||||
|
|
||||||
|
func forceRefreshIfNeeded() {
|
||||||
|
// Need to do this as the 'GlobalSearchViewController' doesn't observe database changes
|
||||||
|
updateSearchResults(searchText: searchText, force: true)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Variables
|
// MARK: - Variables
|
||||||
|
|
||||||
private lazy var defaultSearchResults: [SectionModel] = {
|
private lazy var defaultSearchResults: [SectionModel] = {
|
||||||
|
@ -152,7 +161,7 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateSearchResults(searchText rawSearchText: String) {
|
private func updateSearchResults(searchText rawSearchText: String, force: Bool = false) {
|
||||||
let searchText = rawSearchText.stripped
|
let searchText = rawSearchText.stripped
|
||||||
|
|
||||||
guard searchText.count > 0 else {
|
guard searchText.count > 0 else {
|
||||||
|
@ -161,7 +170,7 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
||||||
reloadTableData()
|
reloadTableData()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard lastSearchText != searchText else { return }
|
guard force || lastSearchText != searchText else { return }
|
||||||
|
|
||||||
lastSearchText = searchText
|
lastSearchText = searchText
|
||||||
|
|
||||||
|
|
|
@ -368,44 +368,13 @@ public class HomeViewModel {
|
||||||
|
|
||||||
public func deleteOrLeave(threadId: String, threadVariant: SessionThread.Variant) {
|
public func deleteOrLeave(threadId: String, threadVariant: SessionThread.Variant) {
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.writeAsync { db in
|
||||||
switch threadVariant {
|
try SessionThread.deleteOrLeave(
|
||||||
case .contact:
|
db,
|
||||||
// We need to custom handle the 'Note to Self' conversation (it should just be
|
threadId: threadId,
|
||||||
// hidden rather than deleted
|
threadVariant: threadVariant,
|
||||||
guard threadId != getUserHexEncodedPublicKey(db) else {
|
shouldSendLeaveMessageForGroups: true,
|
||||||
_ = try Interaction
|
calledFromConfigHandling: false
|
||||||
.filter(Interaction.Columns.threadId == threadId)
|
)
|
||||||
.deleteAll(db)
|
|
||||||
|
|
||||||
_ = try SessionThread
|
|
||||||
.filter(id: threadId)
|
|
||||||
.updateAllAndConfig(
|
|
||||||
db,
|
|
||||||
SessionThread.Columns.shouldBeVisible.set(to: false)
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try SessionUtil
|
|
||||||
.hide(db, contactIds: [threadId])
|
|
||||||
|
|
||||||
case .legacyGroup, .group:
|
|
||||||
MessageSender
|
|
||||||
.leave(db, groupPublicKey: threadId)
|
|
||||||
.sinkUntilComplete()
|
|
||||||
|
|
||||||
case .community:
|
|
||||||
OpenGroupManager.shared.delete(
|
|
||||||
db,
|
|
||||||
openGroupId: threadId,
|
|
||||||
calledFromConfigHandling: false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = try SessionThread
|
|
||||||
.filter(id: threadId)
|
|
||||||
.deleteAll(db)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import SessionUIKit
|
||||||
import SessionMessagingKit
|
import SessionMessagingKit
|
||||||
import SignalUtilitiesKit
|
import SignalUtilitiesKit
|
||||||
|
|
||||||
class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
|
class MessageRequestsViewController: BaseVC, SessionUtilRespondingViewController, UITableViewDelegate, UITableViewDataSource {
|
||||||
private static let loadingHeaderHeight: CGFloat = 40
|
private static let loadingHeaderHeight: CGFloat = 40
|
||||||
|
|
||||||
private let viewModel: MessageRequestsViewModel = MessageRequestsViewModel()
|
private let viewModel: MessageRequestsViewModel = MessageRequestsViewModel()
|
||||||
|
@ -17,6 +17,10 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
||||||
private var isAutoLoadingNextPage: Bool = false
|
private var isAutoLoadingNextPage: Bool = false
|
||||||
private var viewHasAppeared: Bool = false
|
private var viewHasAppeared: Bool = false
|
||||||
|
|
||||||
|
// MARK: - SessionUtilRespondingViewController
|
||||||
|
|
||||||
|
let isConversationList: Bool = true
|
||||||
|
|
||||||
// MARK: - Intialization
|
// MARK: - Intialization
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -466,7 +470,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
||||||
.filter { $0.threadVariant == .contact }
|
.filter { $0.threadVariant == .contact }
|
||||||
.map { $0.threadId })
|
.map { $0.threadId })
|
||||||
.defaulting(to: [])
|
.defaulting(to: [])
|
||||||
let closedGroupThreadIds: [String] = (viewModel.threadData
|
let groupThreadIds: [String] = (viewModel.threadData
|
||||||
.first { $0.model == .threads }?
|
.first { $0.model == .threads }?
|
||||||
.elements
|
.elements
|
||||||
.filter { $0.threadVariant == .legacyGroup || $0.threadVariant == .group }
|
.filter { $0.threadVariant == .legacyGroup || $0.threadVariant == .group }
|
||||||
|
@ -483,7 +487,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
||||||
) { _ in
|
) { _ in
|
||||||
MessageRequestsViewModel.clearAllRequests(
|
MessageRequestsViewModel.clearAllRequests(
|
||||||
contactThreadIds: contactThreadIds,
|
contactThreadIds: contactThreadIds,
|
||||||
closedGroupThreadIds: closedGroupThreadIds
|
groupThreadIds: groupThreadIds
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil))
|
alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil))
|
||||||
|
|
|
@ -185,28 +185,13 @@ public class MessageRequestsViewModel {
|
||||||
cancelStyle: .alert_text
|
cancelStyle: .alert_text
|
||||||
) { _ in
|
) { _ in
|
||||||
Storage.shared.write { db in
|
Storage.shared.write { db in
|
||||||
switch threadVariant {
|
try SessionThread.deleteOrLeave(
|
||||||
case .contact:
|
db,
|
||||||
try SessionUtil
|
threadId: threadId,
|
||||||
.hide(db, contactIds: [threadId])
|
threadVariant: threadVariant,
|
||||||
|
shouldSendLeaveMessageForGroups: false,
|
||||||
_ = try SessionThread
|
calledFromConfigHandling: false
|
||||||
.filter(id: threadId)
|
)
|
||||||
.deleteAll(db)
|
|
||||||
|
|
||||||
case .legacyGroup, .group:
|
|
||||||
try ClosedGroup.removeKeysAndUnsubscribe(
|
|
||||||
db,
|
|
||||||
threadId: threadId,
|
|
||||||
removeGroupData: true,
|
|
||||||
calledFromConfigHandling: false
|
|
||||||
)
|
|
||||||
|
|
||||||
// Trigger a config sync
|
|
||||||
ConfigurationSyncJob.enqueue(db, publicKey: getUserHexEncodedPublicKey(db))
|
|
||||||
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
completion?()
|
completion?()
|
||||||
|
@ -250,14 +235,13 @@ public class MessageRequestsViewModel {
|
||||||
Contact.Columns.didApproveMe.set(to: true)
|
Contact.Columns.didApproveMe.set(to: true)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sync the removal of the thread from other devices
|
try SessionThread.deleteOrLeave(
|
||||||
try SessionUtil
|
db,
|
||||||
.hide(db, contactIds: [threadId])
|
threadId: threadId,
|
||||||
|
threadVariant: .contact,
|
||||||
// Remove the thread
|
shouldSendLeaveMessageForGroups: false,
|
||||||
_ = try SessionThread
|
calledFromConfigHandling: false
|
||||||
.filter(id: threadId)
|
)
|
||||||
.deleteAll(db)
|
|
||||||
},
|
},
|
||||||
completion: { _, _ in completion?() }
|
completion: { _, _ in completion?() }
|
||||||
)
|
)
|
||||||
|
@ -269,24 +253,25 @@ public class MessageRequestsViewModel {
|
||||||
|
|
||||||
static func clearAllRequests(
|
static func clearAllRequests(
|
||||||
contactThreadIds: [String],
|
contactThreadIds: [String],
|
||||||
closedGroupThreadIds: [String]
|
groupThreadIds: [String]
|
||||||
) {
|
) {
|
||||||
// Clear the requests
|
// Clear the requests
|
||||||
Storage.shared.write { db in
|
Storage.shared.write { db in
|
||||||
// Sync the removal of the thread from other devices
|
// Remove the one-to-one requests
|
||||||
try SessionUtil
|
try SessionThread.deleteOrLeave(
|
||||||
.hide(db, contactIds: contactThreadIds)
|
|
||||||
|
|
||||||
// Remove the threads
|
|
||||||
_ = try SessionThread
|
|
||||||
.filter(ids: contactThreadIds)
|
|
||||||
.deleteAll(db)
|
|
||||||
|
|
||||||
// Remove the groups
|
|
||||||
try ClosedGroup.removeKeysAndUnsubscribe(
|
|
||||||
db,
|
db,
|
||||||
threadIds: closedGroupThreadIds,
|
threadIds: contactThreadIds,
|
||||||
removeGroupData: true,
|
threadVariant: .contact,
|
||||||
|
shouldSendLeaveMessageForGroups: false,
|
||||||
|
calledFromConfigHandling: false
|
||||||
|
)
|
||||||
|
|
||||||
|
// Remove the group requests
|
||||||
|
try SessionThread.deleteOrLeave(
|
||||||
|
db,
|
||||||
|
threadIds: groupThreadIds,
|
||||||
|
threadVariant: .group,
|
||||||
|
shouldSendLeaveMessageForGroups: false,
|
||||||
calledFromConfigHandling: false
|
calledFromConfigHandling: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,7 +115,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
||||||
|
|
||||||
createViews()
|
createViews()
|
||||||
|
|
||||||
reachability = Reachability.forInternetConnection()
|
reachability = Environment.shared?.reachabilityManager.reachability
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(reachabilityChanged),
|
selector: #selector(reachabilityChanged),
|
||||||
|
|
|
@ -18,7 +18,7 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
|
||||||
|
|
||||||
private static let cellHeight: CGFloat = 40
|
private static let cellHeight: CGFloat = 40
|
||||||
private static let separatorWidth = Values.separatorThickness
|
private static let separatorWidth = Values.separatorThickness
|
||||||
private static let numHorizontalCells: CGFloat = (UIDevice.current.isIPad ? 4 : 2)
|
fileprivate static let numHorizontalCells: Int = (UIDevice.current.isIPad ? 4 : 2)
|
||||||
|
|
||||||
private lazy var layout: LastRowCenteredLayout = {
|
private lazy var layout: LastRowCenteredLayout = {
|
||||||
let result = LastRowCenteredLayout()
|
let result = LastRowCenteredLayout()
|
||||||
|
@ -157,7 +157,7 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
|
||||||
spinner.isHidden = true
|
spinner.isHidden = true
|
||||||
|
|
||||||
let roomCount: CGFloat = CGFloat(min(rooms.count, 8)) // Cap to a maximum of 8 (4 rows of 2)
|
let roomCount: CGFloat = CGFloat(min(rooms.count, 8)) // Cap to a maximum of 8 (4 rows of 2)
|
||||||
let numRows: CGFloat = ceil(roomCount / OpenGroupSuggestionGrid.numHorizontalCells)
|
let numRows: CGFloat = ceil(roomCount / CGFloat(OpenGroupSuggestionGrid.numHorizontalCells))
|
||||||
let height: CGFloat = ((OpenGroupSuggestionGrid.cellHeight * numRows) + ((numRows - 1) * layout.minimumLineSpacing))
|
let height: CGFloat = ((OpenGroupSuggestionGrid.cellHeight * numRows) + ((numRows - 1) * layout.minimumLineSpacing))
|
||||||
heightConstraint.constant = height
|
heightConstraint.constant = height
|
||||||
collectionView.reloadData()
|
collectionView.reloadData()
|
||||||
|
@ -172,18 +172,18 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
|
||||||
// MARK: - Layout
|
// MARK: - Layout
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||||
guard
|
let totalItems: Int = collectionView.numberOfItems(inSection: indexPath.section)
|
||||||
indexPath.item == (collectionView.numberOfItems(inSection: indexPath.section) - 1) &&
|
let itemsInFinalRow: Int = (totalItems % OpenGroupSuggestionGrid.numHorizontalCells)
|
||||||
indexPath.item % 2 == 0
|
|
||||||
else {
|
guard indexPath.item >= (totalItems - itemsInFinalRow) && itemsInFinalRow != 0 else {
|
||||||
let cellWidth: CGFloat = ((maxWidth / OpenGroupSuggestionGrid.numHorizontalCells) - ((OpenGroupSuggestionGrid.numHorizontalCells - 1) * layout.minimumInteritemSpacing))
|
let cellWidth: CGFloat = ((maxWidth / CGFloat(OpenGroupSuggestionGrid.numHorizontalCells)) - ((CGFloat(OpenGroupSuggestionGrid.numHorizontalCells) - 1) * layout.minimumInteritemSpacing))
|
||||||
|
|
||||||
return CGSize(width: cellWidth, height: OpenGroupSuggestionGrid.cellHeight)
|
return CGSize(width: cellWidth, height: OpenGroupSuggestionGrid.cellHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the last item is by itself then we want to make it wider
|
// If there isn't an even number of items then we want to calculate proper sizing
|
||||||
return CGSize(
|
return CGSize(
|
||||||
width: (Cell.calculatedWith(for: rooms[indexPath.item].name)),
|
width: Cell.calculatedWith(for: rooms[indexPath.item].name),
|
||||||
height: OpenGroupSuggestionGrid.cellHeight
|
height: OpenGroupSuggestionGrid.cellHeight
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -380,16 +380,30 @@ class LastRowCenteredLayout: UICollectionViewFlowLayout {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
guard
|
guard
|
||||||
(elementAttributes?.count ?? 0) % 2 == 1,
|
let remainingItems: Int = elementAttributes.map({ $0.count % OpenGroupSuggestionGrid.numHorizontalCells }),
|
||||||
let lastItemAttributes: UICollectionViewLayoutAttributes = elementAttributes?.last
|
remainingItems != 0,
|
||||||
|
let lastItems: [UICollectionViewLayoutAttributes] = elementAttributes?.suffix(remainingItems),
|
||||||
|
!lastItems.isEmpty
|
||||||
else { return elementAttributes }
|
else { return elementAttributes }
|
||||||
|
|
||||||
lastItemAttributes.frame = CGRect(
|
let totalItemWidth: CGFloat = lastItems
|
||||||
x: ((targetViewWidth - lastItemAttributes.frame.size.width) / 2),
|
.map { $0.frame.size.width }
|
||||||
y: lastItemAttributes.frame.origin.y,
|
.reduce(0, +)
|
||||||
width: lastItemAttributes.frame.size.width,
|
let lastRowWidth: CGFloat = (totalItemWidth + (CGFloat(lastItems.count - 1) * minimumInteritemSpacing))
|
||||||
height: lastItemAttributes.frame.size.height
|
|
||||||
)
|
// Offset the start width by half of the remaining space
|
||||||
|
var itemXPos: CGFloat = ((targetViewWidth - lastRowWidth) / 2)
|
||||||
|
|
||||||
|
lastItems.forEach { item in
|
||||||
|
item.frame = CGRect(
|
||||||
|
x: itemXPos,
|
||||||
|
y: item.frame.origin.y,
|
||||||
|
width: item.frame.size.width,
|
||||||
|
height: item.frame.size.height
|
||||||
|
)
|
||||||
|
|
||||||
|
itemXPos += (item.frame.size.width + minimumInteritemSpacing)
|
||||||
|
}
|
||||||
|
|
||||||
return elementAttributes
|
return elementAttributes
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import UIKit
|
||||||
import Reachability
|
import Reachability
|
||||||
import SessionUIKit
|
import SessionUIKit
|
||||||
import SessionSnodeKit
|
import SessionSnodeKit
|
||||||
|
import SessionMessagingKit
|
||||||
|
|
||||||
final class PathStatusView: UIView {
|
final class PathStatusView: UIView {
|
||||||
enum Size {
|
enum Size {
|
||||||
|
@ -44,7 +45,7 @@ final class PathStatusView: UIView {
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
private let size: Size
|
private let size: Size
|
||||||
private let reachability: Reachability = Reachability.forInternetConnection()
|
private let reachability: Reachability? = Environment.shared?.reachabilityManager.reachability
|
||||||
|
|
||||||
init(size: Size = .small) {
|
init(size: Size = .small) {
|
||||||
self.size = size
|
self.size = size
|
||||||
|
@ -76,10 +77,10 @@ final class PathStatusView: UIView {
|
||||||
self.set(.width, to: self.size.pointSize)
|
self.set(.width, to: self.size.pointSize)
|
||||||
self.set(.height, to: self.size.pointSize)
|
self.set(.height, to: self.size.pointSize)
|
||||||
|
|
||||||
switch (reachability.isReachable(), OnionRequestAPI.paths.isEmpty) {
|
switch (reachability?.isReachable(), OnionRequestAPI.paths.isEmpty) {
|
||||||
case (false, _): setStatus(to: .error)
|
case (.some(false), _), (nil, _): setStatus(to: .error)
|
||||||
case (true, true): setStatus(to: .connecting)
|
case (.some(true), true): setStatus(to: .connecting)
|
||||||
case (true, false): setStatus(to: .connected)
|
case (.some(true), false): setStatus(to: .connected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +125,7 @@ final class PathStatusView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleBuildingPathsNotification() {
|
@objc private func handleBuildingPathsNotification() {
|
||||||
guard reachability.isReachable() else {
|
guard reachability?.isReachable() == true else {
|
||||||
setStatus(to: .error)
|
setStatus(to: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -133,7 +134,7 @@ final class PathStatusView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handlePathsBuiltNotification() {
|
@objc private func handlePathsBuiltNotification() {
|
||||||
guard reachability.isReachable() else {
|
guard reachability?.isReachable() == true else {
|
||||||
setStatus(to: .error)
|
setStatus(to: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -147,7 +148,7 @@ final class PathStatusView: UIView {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard reachability.isReachable() else {
|
guard reachability?.isReachable() == true else {
|
||||||
setStatus(to: .error)
|
setStatus(to: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -241,7 +241,7 @@ private final class LineView: UIView {
|
||||||
private var dotViewWidthConstraint: NSLayoutConstraint!
|
private var dotViewWidthConstraint: NSLayoutConstraint!
|
||||||
private var dotViewHeightConstraint: NSLayoutConstraint!
|
private var dotViewHeightConstraint: NSLayoutConstraint!
|
||||||
private var dotViewAnimationTimer: Timer!
|
private var dotViewAnimationTimer: Timer!
|
||||||
private let reachability: Reachability = Reachability.forInternetConnection()
|
private let reachability: Reachability? = Environment.shared?.reachabilityManager.reachability
|
||||||
|
|
||||||
enum Location {
|
enum Location {
|
||||||
case top, middle, bottom
|
case top, middle, bottom
|
||||||
|
@ -326,10 +326,10 @@ private final class LineView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (reachability.isReachable(), OnionRequestAPI.paths.isEmpty) {
|
switch (reachability?.isReachable(), OnionRequestAPI.paths.isEmpty) {
|
||||||
case (false, _): setStatus(to: .error)
|
case (.some(false), _), (nil, _): setStatus(to: .error)
|
||||||
case (true, true): setStatus(to: .connecting)
|
case (.some(true), true): setStatus(to: .connecting)
|
||||||
case (true, false): setStatus(to: .connected)
|
case (.some(true), false): setStatus(to: .connected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,7 +380,7 @@ private final class LineView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleBuildingPathsNotification() {
|
@objc private func handleBuildingPathsNotification() {
|
||||||
guard reachability.isReachable() else {
|
guard reachability?.isReachable() == true else {
|
||||||
setStatus(to: .error)
|
setStatus(to: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -389,7 +389,7 @@ private final class LineView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handlePathsBuiltNotification() {
|
@objc private func handlePathsBuiltNotification() {
|
||||||
guard reachability.isReachable() else {
|
guard reachability?.isReachable() == true else {
|
||||||
setStatus(to: .error)
|
setStatus(to: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -403,7 +403,7 @@ private final class LineView: UIView {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard reachability.isReachable() else {
|
guard reachability?.isReachable() == true else {
|
||||||
setStatus(to: .error)
|
setStatus(to: .error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ public class SessionCell: UITableViewCell {
|
||||||
return result
|
return result
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private let titleLabel: SRCopyableLabel = {
|
fileprivate let titleLabel: SRCopyableLabel = {
|
||||||
let result: SRCopyableLabel = SRCopyableLabel()
|
let result: SRCopyableLabel = SRCopyableLabel()
|
||||||
result.translatesAutoresizingMaskIntoConstraints = false
|
result.translatesAutoresizingMaskIntoConstraints = false
|
||||||
result.isUserInteractionEnabled = false
|
result.isUserInteractionEnabled = false
|
||||||
|
@ -586,7 +586,16 @@ public class SessionCell: UITableViewCell {
|
||||||
|
|
||||||
extension CombineCompatible where Self: SessionCell {
|
extension CombineCompatible where Self: SessionCell {
|
||||||
var textPublisher: AnyPublisher<String, Never> {
|
var textPublisher: AnyPublisher<String, Never> {
|
||||||
return self.titleTextField.publisher(for: .editingChanged)
|
return self.titleTextField.publisher(for: [.editingChanged, .editingDidEnd])
|
||||||
|
.handleEvents(
|
||||||
|
receiveOutput: { [weak self] textField in
|
||||||
|
// When editing the text update the 'accessibilityLabel' of the cell to match
|
||||||
|
// the text
|
||||||
|
let targetText: String? = (textField.isEditing ? textField.text : self?.titleLabel.text)
|
||||||
|
self?.accessibilityLabel = (targetText ?? self?.accessibilityLabel)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.filter { $0.isEditing } // Don't bother sending events for 'editingDidEnd'
|
||||||
.map { textField -> String in (textField.text ?? "") }
|
.map { textField -> String in (textField.text ?? "") }
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,7 +164,7 @@ public extension ClosedGroup {
|
||||||
|
|
||||||
// Remove the remaining group data if desired
|
// Remove the remaining group data if desired
|
||||||
if removeGroupData {
|
if removeGroupData {
|
||||||
try SessionThread
|
try SessionThread // Intentionally use `deleteAll` here as this gets triggered via `deleteOrLeave`
|
||||||
.filter(ids: threadIds)
|
.filter(ids: threadIds)
|
||||||
.deleteAll(db)
|
.deleteAll(db)
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,15 @@ public struct ControlMessageProcessRecord: Codable, FetchableRecord, Persistable
|
||||||
case unsendRequest = 7
|
case unsendRequest = 7
|
||||||
case messageRequestResponse = 8
|
case messageRequestResponse = 8
|
||||||
case call = 9
|
case call = 9
|
||||||
|
|
||||||
|
/// Since we retrieve messages from all snodes in a swarm there is a fun issue where a user can delete a
|
||||||
|
/// one-to-one conversation (which removes all associated interactions) and then the poller checks a
|
||||||
|
/// different service node, if a previously processed message hadn't been processed yet for that specific
|
||||||
|
/// service node it results in the conversation re-appearing
|
||||||
|
///
|
||||||
|
/// This `Variant` allows us to create a record which survives thread deletion to prevent a duplicate
|
||||||
|
/// message from being reprocessed
|
||||||
|
case visibleMessageDedupe = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The id for the thread the control message is associated to
|
/// The id for the thread the control message is associated to
|
||||||
|
@ -68,10 +77,6 @@ public struct ControlMessageProcessRecord: Codable, FetchableRecord, Persistable
|
||||||
message: Message,
|
message: Message,
|
||||||
serverExpirationTimestamp: TimeInterval?
|
serverExpirationTimestamp: TimeInterval?
|
||||||
) {
|
) {
|
||||||
// All `VisibleMessage` values will have an associated `Interaction` so just let
|
|
||||||
// the unique constraints on that table prevent duplicate messages
|
|
||||||
if message is VisibleMessage { return nil }
|
|
||||||
|
|
||||||
// Allow duplicates for UnsendRequest messages, if a user received an UnsendRequest
|
// Allow duplicates for UnsendRequest messages, if a user received an UnsendRequest
|
||||||
// as a push notification the it wouldn't include a serverHash and, as a result,
|
// as a push notification the it wouldn't include a serverHash and, as a result,
|
||||||
// wouldn't get deleted from the server - since the logic only runs if we find a
|
// wouldn't get deleted from the server - since the logic only runs if we find a
|
||||||
|
@ -113,6 +118,7 @@ public struct ControlMessageProcessRecord: Codable, FetchableRecord, Persistable
|
||||||
case is UnsendRequest: return .unsendRequest
|
case is UnsendRequest: return .unsendRequest
|
||||||
case is MessageRequestResponse: return .messageRequestResponse
|
case is MessageRequestResponse: return .messageRequestResponse
|
||||||
case is CallMessage: return .call
|
case is CallMessage: return .call
|
||||||
|
case is VisibleMessage: return .visibleMessageDedupe
|
||||||
default: preconditionFailure("[ControlMessageProcessRecord] Unsupported message type")
|
default: preconditionFailure("[ControlMessageProcessRecord] Unsupported message type")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
import SessionUtilitiesKit
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public struct GroupMember: Codable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
public struct GroupMember: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||||
public static var databaseTableName: String { "groupMember" }
|
public static var databaseTableName: String { "groupMember" }
|
||||||
internal static let openGroupForeignKey = ForeignKey([Columns.groupId], to: [OpenGroup.Columns.threadId])
|
internal static let openGroupForeignKey = ForeignKey([Columns.groupId], to: [OpenGroup.Columns.threadId])
|
||||||
internal static let closedGroupForeignKey = ForeignKey([Columns.groupId], to: [ClosedGroup.Columns.threadId])
|
internal static let closedGroupForeignKey = ForeignKey([Columns.groupId], to: [ClosedGroup.Columns.threadId])
|
||||||
|
|
|
@ -285,6 +285,89 @@ public extension SessionThread {
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func deleteOrLeave(
|
||||||
|
_ db: Database,
|
||||||
|
threadId: String,
|
||||||
|
threadVariant: Variant,
|
||||||
|
shouldSendLeaveMessageForGroups: Bool,
|
||||||
|
calledFromConfigHandling: Bool
|
||||||
|
) throws {
|
||||||
|
try deleteOrLeave(
|
||||||
|
db,
|
||||||
|
threadIds: [threadId],
|
||||||
|
threadVariant: threadVariant,
|
||||||
|
shouldSendLeaveMessageForGroups: shouldSendLeaveMessageForGroups,
|
||||||
|
calledFromConfigHandling: calledFromConfigHandling
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func deleteOrLeave(
|
||||||
|
_ db: Database,
|
||||||
|
threadIds: [String],
|
||||||
|
threadVariant: Variant,
|
||||||
|
shouldSendLeaveMessageForGroups: Bool,
|
||||||
|
calledFromConfigHandling: Bool
|
||||||
|
) throws {
|
||||||
|
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||||
|
let remainingThreadIds: [String] = threadIds.filter { $0 != currentUserPublicKey }
|
||||||
|
|
||||||
|
switch threadVariant {
|
||||||
|
case .contact:
|
||||||
|
// We need to custom handle the 'Note to Self' conversation (it should just be
|
||||||
|
// hidden rather than deleted
|
||||||
|
if threadIds.contains(currentUserPublicKey) {
|
||||||
|
_ = try Interaction
|
||||||
|
.filter(Interaction.Columns.threadId == currentUserPublicKey)
|
||||||
|
.deleteAll(db)
|
||||||
|
|
||||||
|
_ = try SessionThread
|
||||||
|
.filter(id: currentUserPublicKey)
|
||||||
|
.updateAllAndConfig(
|
||||||
|
db,
|
||||||
|
SessionThread.Columns.pinnedPriority.set(to: 0),
|
||||||
|
SessionThread.Columns.shouldBeVisible.set(to: false)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this wasn't called from config handling then we need to hide the conversation
|
||||||
|
if !calledFromConfigHandling {
|
||||||
|
try SessionUtil
|
||||||
|
.hide(db, contactIds: threadIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .legacyGroup, .group:
|
||||||
|
if shouldSendLeaveMessageForGroups {
|
||||||
|
threadIds.forEach { threadId in
|
||||||
|
MessageSender
|
||||||
|
.leave(db, groupPublicKey: threadId)
|
||||||
|
.sinkUntilComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try ClosedGroup.removeKeysAndUnsubscribe(
|
||||||
|
db,
|
||||||
|
threadIds: threadIds,
|
||||||
|
removeGroupData: true,
|
||||||
|
calledFromConfigHandling: calledFromConfigHandling
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .community:
|
||||||
|
threadIds.forEach { threadId in
|
||||||
|
OpenGroupManager.shared.delete(
|
||||||
|
db,
|
||||||
|
openGroupId: threadId,
|
||||||
|
calledFromConfigHandling: calledFromConfigHandling
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = try SessionThread
|
||||||
|
.filter(ids: remainingThreadIds)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Convenience
|
// MARK: - Convenience
|
||||||
|
|
|
@ -171,8 +171,13 @@ internal extension SessionUtil {
|
||||||
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: [contact.id])
|
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: [contact.id])
|
||||||
|
|
||||||
try SessionThread
|
try SessionThread
|
||||||
.filter(id: contact.id)
|
.deleteOrLeave(
|
||||||
.deleteAll(db)
|
db,
|
||||||
|
threadId: contact.id,
|
||||||
|
threadVariant: .contact,
|
||||||
|
shouldSendLeaveMessageForGroups: false,
|
||||||
|
calledFromConfigHandling: true
|
||||||
|
)
|
||||||
|
|
||||||
case (true, false):
|
case (true, false):
|
||||||
try SessionThread(
|
try SessionThread(
|
||||||
|
@ -230,8 +235,13 @@ internal extension SessionUtil {
|
||||||
|
|
||||||
// Delete the one-to-one conversations associated to the contact
|
// Delete the one-to-one conversations associated to the contact
|
||||||
try SessionThread
|
try SessionThread
|
||||||
.filter(ids: contactIdsToRemove)
|
.deleteOrLeave(
|
||||||
.deleteAll(db)
|
db,
|
||||||
|
threadIds: contactIdsToRemove,
|
||||||
|
threadVariant: .contact,
|
||||||
|
shouldSendLeaveMessageForGroups: false,
|
||||||
|
calledFromConfigHandling: true
|
||||||
|
)
|
||||||
|
|
||||||
try SessionUtil.remove(db, volatileContactIds: contactIdsToRemove)
|
try SessionUtil.remove(db, volatileContactIds: contactIdsToRemove)
|
||||||
}
|
}
|
||||||
|
@ -441,7 +451,13 @@ public extension SessionUtil {
|
||||||
// Mark the contacts as hidden
|
// Mark the contacts as hidden
|
||||||
try SessionUtil.upsert(
|
try SessionUtil.upsert(
|
||||||
contactData: contactIds
|
contactData: contactIds
|
||||||
.map { SyncedContactInfo(id: $0, hidden: true) },
|
.map {
|
||||||
|
SyncedContactInfo(
|
||||||
|
id: $0,
|
||||||
|
hidden: true,
|
||||||
|
priority: 0
|
||||||
|
)
|
||||||
|
},
|
||||||
in: conf
|
in: conf
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,7 +199,7 @@ internal extension SessionUtil {
|
||||||
threadInfo.changes.forEach { change in
|
threadInfo.changes.forEach { change in
|
||||||
switch change {
|
switch change {
|
||||||
case .lastReadTimestampMs(let lastReadMs):
|
case .lastReadTimestampMs(let lastReadMs):
|
||||||
oneToOne.last_read = lastReadMs
|
oneToOne.last_read = max(oneToOne.last_read, lastReadMs)
|
||||||
|
|
||||||
case .markedAsUnread(let unread):
|
case .markedAsUnread(let unread):
|
||||||
oneToOne.unread = unread
|
oneToOne.unread = unread
|
||||||
|
@ -218,7 +218,7 @@ internal extension SessionUtil {
|
||||||
threadInfo.changes.forEach { change in
|
threadInfo.changes.forEach { change in
|
||||||
switch change {
|
switch change {
|
||||||
case .lastReadTimestampMs(let lastReadMs):
|
case .lastReadTimestampMs(let lastReadMs):
|
||||||
legacyGroup.last_read = lastReadMs
|
legacyGroup.last_read = max(legacyGroup.last_read, lastReadMs)
|
||||||
|
|
||||||
case .markedAsUnread(let unread):
|
case .markedAsUnread(let unread):
|
||||||
legacyGroup.unread = unread
|
legacyGroup.unread = unread
|
||||||
|
@ -246,7 +246,7 @@ internal extension SessionUtil {
|
||||||
threadInfo.changes.forEach { change in
|
threadInfo.changes.forEach { change in
|
||||||
switch change {
|
switch change {
|
||||||
case .lastReadTimestampMs(let lastReadMs):
|
case .lastReadTimestampMs(let lastReadMs):
|
||||||
community.last_read = lastReadMs
|
community.last_read = max(community.last_read, lastReadMs)
|
||||||
|
|
||||||
case .markedAsUnread(let unread):
|
case .markedAsUnread(let unread):
|
||||||
community.unread = unread
|
community.unread = unread
|
||||||
|
|
|
@ -209,24 +209,74 @@ internal extension SessionUtil {
|
||||||
// Extract the ones which will respond to SessionUtil changes
|
// Extract the ones which will respond to SessionUtil changes
|
||||||
let targetViewControllers: [any SessionUtilRespondingViewController] = navController
|
let targetViewControllers: [any SessionUtilRespondingViewController] = navController
|
||||||
.viewControllers
|
.viewControllers
|
||||||
.compactMap({ $0 as? SessionUtilRespondingViewController })
|
.compactMap { $0 as? SessionUtilRespondingViewController }
|
||||||
|
let presentedNavController: UINavigationController? = (navController.presentedViewController as? UINavigationController)
|
||||||
|
let presentedTargetViewControllers: [any SessionUtilRespondingViewController] = (presentedNavController?
|
||||||
|
.viewControllers
|
||||||
|
.compactMap { $0 as? SessionUtilRespondingViewController })
|
||||||
|
.defaulting(to: [])
|
||||||
|
|
||||||
// Make sure we have a conversation list and that one of the removed conversations are
|
// Make sure we have a conversation list and that one of the removed conversations are
|
||||||
// in the nav hierarchy
|
// in the nav hierarchy
|
||||||
guard
|
let rootNavControllerNeedsPop: Bool = (
|
||||||
targetViewControllers.count > 1,
|
targetViewControllers.count > 1 &&
|
||||||
targetViewControllers.contains(where: { $0.isConversationList }),
|
targetViewControllers.contains(where: { $0.isConversationList }) &&
|
||||||
targetViewControllers.contains(where: { $0.isConversation(in: removedThreadIds) })
|
targetViewControllers.contains(where: { $0.isConversation(in: removedThreadIds) })
|
||||||
else { return }
|
)
|
||||||
|
let presentedNavControllerNeedsPop: Bool = (
|
||||||
|
presentedTargetViewControllers.count > 1 &&
|
||||||
|
presentedTargetViewControllers.contains(where: { $0.isConversationList }) &&
|
||||||
|
presentedTargetViewControllers.contains(where: { $0.isConversation(in: removedThreadIds) })
|
||||||
|
)
|
||||||
|
|
||||||
// Return to the root view controller as the removed conversation will be invalid
|
// Force the UI to refresh if needed (most screens should do this automatically via database
|
||||||
if navController.presentedViewController != nil {
|
// observation, but a couple of screens don't so need to be done manually)
|
||||||
navController.dismiss(animated: false) {
|
targetViewControllers
|
||||||
navController.popToRootViewController(animated: true)
|
.appending(contentsOf: presentedTargetViewControllers)
|
||||||
}
|
.filter { $0.isConversationList }
|
||||||
}
|
.forEach { $0.forceRefreshIfNeeded() }
|
||||||
else {
|
|
||||||
navController.popToRootViewController(animated: true)
|
switch (rootNavControllerNeedsPop, presentedNavControllerNeedsPop) {
|
||||||
|
case (true, false):
|
||||||
|
// Return to the conversation list as the removed conversation will be invalid
|
||||||
|
guard
|
||||||
|
let targetViewController: UIViewController = navController.viewControllers
|
||||||
|
.last(where: { viewController in
|
||||||
|
((viewController as? SessionUtilRespondingViewController)?.isConversationList)
|
||||||
|
.defaulting(to: false)
|
||||||
|
})
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
if navController.presentedViewController != nil {
|
||||||
|
navController.dismiss(animated: false) {
|
||||||
|
navController.popToViewController(targetViewController, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
navController.popToViewController(targetViewController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
case (false, true):
|
||||||
|
// Return to the conversation list as the removed conversation will be invalid
|
||||||
|
guard
|
||||||
|
let targetViewController: UIViewController = presentedNavController?
|
||||||
|
.viewControllers
|
||||||
|
.last(where: { viewController in
|
||||||
|
((viewController as? SessionUtilRespondingViewController)?.isConversationList)
|
||||||
|
.defaulting(to: false)
|
||||||
|
})
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
if presentedNavController?.presentedViewController != nil {
|
||||||
|
presentedNavController?.dismiss(animated: false) {
|
||||||
|
presentedNavController?.popToViewController(targetViewController, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
presentedNavController?.popToViewController(targetViewController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
default: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,7 +306,12 @@ public extension SessionUtil {
|
||||||
var cThreadId: [CChar] = threadId.cArray
|
var cThreadId: [CChar] = threadId.cArray
|
||||||
|
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .contact: return contacts_get(conf, nil, &cThreadId)
|
case .contact:
|
||||||
|
var contact: contacts_contact = contacts_contact()
|
||||||
|
|
||||||
|
guard contacts_get(conf, &contact, &cThreadId) else { return false }
|
||||||
|
|
||||||
|
return !contact.hidden
|
||||||
|
|
||||||
case .community:
|
case .community:
|
||||||
let maybeUrlInfo: OpenGroupUrlInfo? = Storage.shared
|
let maybeUrlInfo: OpenGroupUrlInfo? = Storage.shared
|
||||||
|
@ -267,8 +322,9 @@ public extension SessionUtil {
|
||||||
|
|
||||||
var cBaseUrl: [CChar] = urlInfo.server.cArray
|
var cBaseUrl: [CChar] = urlInfo.server.cArray
|
||||||
var cRoom: [CChar] = urlInfo.roomToken.cArray
|
var cRoom: [CChar] = urlInfo.roomToken.cArray
|
||||||
|
var community: ugroups_community_info = ugroups_community_info()
|
||||||
|
|
||||||
return user_groups_get_community(conf, nil, &cBaseUrl, &cRoom)
|
return user_groups_get_community(conf, &community, &cBaseUrl, &cRoom)
|
||||||
|
|
||||||
case .legacyGroup:
|
case .legacyGroup:
|
||||||
let groupInfo: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cThreadId)
|
let groupInfo: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cThreadId)
|
||||||
|
@ -331,10 +387,12 @@ public protocol SessionUtilRespondingViewController {
|
||||||
var isConversationList: Bool { get }
|
var isConversationList: Bool { get }
|
||||||
|
|
||||||
func isConversation(in threadIds: [String]) -> Bool
|
func isConversation(in threadIds: [String]) -> Bool
|
||||||
|
func forceRefreshIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension SessionUtilRespondingViewController {
|
public extension SessionUtilRespondingViewController {
|
||||||
var isConversationList: Bool { false }
|
var isConversationList: Bool { false }
|
||||||
|
|
||||||
func isConversation(in threadIds: [String]) -> Bool { return false }
|
func isConversation(in threadIds: [String]) -> Bool { return false }
|
||||||
|
func forceRefreshIfNeeded() {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ internal extension SessionUtil {
|
||||||
),
|
),
|
||||||
groupMembers: members
|
groupMembers: members
|
||||||
.filter { _, isAdmin in !isAdmin }
|
.filter { _, isAdmin in !isAdmin }
|
||||||
.map { memberId, admin in
|
.map { memberId, _ in
|
||||||
GroupMember(
|
GroupMember(
|
||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
profileId: memberId,
|
profileId: memberId,
|
||||||
|
@ -98,7 +98,7 @@ internal extension SessionUtil {
|
||||||
},
|
},
|
||||||
groupAdmins: members
|
groupAdmins: members
|
||||||
.filter { _, isAdmin in isAdmin }
|
.filter { _, isAdmin in isAdmin }
|
||||||
.map { memberId, admin in
|
.map { memberId, _ in
|
||||||
GroupMember(
|
GroupMember(
|
||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
profileId: memberId,
|
profileId: memberId,
|
||||||
|
@ -171,13 +171,14 @@ internal extension SessionUtil {
|
||||||
if !communityIdsToRemove.isEmpty {
|
if !communityIdsToRemove.isEmpty {
|
||||||
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: Array(communityIdsToRemove))
|
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: Array(communityIdsToRemove))
|
||||||
|
|
||||||
communityIdsToRemove.forEach { threadId in
|
try SessionThread
|
||||||
OpenGroupManager.shared.delete(
|
.deleteOrLeave(
|
||||||
db,
|
db,
|
||||||
openGroupId: threadId,
|
threadIds: Array(communityIdsToRemove),
|
||||||
|
threadVariant: .community,
|
||||||
|
shouldSendLeaveMessageForGroups: false,
|
||||||
calledFromConfigHandling: true
|
calledFromConfigHandling: true
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -- Handle Legacy Group Changes
|
// MARK: -- Handle Legacy Group Changes
|
||||||
|
@ -200,7 +201,7 @@ internal extension SessionUtil {
|
||||||
let name: String = group.name,
|
let name: String = group.name,
|
||||||
let lastKeyPair: ClosedGroupKeyPair = group.lastKeyPair,
|
let lastKeyPair: ClosedGroupKeyPair = group.lastKeyPair,
|
||||||
let members: [GroupMember] = group.groupMembers,
|
let members: [GroupMember] = group.groupMembers,
|
||||||
let updatedAdmins: [GroupMember] = group.groupAdmins
|
let updatedAdmins: Set<GroupMember> = group.groupAdmins?.asSet()
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
if !existingLegacyGroupIds.contains(group.id) {
|
if !existingLegacyGroupIds.contains(group.id) {
|
||||||
|
@ -214,7 +215,8 @@ internal extension SessionUtil {
|
||||||
secretKey: lastKeyPair.secretKey.bytes
|
secretKey: lastKeyPair.secretKey.bytes
|
||||||
),
|
),
|
||||||
members: members
|
members: members
|
||||||
.appending(contentsOf: updatedAdmins) // Admins should also have 'standard' member entries
|
.asSet()
|
||||||
|
.inserting(contentsOf: updatedAdmins) // Admins should also have 'standard' member entries
|
||||||
.map { $0.profileId },
|
.map { $0.profileId },
|
||||||
admins: updatedAdmins.map { $0.profileId },
|
admins: updatedAdmins.map { $0.profileId },
|
||||||
expirationTimer: UInt32(group.disappearingConfig?.durationSeconds ?? 0),
|
expirationTimer: UInt32(group.disappearingConfig?.durationSeconds ?? 0),
|
||||||
|
@ -253,7 +255,7 @@ internal extension SessionUtil {
|
||||||
.saved(db)
|
.saved(db)
|
||||||
|
|
||||||
// Update the members
|
// Update the members
|
||||||
let updatedMembers: [GroupMember] = members
|
let updatedMembers: Set<GroupMember> = members
|
||||||
.appending(
|
.appending(
|
||||||
contentsOf: updatedAdmins.map { admin in
|
contentsOf: updatedAdmins.map { admin in
|
||||||
GroupMember(
|
GroupMember(
|
||||||
|
@ -264,10 +266,12 @@ internal extension SessionUtil {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.asSet()
|
||||||
|
|
||||||
if
|
if
|
||||||
let existingMembers: [GroupMember] = existingLegacyGroupMembers[group.id]?
|
let existingMembers: Set<GroupMember> = existingLegacyGroupMembers[group.id]?
|
||||||
.filter({ $0.role == .standard || $0.role == .zombie }),
|
.filter({ $0.role == .standard || $0.role == .zombie })
|
||||||
|
.asSet(),
|
||||||
existingMembers != updatedMembers
|
existingMembers != updatedMembers
|
||||||
{
|
{
|
||||||
// Add in any new members and remove any removed members
|
// Add in any new members and remove any removed members
|
||||||
|
@ -288,8 +292,9 @@ internal extension SessionUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
if
|
if
|
||||||
let existingAdmins: [GroupMember] = existingLegacyGroupMembers[group.id]?
|
let existingAdmins: Set<GroupMember> = existingLegacyGroupMembers[group.id]?
|
||||||
.filter({ $0.role == .admin }),
|
.filter({ $0.role == .admin })
|
||||||
|
.asSet(),
|
||||||
existingAdmins != updatedAdmins
|
existingAdmins != updatedAdmins
|
||||||
{
|
{
|
||||||
// Add in any new admins and remove any removed admins
|
// Add in any new admins and remove any removed admins
|
||||||
|
@ -326,12 +331,14 @@ internal extension SessionUtil {
|
||||||
if !legacyGroupIdsToRemove.isEmpty {
|
if !legacyGroupIdsToRemove.isEmpty {
|
||||||
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: Array(legacyGroupIdsToRemove))
|
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: Array(legacyGroupIdsToRemove))
|
||||||
|
|
||||||
try ClosedGroup.removeKeysAndUnsubscribe(
|
try SessionThread
|
||||||
db,
|
.deleteOrLeave(
|
||||||
threadIds: Array(legacyGroupIdsToRemove),
|
db,
|
||||||
removeGroupData: true,
|
threadIds: Array(legacyGroupIdsToRemove),
|
||||||
calledFromConfigHandling: true
|
threadVariant: .legacyGroup,
|
||||||
)
|
shouldSendLeaveMessageForGroups: false,
|
||||||
|
calledFromConfigHandling: true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -- Handle Group Changes
|
// MARK: -- Handle Group Changes
|
||||||
|
@ -364,8 +371,6 @@ internal extension SessionUtil {
|
||||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||||
guard !legacyGroups.isEmpty else { return }
|
guard !legacyGroups.isEmpty else { return }
|
||||||
|
|
||||||
// Since we are doing direct memory manipulation we are using an `Atomic` type which has
|
|
||||||
// blocking access in it's `mutate` closure
|
|
||||||
legacyGroups
|
legacyGroups
|
||||||
.forEach { legacyGroup in
|
.forEach { legacyGroup in
|
||||||
var cGroupId: [CChar] = legacyGroup.id.cArray
|
var cGroupId: [CChar] = legacyGroup.id.cArray
|
||||||
|
@ -405,7 +410,12 @@ internal extension SessionUtil {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if let groupMembers: [GroupMember] = legacyGroup.groupMembers {
|
if let groupMembers: [GroupMember] = legacyGroup.groupMembers {
|
||||||
let memberIds: Set<String> = groupMembers.map { $0.profileId }.asSet()
|
// Need to make sure we remove any admins before adding them here otherwise we will
|
||||||
|
// overwrite the admin permission to be a standard user permission
|
||||||
|
let memberIds: Set<String> = groupMembers
|
||||||
|
.map { $0.profileId }
|
||||||
|
.asSet()
|
||||||
|
.subtracting(legacyGroup.groupAdmins.defaulting(to: []).map { $0.profileId }.asSet())
|
||||||
let existingMemberIds: Set<String> = Array(existingMembers
|
let existingMemberIds: Set<String> = Array(existingMembers
|
||||||
.filter { _, isAdmin in !isAdmin }
|
.filter { _, isAdmin in !isAdmin }
|
||||||
.keys)
|
.keys)
|
||||||
|
@ -555,6 +565,19 @@ public extension SessionUtil {
|
||||||
for: .userGroups,
|
for: .userGroups,
|
||||||
publicKey: getUserHexEncodedPublicKey(db)
|
publicKey: getUserHexEncodedPublicKey(db)
|
||||||
) { conf in
|
) { conf in
|
||||||
|
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||||
|
|
||||||
|
var cGroupId: [CChar] = groupPublicKey.cArray
|
||||||
|
let userGroup: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cGroupId)
|
||||||
|
|
||||||
|
// Need to make sure the group doesn't already exist (otherwise we will end up overriding the
|
||||||
|
// content which could revert newer changes since this can be triggered from other 'NEW' messages
|
||||||
|
// coming in from the legacy group swarm)
|
||||||
|
guard userGroup == nil else {
|
||||||
|
ugroups_legacy_group_free(userGroup)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try SessionUtil.upsert(
|
try SessionUtil.upsert(
|
||||||
legacyGroups: [
|
legacyGroups: [
|
||||||
LegacyGroupInfo(
|
LegacyGroupInfo(
|
||||||
|
|
|
@ -98,6 +98,20 @@ internal extension SessionUtil {
|
||||||
db,
|
db,
|
||||||
SessionThread.Columns.pinnedPriority.set(to: targetPriority)
|
SessionThread.Columns.pinnedPriority.set(to: targetPriority)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If the 'Note to Self' conversation is hidden then we should trigger the proper
|
||||||
|
// `deleteOrLeave` behaviour (for 'Note to Self' this will leave the conversation
|
||||||
|
// but remove the associated interactions)
|
||||||
|
if targetHiddenState {
|
||||||
|
try SessionThread
|
||||||
|
.deleteOrLeave(
|
||||||
|
db,
|
||||||
|
threadId: userPublicKey,
|
||||||
|
threadVariant: .contact,
|
||||||
|
shouldSendLeaveMessageForGroups: false,
|
||||||
|
calledFromConfigHandling: true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a contact for the current user if needed (also force-approve the current user
|
// Create a contact for the current user if needed (also force-approve the current user
|
||||||
|
|
|
@ -93,8 +93,13 @@ extension MessageReceiver {
|
||||||
.updateAll(db, Interaction.Columns.threadId.set(to: unblindedThread.id))
|
.updateAll(db, Interaction.Columns.threadId.set(to: unblindedThread.id))
|
||||||
|
|
||||||
_ = try SessionThread
|
_ = try SessionThread
|
||||||
.filter(id: blindedIdLookup.blindedId)
|
.deleteOrLeave(
|
||||||
.deleteAll(db)
|
db,
|
||||||
|
threadId: blindedIdLookup.blindedId,
|
||||||
|
threadVariant: .contact,
|
||||||
|
shouldSendLeaveMessageForGroups: false,
|
||||||
|
calledFromConfigHandling: false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the `didApproveMe` state of the sender
|
// Update the `didApproveMe` state of the sender
|
||||||
|
|
|
@ -7,6 +7,8 @@ public enum ReachabilityType: Int {
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public protocol SSKReachabilityManager {
|
public protocol SSKReachabilityManager {
|
||||||
|
var reachability: Reachability { get }
|
||||||
|
|
||||||
var observationContext: AnyObject { get }
|
var observationContext: AnyObject { get }
|
||||||
func setup()
|
func setup()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue