Fixed a few bugs uncovered with further testing

Added some more logs to libSession build script and tweaked the stdout location
Added shadow threads to the GarbageCollectionJob
Changed the seed node retries to 2 because it's likely we will swap to another seed node pretty quickly which could resolve the issue
Fixed a bug where the user could get kicked from a draft conversation if they get a contacts update before sending a message
Fixed a bug where message status or media message download statuses would trigger the conversation to jump to the bottom
This commit is contained in:
Morgan Pretty 2023-05-10 17:27:36 +10:00
parent 6685dc0572
commit 977c2051ed
10 changed files with 124 additions and 33 deletions

View File

@ -25,19 +25,32 @@
# Need to set the path or we won't find cmake # Need to set the path or we won't find cmake
PATH=${PATH}:/usr/local/bin:/opt/homebrew/bin:/sbin/md5 PATH=${PATH}:/usr/local/bin:/opt/homebrew/bin:/sbin/md5
# Direct the output to a log file # Ensure the build directory exists (in case we need it before XCode creates it)
exec > "${TARGET_BUILD_DIR}/libsession_util_output.log" 2>&1 mkdir -p "${TARGET_BUILD_DIR}"
# Remove any old build errors # Remove any old build errors
rm -rf "${TARGET_BUILD_DIR}/libsession_util_error.log" rm -rf "${TARGET_BUILD_DIR}/libsession_util_error.log"
# First ensure cmake is installed (store the error in a log and exit with a success status - xcode will output the error) # First ensure cmake is installed (store the error in a log and exit with a success status - xcode will output the error)
echo "info: Validating build requirements"
if ! which cmake > /dev/null; then if ! which cmake > /dev/null; then
echo "error: cmake is required to build, please install (can install via homebrew with 'brew install cmake')." > "${TARGET_BUILD_DIR}/error.log" touch "${TARGET_BUILD_DIR}/libsession_util_error.log"
echo "error: cmake is required to build, please install (can install via homebrew with 'brew install cmake')."
echo "error: cmake is required to build, please install (can install via homebrew with 'brew install cmake')." > "${TARGET_BUILD_DIR}/libsession_util_error.log"
exit 0 exit 0
fi fi
if [ ! -d "${SRCROOT}/LibSession-Util" ] || [ ! -d "${SRCROOT}/LibSession-Util/src" ]; then
touch "${TARGET_BUILD_DIR}/libsession_util_error.log"
echo "error: Need to fetch LibSession-Util submodule."
echo "error: Need to fetch LibSession-Util submodule." > "${TARGET_BUILD_DIR}/libsession_util_error.log"
exit 1
fi
# Generate a hash of the libSession-util source files and check if they differ from the last hash # Generate a hash of the libSession-util source files and check if they differ from the last hash
echo "info: Checking for changes to source"
NEW_SOURCE_HASH=$(find "${SRCROOT}/LibSession-Util/src" -type f -exec md5 {} + | awk '{print $NF}' | sort | md5 | awk '{print $NF}') NEW_SOURCE_HASH=$(find "${SRCROOT}/LibSession-Util/src" -type f -exec md5 {} + | awk '{print $NF}' | sort | md5 | awk '{print $NF}')
NEW_HEADER_HASH=$(find "${SRCROOT}/LibSession-Util/include" -type f -exec md5 {} + | awk '{print $NF}' | sort | md5 | awk '{print $NF}') NEW_HEADER_HASH=$(find "${SRCROOT}/LibSession-Util/include" -type f -exec md5 {} + | awk '{print $NF}' | sort | md5 | awk '{print $NF}')
@ -62,13 +75,15 @@ if [ "${NEW_SOURCE_HASH}" != "${OLD_SOURCE_HASH}" ] || [ "${NEW_HEADER_HASH}" !=
rm -rf "${TARGET_BUILD_DIR}/libsession-util.a" rm -rf "${TARGET_BUILD_DIR}/libsession-util.a"
rm -rf "${TARGET_BUILD_DIR}/libsession-util.xcframework" rm -rf "${TARGET_BUILD_DIR}/libsession-util.xcframework"
rm -rf "${BUILD_DIR}/libsession-util.xcframework" rm -rf "${BUILD_DIR}/libsession-util.xcframework"
# Trigger the new build # Trigger the new build
cd "${SRCROOT}/LibSession-Util" cd "${SRCROOT}/LibSession-Util"
result=$(./utils/ios.sh "libsession-util" false) result=$(./utils/ios.sh "libsession-util" false)
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "error: Failed to build libsession-util (See details in '${TARGET_BUILD_DIR}/pre-action-output.log')." > "${TARGET_BUILD_DIR}/error.log" touch "${TARGET_BUILD_DIR}/libsession_util_error.log"
echo "error: Failed to build libsession-util (See details in '${TARGET_BUILD_DIR}/pre-action-output.log')."
echo "error: Failed to build libsession-util (See details in '${TARGET_BUILD_DIR}/pre-action-output.log')." > "${TARGET_BUILD_DIR}/libsession_util_error.log"
exit 0 exit 0
fi fi
@ -76,6 +91,10 @@ if [ "${NEW_SOURCE_HASH}" != "${OLD_SOURCE_HASH}" ] || [ "${NEW_HEADER_HASH}" !=
echo "${NEW_SOURCE_HASH}" > "${TARGET_BUILD_DIR}/libsession_util_source_hash.log" echo "${NEW_SOURCE_HASH}" > "${TARGET_BUILD_DIR}/libsession_util_source_hash.log"
echo "${NEW_HEADER_HASH}" > "${TARGET_BUILD_DIR}/libsession_util_header_hash.log" echo "${NEW_HEADER_HASH}" > "${TARGET_BUILD_DIR}/libsession_util_header_hash.log"
echo "${ARCHS[*]}" > "${TARGET_BUILD_DIR}/libsession_util_archs.log" echo "${ARCHS[*]}" > "${TARGET_BUILD_DIR}/libsession_util_archs.log"
echo ""
echo "info: Build complete"
else
echo "info: Build is up-to-date"
fi fi
# Move the target-specific libSession-util build to the parent build directory (so XCode can have a reference to a single build) # Move the target-specific libSession-util build to the parent build directory (so XCode can have a reference to a single build)

View File

@ -734,6 +734,11 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
initialIsBlocked: (viewModel.threadData.threadIsBlocked == true) initialIsBlocked: (viewModel.threadData.threadIsBlocked == true)
) )
messageRequestDescriptionLabel.text = (updatedThreadData.threadRequiresApproval == false ?
"MESSAGE_REQUESTS_INFO".localized() :
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO".localized()
)
let messageRequestsViewWasVisible: Bool = ( let messageRequestsViewWasVisible: Bool = (
messageRequestStackView.isHidden == false messageRequestStackView.isHidden == false
) )
@ -865,15 +870,18 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
self.viewModel.updateInteractionData(updatedData) self.viewModel.updateInteractionData(updatedData)
self.tableView.reloadData() self.tableView.reloadData()
// We need to dispatch to the next run loop because it seems trying to scroll immediately after // If we just sent a message then we want to jump to the bottom of the conversation instantly
// triggering a 'reloadData' doesn't work if didSendMessageBeforeUpdate {
DispatchQueue.main.async { [weak self] in // We need to dispatch to the next run loop because it seems trying to scroll immediately after
self?.scrollToBottom(isAnimated: false) // triggering a 'reloadData' doesn't work
DispatchQueue.main.async { [weak self] in
// Note: The scroll button alpha won't get set correctly in this case so we forcibly set it to self?.scrollToBottom(isAnimated: false)
// have an alpha of 0 to stop it appearing buggy
self?.scrollButton.alpha = 0 // Note: The scroll button alpha won't get set correctly in this case so we forcibly set it to
self?.unreadCountView.alpha = 0 // have an alpha of 0 to stop it appearing buggy
self?.scrollButton.alpha = 0
self?.unreadCountView.alpha = 0
}
} }
return return
} }

View File

@ -41,10 +41,6 @@ class SessionTableViewModel<NavItemId: Equatable, Section: SessionTableSection,
open var title: String { preconditionFailure("abstract class - override in subclass") } open var title: String { preconditionFailure("abstract class - override in subclass") }
open var emptyStateTextPublisher: AnyPublisher<String?, Never> { Just(nil).eraseToAnyPublisher() } open var emptyStateTextPublisher: AnyPublisher<String?, Never> { Just(nil).eraseToAnyPublisher() }
open var settingsData: [SectionModel] { preconditionFailure("abstract class - override in subclass") }
open var observableSettingsData: ObservableData {
preconditionFailure("abstract class - override in subclass")
}
open var footerView: AnyPublisher<UIView?, Never> { Just(nil).eraseToAnyPublisher() } open var footerView: AnyPublisher<UIView?, Never> { Just(nil).eraseToAnyPublisher() }
open var footerButtonInfo: AnyPublisher<SessionButton.Info?, Never> { open var footerButtonInfo: AnyPublisher<SessionButton.Info?, Never> {
Just(nil).eraseToAnyPublisher() Just(nil).eraseToAnyPublisher()

View File

@ -296,6 +296,34 @@ public enum GarbageCollectionJob: JobExecutor {
.filter(PendingReadReceipt.Columns.serverExpirationTimestamp <= timestampNow) .filter(PendingReadReceipt.Columns.serverExpirationTimestamp <= timestampNow)
.deleteAll(db) .deleteAll(db)
} }
if finalTypesToCollect.contains(.shadowThreads) {
// Shadow threads are thread records which were created to start a conversation that
// didn't actually get turned into conversations (ie. the app was closed or crashed
// before the user sent a message)
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias()
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
let closedGroup: TypedTableAlias<ClosedGroup> = TypedTableAlias()
try db.execute(literal: """
DELETE FROM \(SessionThread.self)
WHERE \(Column.rowID) IN (
SELECT \(thread.alias[Column.rowID])
FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])
LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
WHERE (
\(contact[.id]) IS NULL AND
\(openGroup[.threadId]) IS NULL AND
\(closedGroup[.threadId]) IS NULL AND
\(thread[.shouldBeVisible]) = false AND
\(SQL("\(thread[.id]) != \(getUserHexEncodedPublicKey(db))"))
)
)
""")
}
}, },
completion: { _, _ in completion: { _, _ in
// Dispatch async so we can swap from the write queue to a read one (we are done writing) // Dispatch async so we can swap from the write queue to a read one (we are done writing)
@ -450,6 +478,7 @@ extension GarbageCollectionJob {
case orphanedAttachmentFiles case orphanedAttachmentFiles
case orphanedProfileAvatars case orphanedProfileAvatars
case expiredPendingReadReceipts case expiredPendingReadReceipts
case shadowThreads
} }
public struct Details: Codable { public struct Details: Codable {

View File

@ -206,6 +206,28 @@ public extension Message {
} }
} }
static func threadId(forMessage message: Message, destination: Message.Destination) -> String {
switch destination {
case .contact(let publicKey):
// Extract the 'syncTarget' value if there is one
let maybeSyncTarget: String?
switch message {
case let message as VisibleMessage: maybeSyncTarget = message.syncTarget
case let message as ExpirationTimerUpdate: maybeSyncTarget = message.syncTarget
default: maybeSyncTarget = nil
}
return (maybeSyncTarget ?? publicKey)
case .closedGroup(let groupPublicKey): return groupPublicKey
case .openGroup(let roomToken, let server, _, _, _):
return OpenGroup.idFor(roomToken: roomToken, server: server)
case .openGroupInbox(_, _, let blindedPublicKey): return blindedPublicKey
}
}
static func processRawReceivedMessage( static func processRawReceivedMessage(
_ db: Database, _ db: Database,
rawMessage: SnodeReceivedMessage rawMessage: SnodeReceivedMessage

View File

@ -382,6 +382,12 @@ public final class OpenGroupManager {
.filter(id: openGroupId) .filter(id: openGroupId)
.deleteAll(db) .deleteAll(db)
// Remove any MessageProcessRecord entries (we will want to reprocess all OpenGroup messages
// if they get re-added)
_ = try? ControlMessageProcessRecord
.filter(ControlMessageProcessRecord.Columns.threadId == openGroupId)
.deleteAll(db)
// Remove the open group (no foreign key to the thread so it won't auto-delete) // Remove the open group (no foreign key to the thread so it won't auto-delete)
if server?.lowercased() != OpenGroupAPI.defaultServer.lowercased() { if server?.lowercased() != OpenGroupAPI.defaultServer.lowercased() {
_ = try? OpenGroup _ = try? OpenGroup

View File

@ -961,16 +961,8 @@ public final class MessageSender {
} }
} }
let threadId: String = { // Extract the threadId from the message
switch destination { let threadId: String = Message.threadId(forMessage: message, destination: destination)
case .contact(let publicKey): return publicKey
case .closedGroup(let groupPublicKey): return groupPublicKey
case .openGroup(let roomToken, let server, _, _, _):
return OpenGroup.idFor(roomToken: roomToken, server: server)
case .openGroupInbox(_, _, let blindedPublicKey): return blindedPublicKey
}
}()
// Prevent ControlMessages from being handled multiple times if not supported // Prevent ControlMessages from being handled multiple times if not supported
try? ControlMessageProcessRecord( try? ControlMessageProcessRecord(

View File

@ -210,7 +210,7 @@ internal extension SessionUtil {
} }
} }
// Delete any contact/thread records which aren't in the config message /// Delete any contact/thread records which aren't in the config message
let syncedContactIds: [String] = targetContactData let syncedContactIds: [String] = targetContactData
.map { $0.key } .map { $0.key }
.appending(userPublicKey) .appending(userPublicKey)
@ -226,7 +226,28 @@ internal extension SessionUtil {
.select(.id) .select(.id)
.asRequest(of: String.self) .asRequest(of: String.self)
.fetchAll(db) .fetchAll(db)
let combinedIds: [String] = contactIdsToRemove.appending(contentsOf: threadIdsToRemove)
/// When the user opens a brand new conversation this creates a "draft conversation" which has a hidden thread but no
/// contact record, when we receive a contact update this "draft conversation" would be included in the
/// `threadIdsToRemove` which would result in the user getting kicked from the screen and the thread removed, we
/// want to avoid this (as it's essentially a bug) so find any conversations in this state and remove them from the list that
/// will be pruned
let threadT: TypedTableAlias<SessionThread> = TypedTableAlias()
let contactT: TypedTableAlias<Contact> = TypedTableAlias()
let draftConversationIds: [String] = try SQLRequest<String>("""
SELECT \(threadT[.id])
FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contactT[.id]) = \(threadT[.id])
WHERE (
\(SQL("\(threadT[.id]) IN \(threadIdsToRemove)")) AND
\(contactT[.id]) IS NULL
)
""").fetchAll(db)
/// Consolidate the ids which should be removed
let combinedIds: [String] = contactIdsToRemove
.appending(contentsOf: threadIdsToRemove)
.filter { !draftConversationIds.contains($0) }
if !combinedIds.isEmpty { if !combinedIds.isEmpty {
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: combinedIds) SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: combinedIds)

View File

@ -784,7 +784,6 @@ public extension SessionThreadViewModel {
static func homeFilterSQL(userPublicKey: String) -> SQL { static func homeFilterSQL(userPublicKey: String) -> SQL {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias() let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias() let contact: TypedTableAlias<Contact> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
return """ return """
\(thread[.shouldBeVisible]) = true AND ( \(thread[.shouldBeVisible]) = true AND (
@ -848,7 +847,6 @@ public extension SessionThreadViewModel {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction")
let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
let closedGroupUserCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.closedGroupUserCountString)_table") let closedGroupUserCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.closedGroupUserCountString)_table")
let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name)
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name)

View File

@ -1105,7 +1105,7 @@ public final class SnodeAPI {
.compactMap { $0.value } .compactMap { $0.value }
.asSet() .asSet()
} }
.retry(4) .retry(2)
.handleEvents( .handleEvents(
receiveCompletion: { result in receiveCompletion: { result in
switch result { switch result {