Merge branch 'dev' into cleanup

pull/253/head
nielsandriesse 5 years ago
commit b34ef5b4fc

@ -4088,7 +4088,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 105; CURRENT_PROJECT_VERSION = 109;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -4102,7 +4102,7 @@
INFOPLIST_FILE = SignalShareExtension/Info.plist; INFOPLIST_FILE = SignalShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.4.6; MARKETING_VERSION = 1.5.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -4150,7 +4150,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 105; CURRENT_PROJECT_VERSION = 109;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
@ -4169,7 +4169,7 @@
INFOPLIST_FILE = SignalShareExtension/Info.plist; INFOPLIST_FILE = SignalShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.4.6; MARKETING_VERSION = 1.5.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -4204,7 +4204,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 105; CURRENT_PROJECT_VERSION = 109;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
@ -4223,7 +4223,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.4.6; MARKETING_VERSION = 1.5.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@ -4274,7 +4274,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 105; CURRENT_PROJECT_VERSION = 109;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
@ -4298,7 +4298,7 @@
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
MARKETING_VERSION = 1.4.6; MARKETING_VERSION = 1.5.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
@ -4336,7 +4336,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 105; CURRENT_PROJECT_VERSION = 109;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -4348,7 +4348,7 @@
INFOPLIST_FILE = LokiPushNotificationService/Info.plist; INFOPLIST_FILE = LokiPushNotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.4.6; MARKETING_VERSION = 1.5.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service";
@ -4399,7 +4399,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 105; CURRENT_PROJECT_VERSION = 109;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
@ -4416,7 +4416,7 @@
INFOPLIST_FILE = LokiPushNotificationService/Info.plist; INFOPLIST_FILE = LokiPushNotificationService/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 1.4.6; MARKETING_VERSION = 1.5.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service";
@ -4600,7 +4600,7 @@
CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 105; CURRENT_PROJECT_VERSION = 109;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -4635,7 +4635,7 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 1.4.6; MARKETING_VERSION = 1.5.0;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -4668,7 +4668,7 @@
CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 105; CURRENT_PROJECT_VERSION = 109;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -4703,7 +4703,7 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 1.4.6; MARKETING_VERSION = 1.5.0;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session; PRODUCT_NAME = Session;

@ -114,8 +114,8 @@ final class ConversationTitleView : UIView {
guard let timestamp = notification.object as? NSNumber else { return } guard let timestamp = notification.object as? NSNumber else { return }
setStatusIfNeeded(to: .messageSent, forMessageWithTimestamp: timestamp) setStatusIfNeeded(to: .messageSent, forMessageWithTimestamp: timestamp)
handledMessageTimestamps.insert(timestamp) handledMessageTimestamps.insert(timestamp)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self.clearStatusIfNeededForMessageWithTimestamp(timestamp) self?.clearStatusIfNeededForMessageWithTimestamp(timestamp)
} }
} }

@ -20,10 +20,13 @@ final class LightModeSheet : Sheet {
let explanationLabel = UILabel() let explanationLabel = UILabel()
explanationLabel.textColor = Colors.text explanationLabel.textColor = Colors.text
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
explanationLabel.textAlignment = .center
explanationLabel.text = """ explanationLabel.text = """
Who left the lights on? bla bla bla Whoops, who left the lights on?
Feeling the dark side more? Just pop into the in-app settings and tap the app mode button in the top-right to toggle dark mode again. Thats right, Session has a spiffy new light mode! Take the fresh new color palette for a spin its now the default mode.
Want to go back to the dark side? Just tap the moon symbol in the in-app settings to switch modes.
""" """
explanationLabel.numberOfLines = 0 explanationLabel.numberOfLines = 0
explanationLabel.lineBreakMode = .byWordWrapping explanationLabel.lineBreakMode = .byWordWrapping

@ -174,7 +174,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
guard selectedContacts.count >= 1 else { guard selectedContacts.count >= 1 else {
return showError(title: "Please pick at least 1 group member") return showError(title: "Please pick at least 1 group member")
} }
guard selectedContacts.count < 50 else { // Minus one because we're going to include self later guard selectedContacts.count < ClosedGroupsProtocol.groupSizeLimit else { // Minus one because we're going to include self later
return showError(title: NSLocalizedString("vc_create_closed_group_too_many_group_members_error", comment: "")) return showError(title: NSLocalizedString("vc_create_closed_group_too_many_group_members_error", comment: ""))
} }
let selectedContacts = self.selectedContacts let selectedContacts = self.selectedContacts

@ -120,8 +120,18 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
getSettingButtons().forEach { settingButtonOrSeparator in getSettingButtons().forEach { settingButtonOrSeparator in
settingButtonsStackView.addArrangedSubview(settingButtonOrSeparator) settingButtonsStackView.addArrangedSubview(settingButtonOrSeparator)
} }
// Set up version label
let versionLabel = UILabel()
versionLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
versionLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
versionLabel.numberOfLines = 0
versionLabel.textAlignment = .center
versionLabel.lineBreakMode = .byCharWrapping
let version = Bundle.main.infoDictionary!["CFBundleShortVersionString"]!
let buildNumber = Bundle.main.infoDictionary!["CFBundleVersion"]!
versionLabel.text = "Version \(version) (\(buildNumber))"
// Set up stack view // Set up stack view
let stackView = UIStackView(arrangedSubviews: [ topStackView, settingButtonsStackView ]) let stackView = UIStackView(arrangedSubviews: [ topStackView, settingButtonsStackView, versionLabel ])
stackView.axis = .vertical stackView.axis = .vertical
stackView.spacing = Values.largeSpacing stackView.spacing = Values.largeSpacing
stackView.alignment = .fill stackView.alignment = .fill

@ -195,7 +195,7 @@ NS_ASSUME_NONNULL_BEGIN
timestampLabelText timestampLabelText
= NSLocalizedString(@"MESSAGE_STATUS_SEND_FAILED", @"Label indicating that a message failed to send."); = NSLocalizedString(@"MESSAGE_STATUS_SEND_FAILED", @"Label indicating that a message failed to send.");
} else { } else {
timestampLabelText = [DateUtil formatMessageTimestamp:viewItem.interaction.timestamp]; timestampLabelText = [DateUtil formatMessageTimestamp:viewItem.interaction.timestampForUI];
} }
[self.timestampLabel setText:timestampLabelText.localizedUppercaseString]; [self.timestampLabel setText:timestampLabelText.localizedUppercaseString];

@ -549,7 +549,7 @@ const CGFloat kRemotelySourcedContentRowSpacing = 4;
} }
} else { } else {
OWSContactsManager *contactsManager = Environment.shared.contactsManager; OWSContactsManager *contactsManager = Environment.shared.contactsManager;
__block NSString *quotedAuthor = [contactsManager contactOrProfileNameForPhoneIdentifier:self.quotedMessage.authorId]; __block NSString *quotedAuthor = [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:self.quotedMessage.authorId] ?: [contactsManager contactOrProfileNameForPhoneIdentifier:self.quotedMessage.authorId];
if (quotedAuthor == self.quotedMessage.authorId) { if (quotedAuthor == self.quotedMessage.authorId) {
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {

@ -1221,10 +1221,15 @@ typedef enum : NSUInteger {
} }
- (void)restoreSession { - (void)restoreSession {
if (![self.thread isKindOfClass:TSContactThread.class]) { return; }
TSContactThread *thread = (TSContactThread *)self.thread;
__weak ConversationViewController *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKSessionManagementProtocol startSessionResetInThread:self.thread transaction:transaction]; [thread addSessionRestoreDevice:thread.contactIdentifier transaction:transaction];
[LKSessionManagementProtocol startSessionResetInThread:thread transaction:transaction];
} error:nil]; } error:nil];
[weakSelf updateSessionRestoreBanner];
}); });
} }
@ -3592,7 +3597,6 @@ typedef enum : NSUInteger {
// and won't update the UI state immediately. // and won't update the UI state immediately.
- (void)didScrollToBottom - (void)didScrollToBottom
{ {
id<ConversationViewItem> _Nullable lastVisibleViewItem = [self.viewItems lastObject]; id<ConversationViewItem> _Nullable lastVisibleViewItem = [self.viewItems lastObject];
if (lastVisibleViewItem) { if (lastVisibleViewItem) {
uint64_t lastVisibleSortId = lastVisibleViewItem.interaction.sortId; uint64_t lastVisibleSortId = lastVisibleViewItem.interaction.sortId;
@ -4669,7 +4673,7 @@ typedef enum : NSUInteger {
OWSAssertDebug(left <= mid); OWSAssertDebug(left <= mid);
OWSAssertDebug(mid < right); OWSAssertDebug(mid < right);
id<ConversationViewItem> viewItem = self.viewItems[mid]; id<ConversationViewItem> viewItem = self.viewItems[mid];
if (viewItem.interaction.timestamp >= viewHorizonTimestamp) { if (viewItem.interaction.timestampForUI >= viewHorizonTimestamp) {
right = mid; right = mid;
} else { } else {
// This is an optimization; it also ensures that we converge. // This is an optimization; it also ensures that we converge.
@ -4678,7 +4682,7 @@ typedef enum : NSUInteger {
} }
OWSAssertDebug(left == right); OWSAssertDebug(left == right);
id<ConversationViewItem> viewItem = self.viewItems[left]; id<ConversationViewItem> viewItem = self.viewItems[left];
if (viewItem.interaction.timestamp >= viewHorizonTimestamp) { if (viewItem.interaction.timestampForUI >= viewHorizonTimestamp) {
OWSLogInfo(@"firstIndexPathAtViewHorizonTimestamp: %zd / %zd", left, self.viewItems.count); OWSLogInfo(@"firstIndexPathAtViewHorizonTimestamp: %zd / %zd", left, self.viewItems.count);
return [NSIndexPath indexPathForRow:(NSInteger) left inSection:0]; return [NSIndexPath indexPathForRow:(NSInteger) left inSection:0];
} else { } else {
@ -5388,7 +5392,7 @@ typedef enum : NSUInteger {
__block TSInteraction *targetInteraction; __block TSInteraction *targetInteraction;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self.thread enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *t) { [self.thread enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *t) {
if (interaction.timestamp == timestamp.unsignedLongLongValue) { if (interaction.timestampForUI == timestamp.unsignedLongLongValue) {
targetInteraction = interaction; targetInteraction = interaction;
} }
}]; }];
@ -5412,7 +5416,7 @@ typedef enum : NSUInteger {
{ {
__block TSInteraction *targetInteraction; __block TSInteraction *targetInteraction;
[self.thread enumerateInteractionsUsingBlock:^(TSInteraction *interaction) { [self.thread enumerateInteractionsUsingBlock:^(TSInteraction *interaction) {
if (interaction.timestamp == timestamp.unsignedLongLongValue) { if (interaction.timestampForUI == timestamp.unsignedLongLongValue) {
targetInteraction = interaction; targetInteraction = interaction;
} }
}]; }];

@ -63,8 +63,16 @@ NS_ASSUME_NONNULL_BEGIN
interactionIndexMap[viewItem.interaction.uniqueId] = @(i); interactionIndexMap[viewItem.interaction.uniqueId] = @(i);
[interactionIds addObject:viewItem.interaction.uniqueId]; [interactionIds addObject:viewItem.interaction.uniqueId];
if (viewItem.unreadIndicator != nil) { if (viewItem.unreadIndicator != nil && [viewItem.interaction conformsToProtocol:@protocol(OWSReadTracking)]) {
_unreadIndicatorIndex = @(i); id<OWSReadTracking> interaction = (id<OWSReadTracking>)viewItem.interaction;
// Under normal circumstances !interaction.read should always evaluate to true at this point, but
// there is a bug that can somehow cause it to be false leading to conversations permanently being
// stuck with "unread" messages.
if (!interaction.read) {
_unreadIndicatorIndex = @(i);
}
} }
} }
_interactionIndexMap = [interactionIndexMap copy]; _interactionIndexMap = [interactionIndexMap copy];
@ -1342,7 +1350,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
break; break;
} }
uint64_t viewItemTimestamp = viewItem.interaction.timestamp; uint64_t viewItemTimestamp = viewItem.interaction.timestampForUI;
OWSAssertDebug(viewItemTimestamp > 0); OWSAssertDebug(viewItemTimestamp > 0);
BOOL shouldShowDate = NO; BOOL shouldShowDate = NO;
@ -1409,7 +1417,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
NSAttributedString *_Nullable senderName = nil; NSAttributedString *_Nullable senderName = nil;
OWSInteractionType interactionType = viewItem.interaction.interactionType; OWSInteractionType interactionType = viewItem.interaction.interactionType;
NSString *timestampText = [DateUtil formatTimestampShort:viewItem.interaction.timestamp]; NSString *timestampText = [DateUtil formatTimestampShort:viewItem.interaction.timestampForUI];
if (interactionType == OWSInteractionType_OutgoingMessage) { if (interactionType == OWSInteractionType_OutgoingMessage) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)viewItem.interaction; TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)viewItem.interaction;

@ -663,22 +663,25 @@ const CGFloat kIconViewLength = 24;
[weakSelf showGroupMembersView]; [weakSelf showGroupMembersView];
}] }]
]; ];
[mainSection addItem:[OWSTableItem NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
itemWithCustomCellBlock:^{ if ([((TSGroupThread *)self.thread).groupModel.groupMemberIds containsObject:userPublicKey]) {
UITableViewCell *cell = [mainSection addItem:[OWSTableItem
[weakSelf disclosureCellWithName:NSLocalizedString(@"LEAVE_GROUP_ACTION", itemWithCustomCellBlock:^{
@"table cell label in conversation settings") UITableViewCell *cell =
iconName:@"table_ic_group_leave" [weakSelf disclosureCellWithName:NSLocalizedString(@"LEAVE_GROUP_ACTION",
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( @"table cell label in conversation settings")
OWSConversationSettingsViewController, @"leave_group")]; iconName:@"table_ic_group_leave"
cell.userInteractionEnabled = !weakSelf.hasLeftGroup; accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
OWSConversationSettingsViewController, @"leave_group")];
return cell; cell.userInteractionEnabled = !weakSelf.hasLeftGroup;
}
actionBlock:^{ return cell;
[weakSelf didTapLeaveGroup]; }
}] actionBlock:^{
]; [weakSelf didTapLeaveGroup];
}]
];
}
} }
@ -837,6 +840,15 @@ const CGFloat kIconViewLength = 24;
return cell; return cell;
} }
actionBlock:nil]]; actionBlock:nil]];
[mainSection addItem:
[OWSTableItem itemWithCustomCellBlock:^{
NSString *title = @"Reset Secure Session";
NSString *accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(OWSConversationSettingsViewController, @"reset_secure_session");
return [weakSelf disclosureCellWithName:title iconName:@"system_message_security" accessibilityIdentifier:accessibilityIdentifier];
}
actionBlock:^{ [weakSelf resetSecureSession]; }]
];
} }
self.contents = contents; self.contents = contents;
@ -1398,6 +1410,26 @@ const CGFloat kIconViewLength = 24;
[self.conversationSettingsViewDelegate conversationSettingsDidRequestConversationSearch:self]; [self.conversationSettingsViewDelegate conversationSettingsDidRequestConversationSearch:self];
} }
- (void)resetSecureSession
{
if (![self.thread isKindOfClass:TSContactThread.class]) { return; }
TSContactThread *thread = (TSContactThread *)self.thread;
__weak OWSConversationSettingsViewController *weakSelf = self;
NSString *message = @"This may help if you're having encryption problems in this conversation. Your messages will be kept.";
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Reset Secure Session?" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", @"") style:UIAlertActionStyleDefault handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:@"Reset" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
dispatch_async(dispatch_get_main_queue(), ^{
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[thread addSessionRestoreDevice:thread.contactIdentifier transaction:transaction];
[LKSessionManagementProtocol startSessionResetInThread:thread transaction:transaction];
} error:nil];
[weakSelf.navigationController popViewControllerAnimated:YES];
});
}]];
[self presentViewController:alert animated:YES completion:nil];
}
#pragma mark - Notifications #pragma mark - Notifications
- (void)identityStateDidChange:(NSNotification *)notification - (void)identityStateDidChange:(NSNotification *)notification

@ -127,7 +127,7 @@
fileprivate func startAnimation() { fileprivate func startAnimation() {
stopAnimation() stopAnimation()
let baseColor = UIColor.white let baseColor = Colors.text
let timeIncrement: CFTimeInterval = 0.15 let timeIncrement: CFTimeInterval = 0.15
var colorValues = [CGColor]() var colorValues = [CGColor]()
var pathValues = [CGPath]() var pathValues = [CGPath]()

@ -348,7 +348,12 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
OWSFailDebug(@"Unexpected object in unseen messages: %@", [object class]); OWSFailDebug(@"Unexpected object in unseen messages: %@", [object class]);
return; return;
} }
[messages addObject:(id<OWSReadTracking>)object]; id<OWSReadTracking> unread = (id<OWSReadTracking>)object;
if (unread.read) {
[LKLogger print:@"Found an already read message in the * unseen * messages list."];
return;
}
[messages addObject:unread];
}]; }];
return [messages copy]; return [messages copy];
@ -356,7 +361,24 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
- (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction - (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction
{ {
return [[transaction ext:TSUnreadDatabaseViewExtensionName] numberOfItemsInGroup:self.uniqueId]; __block NSUInteger count = 0;
YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName];
[unreadMessages enumerateKeysAndObjectsInGroup:self.uniqueId
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
OWSFailDebug(@"Unexpected object in unread messages: %@", [object class]);
return;
}
id<OWSReadTracking> unread = (id<OWSReadTracking>)object;
if (unread.read) {
[LKLogger print:@"Found an already read message in the * unread * messages list."];
return;
}
count += 1;
}];
return count;
} }
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction - (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction

@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)processIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript + (void)processIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript
serverID:(uint64_t)serverID serverID:(uint64_t)serverID
serverTimestamp:(uint64_t)serverTimestamp
attachmentHandler:(void (^)( attachmentHandler:(void (^)(
NSArray<TSAttachmentStream *> *attachmentStreams))attachmentHandler NSArray<TSAttachmentStream *> *attachmentStreams))attachmentHandler
transaction:(YapDatabaseReadWriteTransaction *)transaction; transaction:(YapDatabaseReadWriteTransaction *)transaction;

@ -62,6 +62,7 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)processIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)transcript + (void)processIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)transcript
serverID:(uint64_t)serverID serverID:(uint64_t)serverID
serverTimestamp:(uint64_t)serverTimestamp
attachmentHandler:(void (^)( attachmentHandler:(void (^)(
NSArray<TSAttachmentStream *> *attachmentStreams))attachmentHandler NSArray<TSAttachmentStream *> *attachmentStreams))attachmentHandler
transaction:(YapDatabaseReadWriteTransaction *)transaction transaction:(YapDatabaseReadWriteTransaction *)transaction
@ -104,6 +105,14 @@ NS_ASSUME_NONNULL_BEGIN
contactShare:transcript.contact contactShare:transcript.contact
linkPreview:transcript.linkPreview]; linkPreview:transcript.linkPreview];
if (transcript.thread.isGroupThread) {
TSGroupThread *thread = (TSGroupThread *)transcript.thread;
if (thread.isPublicChat) {
[outgoingMessage setServerTimestampToReceivedTimestamp:serverTimestamp];
}
}
if (serverID != 0) { if (serverID != 0) {
outgoingMessage.openGroupServerMessageID = serverID; outgoingMessage.openGroupServerMessageID = serverID;
} }

@ -12,10 +12,10 @@ public final class FileServerAPI : DotNetAPI {
public static let maxFileSize = 10_000_000 // 10 MB public static let maxFileSize = 10_000_000 // 10 MB
/// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes /// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
/// is on the **HTTP request** and not the file size. Because of onion request encryption, a file that's about 4 MB will result in a request that's about 18 MB. /// is on the **HTTP request** and not the file size. Because of onion request encryption, a file that's about 4 MB will result in a request that's about 18 MB.
/// On average the multiplier appears to be about 4.4, so when checking whether the file will exceed the file size limit when uploading a file we just divide /// On average the multiplier appears to be about 6, so when checking whether the file will exceed the file size limit when uploading a file we just divide
/// the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only possible after proof of work /// the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only possible after proof of work
/// has been calculated and the onion request encryption has happened, which takes several seconds. /// has been calculated and the onion request encryption has happened, which takes several seconds.
public static let fileSizeORMultiplier = 4.4 public static let fileSizeORMultiplier: Double = 6
@objc public static let server = "https://file.getsession.org" @objc public static let server = "https://file.getsession.org"

@ -6,11 +6,6 @@ public enum OnionRequestAPI {
public static var guardSnodes: Set<Snode> = [] public static var guardSnodes: Set<Snode> = []
public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user
private static var snodePool: Set<Snode> {
let unreliableSnodes = Set(SnodeAPI.snodeFailureCount.keys)
return SnodeAPI.snodePool.subtracting(unreliableSnodes)
}
// MARK: Settings // MARK: Settings
/// The number of snodes (including the guard snode) in a path. /// The number of snodes (including the guard snode) in a path.
private static let pathSize: UInt = 3 private static let pathSize: UInt = 3
@ -84,7 +79,7 @@ public enum OnionRequestAPI {
} else { } else {
print("[Loki] [Onion Request API] Populating guard snode cache.") print("[Loki] [Onion Request API] Populating guard snode cache.")
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<Set<Snode>> in // Just used to populate the snode pool return SnodeAPI.getRandomSnode().then2 { _ -> Promise<Set<Snode>> in // Just used to populate the snode pool
var unusedSnodes = snodePool // Sync on LokiAPI.workQueue var unusedSnodes = SnodeAPI.snodePool // Sync on LokiAPI.workQueue
guard unusedSnodes.count >= guardSnodeCount else { throw Error.insufficientSnodes } guard unusedSnodes.count >= guardSnodeCount else { throw Error.insufficientSnodes }
func getGuardSnode() -> Promise<Snode> { func getGuardSnode() -> Promise<Snode> {
// randomElement() uses the system's default random generator, which is cryptographically secure // randomElement() uses the system's default random generator, which is cryptographically secure
@ -115,7 +110,7 @@ public enum OnionRequestAPI {
} }
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<[Path]> in // Just used to populate the snode pool return SnodeAPI.getRandomSnode().then2 { _ -> Promise<[Path]> in // Just used to populate the snode pool
return getGuardSnodes().map2 { guardSnodes -> [Path] in return getGuardSnodes().map2 { guardSnodes -> [Path] in
var unusedSnodes = snodePool.subtracting(guardSnodes) var unusedSnodes = SnodeAPI.snodePool.subtracting(guardSnodes)
let pathSnodeCount = guardSnodeCount * pathSize - guardSnodeCount let pathSnodeCount = guardSnodeCount * pathSize - guardSnodeCount
guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes } guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes }
// Don't test path snodes as this would reveal the user's IP to them // Don't test path snodes as this would reveal the user's IP to them
@ -290,8 +285,7 @@ public enum OnionRequestAPI {
let url = "\(guardSnode.address):\(guardSnode.port)/onion_req" let url = "\(guardSnode.address):\(guardSnode.port)/onion_req"
let finalEncryptionResult = intermediate.finalEncryptionResult let finalEncryptionResult = intermediate.finalEncryptionResult
let onion = finalEncryptionResult.ciphertext let onion = finalEncryptionResult.ciphertext
let requestSizeLimit = Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier if case Destination.server = destination, Double(onion.count) > 0.75 * Double(FileServerAPI.maxFileSize) {
if case Destination.server = destination, Double(onion.count) > 0.75 * requestSizeLimit {
print("[Loki] Approaching request size limit: ~\(onion.count) bytes.") print("[Loki] Approaching request size limit: ~\(onion.count) bytes.")
} }
let parameters: JSON = [ let parameters: JSON = [
@ -339,7 +333,13 @@ public enum OnionRequestAPI {
} }
} }
promise.catch2 { error in // Must be invoked on LokiAPI.workQueue promise.catch2 { error in // Must be invoked on LokiAPI.workQueue
guard case HTTP.Error.httpRequestFailed(_, _) = error else { return } guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error else { return }
// Marking all the snodes in the path as unreliable here is aggressive, but otherwise users
// can get stuck with a failing path that just refreshes to the same path.
let path = paths.first { $0.contains(guardSnode) }
path?.forEach { snode in
SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw
}
dropAllPaths() // A snode in the path is bad; retry with a different path dropAllPaths() // A snode in the path is bad; retry with a different path
dropGuardSnode(guardSnode) dropGuardSnode(guardSnode)
} }

@ -174,6 +174,7 @@ public final class PublicChatPoller : NSObject {
envelope.setSource(senderPublicKey) envelope.setSource(senderPublicKey)
envelope.setSourceDevice(OWSDevicePrimaryDeviceId) envelope.setSourceDevice(OWSDevicePrimaryDeviceId)
envelope.setContent(try! content.build().serializedData()) envelope.setContent(try! content.build().serializedData())
envelope.setServerTimestamp(message.serverTimestamp)
try! Storage.writeSync { transaction in try! Storage.writeSync { transaction in
transaction.setObject(senderDisplayName, forKey: senderPublicKey, inCollection: publicChat.id) transaction.setObject(senderDisplayName, forKey: senderPublicKey, inCollection: publicChat.id)
let messageServerID = message.serverID let messageServerID = message.serverID

@ -15,7 +15,7 @@ public final class SnodeAPI : NSObject {
// MARK: Settings // MARK: Settings
private static let maxRetryCount: UInt = 4 private static let maxRetryCount: UInt = 4
private static let minimumSnodePoolCount = 32 private static let minimumSnodePoolCount = 64
private static let minimumSwarmSnodeCount = 2 private static let minimumSwarmSnodeCount = 2
private static let seedNodePool: Set<String> = [ "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ] private static let seedNodePool: Set<String> = [ "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ]
private static let snodeFailureThreshold = 2 private static let snodeFailureThreshold = 2
@ -296,7 +296,7 @@ public final class SnodeAPI : NSObject {
// MARK: Error Handling // MARK: Error Handling
/// - Note: Should only be invoked from `LokiAPI.workQueue` to avoid race conditions. /// - Note: Should only be invoked from `LokiAPI.workQueue` to avoid race conditions.
internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Snode, associatedWith publicKey: String) -> Error? { internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Snode, associatedWith publicKey: String? = nil) -> Error? {
#if DEBUG #if DEBUG
assertOnQueue(SnodeAPI.workQueue) assertOnQueue(SnodeAPI.workQueue)
#endif #endif
@ -307,8 +307,11 @@ public final class SnodeAPI : NSObject {
print("[Loki] Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).") print("[Loki] Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).")
if newFailureCount >= SnodeAPI.snodeFailureThreshold { if newFailureCount >= SnodeAPI.snodeFailureThreshold {
print("[Loki] Failure threshold reached for: \(snode); dropping it.") print("[Loki] Failure threshold reached for: \(snode); dropping it.")
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey) if let publicKey = publicKey {
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
}
SnodeAPI.dropSnodeFromSnodePool(snode) SnodeAPI.dropSnodeFromSnodePool(snode)
print("[Loki] Snode pool count: \(snodePool.count).")
SnodeAPI.snodeFailureCount[snode] = 0 SnodeAPI.snodeFailureCount[snode] = 0
} }
} }
@ -321,8 +324,12 @@ public final class SnodeAPI : NSObject {
return SnodeAPI.SnodeAPIError.clockOutOfSync return SnodeAPI.SnodeAPIError.clockOutOfSync
case 421: case 421:
// The snode isn't associated with the given public key anymore // The snode isn't associated with the given public key anymore
print("[Loki] Invalidating swarm for: \(publicKey).") if let publicKey = publicKey {
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey) print("[Loki] Invalidating swarm for: \(publicKey).")
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
} else {
print("[Loki] Got a 421 without an associated public key.")
}
case 432: case 432:
// The proof of work difficulty is too low // The proof of work difficulty is too low
if let powDifficulty = json?["difficulty"] as? UInt { if let powDifficulty = json?["difficulty"] as? UInt {

@ -13,6 +13,7 @@ import PromiseKit
@objc(LKClosedGroupsProtocol) @objc(LKClosedGroupsProtocol)
public final class ClosedGroupsProtocol : NSObject { public final class ClosedGroupsProtocol : NSObject {
public static let isSharedSenderKeysEnabled = false public static let isSharedSenderKeysEnabled = false
public static let groupSizeLimit = 10
// MARK: - Sending // MARK: - Sending
@ -54,6 +55,7 @@ public final class ClosedGroupsProtocol : NSObject {
// Send a closed group update message to all members (and their linked devices) using established channels // Send a closed group update message to all members (and their linked devices) using established channels
var promises: [Promise<Void>] = [] var promises: [Promise<Void>] = []
for member in members { // Not `membersAndLinkedDevices` as this internally takes care of multi device already for member in members { // Not `membersAndLinkedDevices` as this internally takes care of multi device already
guard member != userPublicKey else { continue }
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction) thread.save(with: transaction)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name,
@ -182,6 +184,7 @@ public final class ClosedGroupsProtocol : NSObject {
let userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction) let userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction)
let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey)) let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey))
for member in members { // This internally takes care of multi device for member in members { // This internally takes care of multi device
guard member != userPublicKey else { continue }
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction) thread.save(with: transaction)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: userSenderKey) let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: userSenderKey)
@ -201,6 +204,7 @@ public final class ClosedGroupsProtocol : NSObject {
} }
public static func requestSenderKey(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { public static func requestSenderKey(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
print("[Loki] Requesting sender key for group public key: \(groupPublicKey), sender public key: \(senderPublicKey).")
// Establish session if needed // Establish session if needed
SessionManagementProtocol.sendSessionRequestIfNeeded(to: senderPublicKey, using: transaction) SessionManagementProtocol.sendSessionRequestIfNeeded(to: senderPublicKey, using: transaction)
// Send the request // Send the request
@ -286,7 +290,7 @@ public final class ClosedGroupsProtocol : NSObject {
} }
let group = thread.groupModel let group = thread.groupModel
// Check that the sender is a member of the group (before the update) // Check that the sender is a member of the group (before the update)
var membersAndLinkedDevices: Set<String> = Set(group.groupMemberIds) var membersAndLinkedDevices: Set<String> = Set(members)
for member in group.groupMemberIds { for member in group.groupMemberIds {
let deviceLinks = OWSPrimaryStorage.shared().getDeviceLinks(for: member, in: transaction) let deviceLinks = OWSPrimaryStorage.shared().getDeviceLinks(for: member, in: transaction)
membersAndLinkedDevices.formUnion(deviceLinks.flatMap { [ $0.master.publicKey, $0.slave.publicKey ] }) membersAndLinkedDevices.formUnion(deviceLinks.flatMap { [ $0.master.publicKey, $0.slave.publicKey ] })
@ -315,6 +319,7 @@ public final class ClosedGroupsProtocol : NSObject {
let userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction) let userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction)
let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey)) let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey))
for member in members { for member in members {
guard member != userPublicKey else { continue }
let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction)
thread.save(with: transaction) thread.save(with: transaction)
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: userSenderKey) let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: userSenderKey)
@ -354,6 +359,7 @@ public final class ClosedGroupsProtocol : NSObject {
return print("[Loki] Ignoring closed group sender key request from non-member.") return print("[Loki] Ignoring closed group sender key request from non-member.")
} }
// Respond to the request // Respond to the request
print("[Loki] Responding to sender key request from: \(senderPublicKey).")
SessionManagementProtocol.sendSessionRequestIfNeeded(to: senderPublicKey, using: transaction) // This internally takes care of multi device SessionManagementProtocol.sendSessionRequestIfNeeded(to: senderPublicKey, using: transaction) // This internally takes care of multi device
let userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction) let userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction)
let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey)) let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey))
@ -389,6 +395,7 @@ public final class ClosedGroupsProtocol : NSObject {
return print("[Loki] Ignoring invalid closed group sender key.") return print("[Loki] Ignoring invalid closed group sender key.")
} }
// Store the sender key // Store the sender key
print("[Loki] Received a sender key from: \(senderPublicKey).")
let ratchet = ClosedGroupRatchet(chainKey: senderKey.chainKey.toHexString(), keyIndex: UInt(senderKey.keyIndex), messageKeys: []) let ratchet = ClosedGroupRatchet(chainKey: senderKey.chainKey.toHexString(), keyIndex: UInt(senderKey.keyIndex), messageKeys: [])
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, using: transaction) Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, using: transaction)
} }

@ -133,7 +133,17 @@ public final class SharedSenderKeysImplementation : NSObject, SharedSenderKeysPr
} }
public func encrypt(_ plaintext: Data, for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws -> (ivAndCiphertext: Data, keyIndex: UInt) { public func encrypt(_ plaintext: Data, for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws -> (ivAndCiphertext: Data, keyIndex: UInt) {
let ratchet = try stepRatchetOnce(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction) let ratchet: ClosedGroupRatchet
do {
ratchet = try stepRatchetOnce(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
} catch {
// FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more
// convenient because there's an easy way to get the sender public key from here.
if case RatchetingError.loadingFailed(_, _) = error {
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
}
throw error
}
let iv = Data.getSecureRandomData(ofSize: SharedSenderKeysImplementation.ivSize)! let iv = Data.getSecureRandomData(ofSize: SharedSenderKeysImplementation.ivSize)!
let gcm = GCM(iv: iv.bytes, tagLength: Int(SharedSenderKeysImplementation.gcmTagSize), mode: .combined) let gcm = GCM(iv: iv.bytes, tagLength: Int(SharedSenderKeysImplementation.gcmTagSize), mode: .combined)
let messageKey = ratchet.messageKeys.last! let messageKey = ratchet.messageKeys.last!

@ -141,7 +141,6 @@ public final class SessionManagementProtocol : NSObject {
guard let thread = thread as? TSContactThread else { guard let thread = thread as? TSContactThread else {
return print("[Loki] Can't restore session for non contact thread.") return print("[Loki] Can't restore session for non contact thread.")
} }
guard thread.sessionResetStatus == .none else { return }
// Send end session messages to the devices requiring session restoration // Send end session messages to the devices requiring session restoration
let devices = thread.sessionRestoreDevices // TODO: Rename this to something that reads better let devices = thread.sessionRestoreDevices // TODO: Rename this to something that reads better
for device in devices { for device in devices {

@ -39,9 +39,14 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value);
@property (nonatomic, readonly) uint64_t timestamp; @property (nonatomic, readonly) uint64_t timestamp;
@property (nonatomic, readonly) uint64_t sortId; @property (nonatomic, readonly) uint64_t sortId;
@property (nonatomic, readonly) uint64_t receivedAtTimestamp; @property (nonatomic, readonly) uint64_t receivedAtTimestamp;
@property (nonatomic, readonly) BOOL shouldUseServerTime;
/// Used for public chats where a message sent from a slave device is interpreted as having been sent from the master device. /// Used for public chats where a message sent from a slave device is interpreted as having been sent from the master device.
@property (nonatomic) NSString *actualSenderHexEncodedPublicKey; @property (nonatomic) NSString *actualSenderHexEncodedPublicKey;
- (void)setServerTimestampToReceivedTimestamp:(uint64_t)receivedAtTimestamp;
- (uint64_t)timestampForUI;
- (NSDate *)receivedAtDate; - (NSDate *)receivedAtDate;
- (OWSInteractionType)interactionType; - (OWSInteractionType)interactionType;

@ -41,6 +41,8 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value)
@implementation TSInteraction @implementation TSInteraction
@synthesize timestamp = _timestamp;
+ (NSArray<TSInteraction *> *)interactionsWithTimestamp:(uint64_t)timestamp + (NSArray<TSInteraction *> *)interactionsWithTimestamp:(uint64_t)timestamp
ofClass:(Class)clazz ofClass:(Class)clazz
withTransaction:(YapDatabaseReadTransaction *)transaction withTransaction:(YapDatabaseReadTransaction *)transaction
@ -175,11 +177,25 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value)
#pragma mark Date operations #pragma mark Date operations
- (uint64_t)timestampForUI
{
if (_shouldUseServerTime) {
return _receivedAtTimestamp;
}
return _timestamp;
}
- (uint64_t)timestampForLegacySorting - (uint64_t)timestampForLegacySorting
{ {
return self.timestamp; return self.timestamp;
} }
- (void)setServerTimestampToReceivedTimestamp:(uint64_t)receivedAtTimestamp
{
_shouldUseServerTime = YES;
_receivedAtTimestamp = receivedAtTimestamp;
}
- (NSDate *)receivedAtDate - (NSDate *)receivedAtDate
{ {
return [NSDate ows_dateWithMillisecondsSince1970:self.receivedAtTimestamp]; return [NSDate ows_dateWithMillisecondsSince1970:self.receivedAtTimestamp];

@ -581,7 +581,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
return; return;
} }
// FIXME: This is a temporary patch for bad mac issues. At least with this people will be able to message again. We have to figure out the root cause of the issue though. // Attempt to recover automatically
if ([decryptError userInfo][NSUnderlyingErrorKey] != nil) { if ([decryptError userInfo][NSUnderlyingErrorKey] != nil) {
NSDictionary *underlyingErrorUserInfo = [[decryptError userInfo][NSUnderlyingErrorKey] userInfo]; NSDictionary *underlyingErrorUserInfo = [[decryptError userInfo][NSUnderlyingErrorKey] userInfo];
if (underlyingErrorUserInfo[SCKExceptionWrapperUnderlyingExceptionKey] != nil) { if (underlyingErrorUserInfo[SCKExceptionWrapperUnderlyingExceptionKey] != nil) {

@ -915,6 +915,7 @@ NS_ASSUME_NONNULL_BEGIN
[OWSRecordTranscriptJob [OWSRecordTranscriptJob
processIncomingSentMessageTranscript:transcript processIncomingSentMessageTranscript:transcript
serverID:0 serverID:0
serverTimestamp:0
attachmentHandler:^(NSArray<TSAttachmentStream *> *attachmentStreams) { attachmentHandler:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
OWSAssertDebug(attachmentStreams.count == 1); OWSAssertDebug(attachmentStreams.count == 1);
TSAttachmentStream *attachmentStream = attachmentStreams.firstObject; TSAttachmentStream *attachmentStream = attachmentStreams.firstObject;
@ -943,6 +944,7 @@ NS_ASSUME_NONNULL_BEGIN
[OWSRecordTranscriptJob [OWSRecordTranscriptJob
processIncomingSentMessageTranscript:transcript processIncomingSentMessageTranscript:transcript
serverID:(serverID ?: 0) serverID:(serverID ?: 0)
serverTimestamp:(uint64_t)envelope.serverTimestamp
attachmentHandler:^(NSArray<TSAttachmentStream *> *attachmentStreams) { attachmentHandler:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
OWSLogDebug(@"successfully fetched transcript attachments: %lu", OWSLogDebug(@"successfully fetched transcript attachments: %lu",
(unsigned long)attachmentStreams.count); (unsigned long)attachmentStreams.count);
@ -1417,6 +1419,11 @@ NS_ASSUME_NONNULL_BEGIN
serverTimestamp:serverTimestamp serverTimestamp:serverTimestamp
wasReceivedByUD:wasReceivedByUD]; wasReceivedByUD:wasReceivedByUD];
// For open group messages, use the server timestamp as the received timestamp
if (oldGroupThread.isPublicChat) {
[incomingMessage setServerTimestampToReceivedTimestamp:(uint64_t)envelope.serverTimestamp];
}
// Loki: Set open group server ID if needed // Loki: Set open group server ID if needed
if (dataMessage.publicChatInfo != nil && dataMessage.publicChatInfo.hasServerID) { if (dataMessage.publicChatInfo != nil && dataMessage.publicChatInfo.hasServerID) {
incomingMessage.openGroupServerMessageID = dataMessage.publicChatInfo.serverID; incomingMessage.openGroupServerMessageID = dataMessage.publicChatInfo.serverID;

@ -18,6 +18,7 @@
#import "TSThread.h" #import "TSThread.h"
#import "UIImage+OWS.h" #import "UIImage+OWS.h"
#import <YapDatabase/YapDatabase.h> #import <YapDatabase/YapDatabase.h>
#import <SessionServiceKit/SessionServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -65,6 +66,30 @@ NS_ASSUME_NONNULL_BEGIN
- (NSUInteger)unreadMessagesCount - (NSUInteger)unreadMessagesCount
{ {
__block NSUInteger count = 0;
[LKStorage readWithBlock:^(YapDatabaseReadTransaction *transaction) {
YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName];
NSArray<NSString *> *allGroups = [unreadMessages allGroups];
for (NSString *groupID in allGroups) {
[unreadMessages enumerateKeysAndObjectsInGroup:groupID
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
OWSFailDebug(@"Unexpected object in unread messages: %@", [object class]);
return;
}
id<OWSReadTracking> unread = (id<OWSReadTracking>)object;
if (unread.read) {
[LKLogger print:@"Found an already read message in the * unread * messages list."];
return;
}
count += 1;
}];
}
}];
return count;
__block NSUInteger numberOfItems; __block NSUInteger numberOfItems;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
numberOfItems = [[transaction ext:TSUnreadDatabaseViewExtensionName] numberOfItemsInAllGroups]; numberOfItems = [[transaction ext:TSUnreadDatabaseViewExtensionName] numberOfItemsInAllGroups];

@ -491,7 +491,10 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
return; return;
} }
OWSAssertDebug(!possiblyRead.read); // Under normal circumstances !possiblyRead.read should always evaluate to true at this point, but
// there is a bug that can somehow cause it to be false leading to conversations permanently being
// stuck with "unread" messages.
OWSAssertDebug(possiblyRead.expireStartedAt == 0); OWSAssertDebug(possiblyRead.expireStartedAt == 0);
if (!possiblyRead.read) { if (!possiblyRead.read) {
[newlyReadList addObject:possiblyRead]; [newlyReadList addObject:possiblyRead];

Loading…
Cancel
Save