mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
a6ceaa3cc4
|
@ -133,6 +133,10 @@
|
||||||
76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
|
76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
|
||||||
76EB054018170B33006006FC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76EB03C318170B33006006FC /* AppDelegate.m */; };
|
76EB054018170B33006006FC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76EB03C318170B33006006FC /* AppDelegate.m */; };
|
||||||
7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E1271E743B00848B49 /* OWSSounds.swift */; };
|
7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E1271E743B00848B49 /* OWSSounds.swift */; };
|
||||||
|
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; };
|
||||||
|
7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; };
|
||||||
|
7B1D74AE27C346220030B423 /* UnreadMentionMigtation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AD27C346220030B423 /* UnreadMentionMigtation.swift */; };
|
||||||
|
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; };
|
||||||
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; };
|
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; };
|
||||||
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; };
|
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; };
|
||||||
7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */; };
|
7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */; };
|
||||||
|
@ -786,8 +790,7 @@
|
||||||
FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFF27C4691300510D0C /* MockDataGenerator.swift */; };
|
FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFF27C4691300510D0C /* MockDataGenerator.swift */; };
|
||||||
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; };
|
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; };
|
||||||
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; };
|
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; };
|
||||||
FDC4389E27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; };
|
FDC4389E27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
|
||||||
FDC4389F27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -940,17 +943,6 @@
|
||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
FDC438A227BA2B8A00C60D73 /* Embed Frameworks */ = {
|
|
||||||
isa = PBXCopyFilesBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
dstPath = "";
|
|
||||||
dstSubfolderSpec = 10;
|
|
||||||
files = (
|
|
||||||
FDC4389F27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Embed Frameworks */,
|
|
||||||
);
|
|
||||||
name = "Embed Frameworks";
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
};
|
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
@ -1136,6 +1128,10 @@
|
||||||
76EB03C318170B33006006FC /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
76EB03C318170B33006006FC /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
||||||
7ABE4694B110C1BBCB0E46A2 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig"; sourceTree = "<group>"; };
|
7ABE4694B110C1BBCB0E46A2 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig"; sourceTree = "<group>"; };
|
||||||
7B1581E1271E743B00848B49 /* OWSSounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSSounds.swift; sourceTree = "<group>"; };
|
7B1581E1271E743B00848B49 /* OWSSounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSSounds.swift; sourceTree = "<group>"; };
|
||||||
|
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSENotificationPresenter.swift; sourceTree = "<group>"; };
|
||||||
|
7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = "<group>"; };
|
||||||
|
7B1D74AD27C346220030B423 /* UnreadMentionMigtation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMentionMigtation.swift; sourceTree = "<group>"; };
|
||||||
|
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = "<group>"; };
|
||||||
7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
|
7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
|
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
|
||||||
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
|
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2098,6 +2094,7 @@
|
||||||
C31C219B255BC92200EC2D66 /* Meta */,
|
C31C219B255BC92200EC2D66 /* Meta */,
|
||||||
7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */,
|
7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */,
|
||||||
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */,
|
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */,
|
||||||
|
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */,
|
||||||
);
|
);
|
||||||
path = SessionNotificationServiceExtension;
|
path = SessionNotificationServiceExtension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2310,6 +2307,7 @@
|
||||||
C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */,
|
C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */,
|
||||||
C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */,
|
C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */,
|
||||||
C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */,
|
C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */,
|
||||||
|
7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */,
|
||||||
);
|
);
|
||||||
path = PromiseKit;
|
path = PromiseKit;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2369,6 +2367,7 @@
|
||||||
C33FDB45255A580C00E217F9 /* NSString+SSK.m */,
|
C33FDB45255A580C00E217F9 /* NSString+SSK.m */,
|
||||||
C352A3762557859C00338F3E /* NSTimer+Proxying.h */,
|
C352A3762557859C00338F3E /* NSTimer+Proxying.h */,
|
||||||
C352A36C2557858D00338F3E /* NSTimer+Proxying.m */,
|
C352A36C2557858D00338F3E /* NSTimer+Proxying.m */,
|
||||||
|
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */,
|
||||||
C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */,
|
C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */,
|
||||||
C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */,
|
C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */,
|
||||||
C33FDB14255A580800E217F9 /* OWSMath.h */,
|
C33FDB14255A580800E217F9 /* OWSMath.h */,
|
||||||
|
@ -3071,6 +3070,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B8B32044258C117C0020074B /* ContactsMigration.swift */,
|
B8B32044258C117C0020074B /* ContactsMigration.swift */,
|
||||||
|
7B1D74AD27C346220030B423 /* UnreadMentionMigtation.swift */,
|
||||||
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */,
|
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */,
|
||||||
C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */,
|
C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */,
|
||||||
C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */,
|
C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */,
|
||||||
|
@ -3949,7 +3949,6 @@
|
||||||
C3C2A6EC25539DE700C340D1 /* Sources */,
|
C3C2A6EC25539DE700C340D1 /* Sources */,
|
||||||
C3C2A6ED25539DE700C340D1 /* Frameworks */,
|
C3C2A6ED25539DE700C340D1 /* Frameworks */,
|
||||||
C3C2A6EE25539DE700C340D1 /* Resources */,
|
C3C2A6EE25539DE700C340D1 /* Resources */,
|
||||||
FDC438A227BA2B8A00C60D73 /* Embed Frameworks */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
@ -4463,6 +4462,7 @@
|
||||||
files = (
|
files = (
|
||||||
7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */,
|
7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */,
|
||||||
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */,
|
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */,
|
||||||
|
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -4615,6 +4615,7 @@
|
||||||
C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */,
|
C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */,
|
||||||
C38EF31D255B6DBF007E1867 /* UIImage+OWS.swift in Sources */,
|
C38EF31D255B6DBF007E1867 /* UIImage+OWS.swift in Sources */,
|
||||||
C38EF359255B6DCC007E1867 /* SheetViewController.swift in Sources */,
|
C38EF359255B6DCC007E1867 /* SheetViewController.swift in Sources */,
|
||||||
|
7B1D74AE27C346220030B423 /* UnreadMentionMigtation.swift in Sources */,
|
||||||
B8F5F52925EC4F8A003BF8D4 /* BlockListUIUtils.m in Sources */,
|
B8F5F52925EC4F8A003BF8D4 /* BlockListUIUtils.m in Sources */,
|
||||||
C38EF386255B6DD2007E1867 /* AttachmentApprovalInputAccessoryView.swift in Sources */,
|
C38EF386255B6DD2007E1867 /* AttachmentApprovalInputAccessoryView.swift in Sources */,
|
||||||
B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */,
|
B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */,
|
||||||
|
@ -4655,9 +4656,12 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
C3AABDDF2553ECF00042FF4C /* Array+Utilities.swift in Sources */,
|
||||||
|
7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */,
|
||||||
C3AABDDF2553ECF00042FF4C /* Array+Utilities.swift in Sources */,
|
C3AABDDF2553ECF00042FF4C /* Array+Utilities.swift in Sources */,
|
||||||
C32C5A47256DB8F0003C73A2 /* ECKeyPair+Hexadecimal.swift in Sources */,
|
C32C5A47256DB8F0003C73A2 /* ECKeyPair+Hexadecimal.swift in Sources */,
|
||||||
C3D9E41525676C320040E4F3 /* Storage.swift in Sources */,
|
C3D9E41525676C320040E4F3 /* Storage.swift in Sources */,
|
||||||
|
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */,
|
||||||
C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */,
|
C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */,
|
||||||
C3D9E39B256763C20040E4F3 /* AppContext.m in Sources */,
|
C3D9E39B256763C20040E4F3 /* AppContext.m in Sources */,
|
||||||
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */,
|
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */,
|
||||||
|
@ -5117,7 +5121,6 @@
|
||||||
};
|
};
|
||||||
FDC438A127BA2B8A00C60D73 /* PBXTargetDependency */ = {
|
FDC438A127BA2B8A00C60D73 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
platformFilter = ios;
|
|
||||||
target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */;
|
target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */;
|
||||||
targetProxy = FDC438A027BA2B8A00C60D73 /* PBXContainerItemProxy */;
|
targetProxy = FDC438A027BA2B8A00C60D73 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
@ -5186,7 +5189,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 = 319;
|
CURRENT_PROJECT_VERSION = 322;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||||
|
@ -5211,7 +5214,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.11.21;
|
MARKETING_VERSION = 1.11.22;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -5259,7 +5262,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 = 319;
|
CURRENT_PROJECT_VERSION = 322;
|
||||||
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;
|
||||||
|
@ -5289,7 +5292,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.11.21;
|
MARKETING_VERSION = 1.11.22;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -5325,7 +5328,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 = 319;
|
CURRENT_PROJECT_VERSION = 322;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||||
|
@ -5348,7 +5351,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.11.21;
|
MARKETING_VERSION = 1.11.22;
|
||||||
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.NotificationServiceExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
||||||
|
@ -5399,7 +5402,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 = 319;
|
CURRENT_PROJECT_VERSION = 322;
|
||||||
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;
|
||||||
|
@ -5427,7 +5430,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.11.21;
|
MARKETING_VERSION = 1.11.22;
|
||||||
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.NotificationServiceExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
||||||
|
@ -6335,7 +6338,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Session/Meta/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 = 319;
|
CURRENT_PROJECT_VERSION = 322;
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -6374,7 +6377,7 @@
|
||||||
"$(SRCROOT)",
|
"$(SRCROOT)",
|
||||||
);
|
);
|
||||||
LLVM_LTO = NO;
|
LLVM_LTO = NO;
|
||||||
MARKETING_VERSION = 1.11.21;
|
MARKETING_VERSION = 1.11.22;
|
||||||
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";
|
||||||
|
@ -6406,7 +6409,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Session/Meta/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 = 319;
|
CURRENT_PROJECT_VERSION = 322;
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -6445,7 +6448,7 @@
|
||||||
"$(SRCROOT)",
|
"$(SRCROOT)",
|
||||||
);
|
);
|
||||||
LLVM_LTO = NO;
|
LLVM_LTO = NO;
|
||||||
MARKETING_VERSION = 1.11.21;
|
MARKETING_VERSION = 1.11.22;
|
||||||
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;
|
||||||
|
|
|
@ -692,11 +692,6 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
||||||
}
|
}
|
||||||
|
|
||||||
func delete(_ viewItem: ConversationViewItem) {
|
func delete(_ viewItem: ConversationViewItem) {
|
||||||
if (!self.isUnsendRequestsEnabled) {
|
|
||||||
viewItem.deleteAction()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let message = viewItem.interaction as? TSMessage else { return self.deleteLocally(viewItem) }
|
guard let message = viewItem.interaction as? TSMessage else { return self.deleteLocally(viewItem) }
|
||||||
|
|
||||||
// Handle open group messages the old way
|
// Handle open group messages the old way
|
||||||
|
@ -1117,22 +1112,6 @@ extension ConversationVC: UIDocumentInteractionControllerDelegate {
|
||||||
// MARK: - Message Request Actions
|
// MARK: - Message Request Actions
|
||||||
|
|
||||||
extension ConversationVC {
|
extension ConversationVC {
|
||||||
@objc func handleBackPressed() {
|
|
||||||
// If this thread started as a message request but isn't one anymore then we want to skip the
|
|
||||||
// `MessageRequestsViewController` when going back
|
|
||||||
guard
|
|
||||||
threadStartedAsMessageRequest,
|
|
||||||
!thread.isMessageRequest(),
|
|
||||||
let viewControllers: [UIViewController] = navigationController?.viewControllers,
|
|
||||||
let messageRequestsIndex = viewControllers.firstIndex(where: { $0 is MessageRequestsViewController }),
|
|
||||||
messageRequestsIndex > 0
|
|
||||||
else {
|
|
||||||
navigationController?.popViewController(animated: true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
navigationController?.popToViewController(viewControllers[messageRequestsIndex - 1], animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, with transaction: YapDatabaseReadWriteTransaction, isNewThread: Bool, timestamp: UInt64) -> Promise<Void> {
|
fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, with transaction: YapDatabaseReadWriteTransaction, isNewThread: Bool, timestamp: UInt64) -> Promise<Void> {
|
||||||
guard let contactThread: TSContactThread = thread as? TSContactThread else { return Promise.value(()) }
|
guard let contactThread: TSContactThread = thread as? TSContactThread else { return Promise.value(()) }
|
||||||
|
@ -1204,6 +1183,13 @@ extension ConversationVC {
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
self?.updateNavBarButtons()
|
self?.updateNavBarButtons()
|
||||||
|
if let viewControllers: [UIViewController] = self?.navigationController?.viewControllers,
|
||||||
|
let messageRequestsIndex = viewControllers.firstIndex(where: { $0 is MessageRequestsViewController }),
|
||||||
|
messageRequestsIndex > 0 {
|
||||||
|
var newViewControllers = viewControllers
|
||||||
|
newViewControllers.remove(at: messageRequestsIndex)
|
||||||
|
self?.navigationController?.setViewControllers(newViewControllers, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
// Send a sync message with the details of the contact
|
// Send a sync message with the details of the contact
|
||||||
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
|
||||||
|
|
|
@ -8,11 +8,11 @@ import UIKit
|
||||||
// • Remaining search glitchiness
|
// • Remaining search glitchiness
|
||||||
|
|
||||||
final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversationSettingsViewDelegate, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
|
final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversationSettingsViewDelegate, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
|
||||||
let isUnsendRequestsEnabled = true // Set to true once unsend requests are done on all platforms
|
|
||||||
let thread: TSThread
|
let thread: TSThread
|
||||||
let threadStartedAsMessageRequest: Bool
|
let threadStartedAsMessageRequest: Bool
|
||||||
let focusedMessageID: String? // This is used for global search
|
let focusedMessageID: String? // This is used for global search
|
||||||
var focusedMessageIndexPath: IndexPath?
|
var focusedMessageIndexPath: IndexPath?
|
||||||
|
var initialUnreadCount: UInt = 0
|
||||||
var unreadViewItems: [ConversationViewItem] = []
|
var unreadViewItems: [ConversationViewItem] = []
|
||||||
var scrollButtonBottomConstraint: NSLayoutConstraint?
|
var scrollButtonBottomConstraint: NSLayoutConstraint?
|
||||||
var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint?
|
var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint?
|
||||||
|
@ -78,7 +78,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
|
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var viewModel = ConversationViewModel(thread: thread, focusMessageIdOnOpen: focusedMessageID, delegate: self)
|
lazy var viewModel = ConversationViewModel(thread: thread, focusMessageIdOnOpen: nil, delegate: self)
|
||||||
|
|
||||||
lazy var mediaCache: NSCache<NSString, AnyObject> = {
|
lazy var mediaCache: NSCache<NSString, AnyObject> = {
|
||||||
let result = NSCache<NSString, AnyObject>()
|
let result = NSCache<NSString, AnyObject>()
|
||||||
|
@ -278,11 +278,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
self.threadStartedAsMessageRequest = thread.isMessageRequest()
|
self.threadStartedAsMessageRequest = thread.isMessageRequest()
|
||||||
self.focusedMessageID = focusedMessageID
|
self.focusedMessageID = focusedMessageID
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
var unreadCount: UInt = 0
|
|
||||||
Storage.read { transaction in
|
Storage.read { transaction in
|
||||||
unreadCount = self.thread.unreadMessageCount(transaction: transaction)
|
self.initialUnreadCount = self.thread.unreadMessageCount(transaction: transaction)
|
||||||
}
|
}
|
||||||
let clampedUnreadCount = min(unreadCount, UInt(kConversationInitialMaxRangeSize), UInt(viewItems.endIndex))
|
let clampedUnreadCount = min(self.initialUnreadCount, UInt(kConversationInitialMaxRangeSize), UInt(viewItems.endIndex))
|
||||||
unreadViewItems = clampedUnreadCount != 0 ? [ConversationViewItem](viewItems[viewItems.endIndex - Int(clampedUnreadCount) ..< viewItems.endIndex]) : []
|
unreadViewItems = clampedUnreadCount != 0 ? [ConversationViewItem](viewItems[viewItems.endIndex - Int(clampedUnreadCount) ..< viewItems.endIndex]) : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,10 +398,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
super.viewDidLayoutSubviews()
|
super.viewDidLayoutSubviews()
|
||||||
if !didFinishInitialLayout {
|
if !didFinishInitialLayout {
|
||||||
// Scroll to the last unread message if possible; otherwise scroll to the bottom.
|
// Scroll to the last unread message if possible; otherwise scroll to the bottom.
|
||||||
var unreadCount: UInt = 0
|
|
||||||
Storage.read { transaction in
|
|
||||||
unreadCount = self.thread.unreadMessageCount(transaction: transaction)
|
|
||||||
}
|
|
||||||
// When the unread message count is more than the number of view items of a page,
|
// When the unread message count is more than the number of view items of a page,
|
||||||
// the screen will scroll to the bottom instead of the first unread message.
|
// the screen will scroll to the bottom instead of the first unread message.
|
||||||
// unreadIndicatorIndex is calculated during loading of the viewItems, so it's
|
// unreadIndicatorIndex is calculated during loading of the viewItems, so it's
|
||||||
|
@ -413,7 +408,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
} else {
|
} else {
|
||||||
let firstUnreadMessageIndex = self.viewModel.viewState.unreadIndicatorIndex?.intValue
|
let firstUnreadMessageIndex = self.viewModel.viewState.unreadIndicatorIndex?.intValue
|
||||||
?? (self.viewItems.count - self.unreadViewItems.count)
|
?? (self.viewItems.count - self.unreadViewItems.count)
|
||||||
if unreadCount > 0, let viewItem = self.viewItems[ifValid: firstUnreadMessageIndex], let interactionID = viewItem.interaction.uniqueId {
|
if self.initialUnreadCount > 0, let viewItem = self.viewItems[ifValid: firstUnreadMessageIndex], let interactionID = viewItem.interaction.uniqueId {
|
||||||
self.scrollToInteraction(with: interactionID, position: .top, isAnimated: false)
|
self.scrollToInteraction(with: interactionID, position: .top, isAnimated: false)
|
||||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
self.unreadCountView.alpha = self.scrollButton.alpha
|
||||||
} else {
|
} else {
|
||||||
|
@ -459,6 +454,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
let viewItem = viewItems[indexPath.row]
|
let viewItem = viewItems[indexPath.row]
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: MessageCell.getCellType(for: viewItem).identifier) as! MessageCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: MessageCell.getCellType(for: viewItem).identifier) as! MessageCell
|
||||||
cell.delegate = self
|
cell.delegate = self
|
||||||
|
cell.thread = thread
|
||||||
cell.viewItem = viewItem
|
cell.viewItem = viewItem
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
@ -473,8 +469,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
navigationItem.rightBarButtonItems = []
|
navigationItem.rightBarButtonItems = []
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
navigationItem.leftBarButtonItem = UIViewController.createOWSBackButton(withTarget: self, selector: #selector(handleBackPressed))
|
|
||||||
|
|
||||||
if let contactThread: TSContactThread = thread as? TSContactThread {
|
if let contactThread: TSContactThread = thread as? TSContactThread {
|
||||||
// Don't show the settings button for message requests
|
// Don't show the settings button for message requests
|
||||||
if let contact: Contact = Storage.shared.getContact(with: contactThread.contactSessionID()), contact.isApproved, contact.didApproveMe {
|
if let contact: Contact = Storage.shared.getContact(with: contactThread.contactSessionID()), contact.isApproved, contact.didApproveMe {
|
||||||
|
@ -648,9 +642,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
}
|
}
|
||||||
self.markAllAsRead()
|
self.markAllAsRead()
|
||||||
}
|
}
|
||||||
if shouldScrollToBottom {
|
|
||||||
self.scrollToBottom(isAnimated: false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the input state if this is a contact thread
|
// Update the input state if this is a contact thread
|
||||||
|
@ -753,17 +744,8 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollToBottom(isAnimated: Bool) {
|
func scrollToBottom(isAnimated: Bool) {
|
||||||
guard !isUserScrolling else { return }
|
guard !isUserScrolling && !viewItems.isEmpty else { return }
|
||||||
if let interactionID = viewItems.last?.interaction.uniqueId {
|
messagesTableView.scrollToRow(at: IndexPath(row: viewItems.count - 1, section: 0), at: .bottom, animated: isAnimated)
|
||||||
self.scrollToInteraction(with: interactionID, position: .top, isAnimated: isAnimated)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Ensure the view is fully up to date before we try to scroll to the bottom, since
|
|
||||||
// we use the table view's bounds to determine where the bottom is.
|
|
||||||
view.layoutIfNeeded()
|
|
||||||
let firstContentPageTop: CGFloat = 0
|
|
||||||
let contentOffsetY = max(firstContentPageTop, lastPageTop)
|
|
||||||
messagesTableView.setContentOffset(CGPoint(x: 0, y: contentOffsetY), animated: isAnimated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
@ -796,7 +778,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
|
|
||||||
func autoLoadMoreIfNeeded() {
|
func autoLoadMoreIfNeeded() {
|
||||||
let isMainAppAndActive = CurrentAppContext().isMainAppAndActive
|
let isMainAppAndActive = CurrentAppContext().isMainAppAndActive
|
||||||
guard isMainAppAndActive && viewModel.canLoadMoreItems() && !isLoadingMore
|
guard isMainAppAndActive && didFinishInitialLayout && viewModel.canLoadMoreItems() && !isLoadingMore
|
||||||
&& messagesTableView.contentOffset.y < ConversationVC.loadMoreThreshold else { return }
|
&& messagesTableView.contentOffset.y < ConversationVC.loadMoreThreshold else { return }
|
||||||
isLoadingMore = true
|
isLoadingMore = true
|
||||||
viewModel.loadAnotherPageOfMessages()
|
viewModel.loadAnotherPageOfMessages()
|
||||||
|
@ -836,28 +818,8 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
func showSearchUI() {
|
func showSearchUI() {
|
||||||
isShowingSearchUI = true
|
isShowingSearchUI = true
|
||||||
// Search bar
|
// Search bar
|
||||||
// FIXME: This code is duplicated with SearchBar
|
|
||||||
let searchBar = searchController.uiSearchController.searchBar
|
let searchBar = searchController.uiSearchController.searchBar
|
||||||
searchBar.searchBarStyle = .minimal
|
searchBar.setUpSessionStyle()
|
||||||
searchBar.barStyle = .black
|
|
||||||
searchBar.tintColor = Colors.text
|
|
||||||
let searchIcon = UIImage(named: "searchbar_search")!.asTintedImage(color: Colors.searchBarPlaceholder)
|
|
||||||
searchBar.setImage(searchIcon, for: .search, state: UIControl.State.normal)
|
|
||||||
let clearIcon = UIImage(named: "searchbar_clear")!.asTintedImage(color: Colors.searchBarPlaceholder)
|
|
||||||
searchBar.setImage(clearIcon, for: .clear, state: UIControl.State.normal)
|
|
||||||
let searchTextField: UITextField
|
|
||||||
if #available(iOS 13, *) {
|
|
||||||
searchTextField = searchBar.searchTextField
|
|
||||||
} else {
|
|
||||||
searchTextField = searchBar.value(forKey: "_searchField") as! UITextField
|
|
||||||
}
|
|
||||||
searchTextField.backgroundColor = Colors.searchBarBackground
|
|
||||||
searchTextField.textColor = Colors.text
|
|
||||||
searchTextField.attributedPlaceholder = NSAttributedString(string: "Search", attributes: [ .foregroundColor : Colors.searchBarPlaceholder ])
|
|
||||||
searchTextField.keyboardAppearance = isLightMode ? .default : .dark
|
|
||||||
searchBar.setPositionAdjustment(UIOffset(horizontal: 4, vertical: 0), for: .search)
|
|
||||||
searchBar.searchTextPositionAdjustment = UIOffset(horizontal: 2, vertical: 0)
|
|
||||||
searchBar.setPositionAdjustment(UIOffset(horizontal: -4, vertical: 0), for: .clear)
|
|
||||||
navigationItem.titleView = searchBar
|
navigationItem.titleView = searchBar
|
||||||
// Nav bar buttons
|
// Nav bar buttons
|
||||||
updateNavBarButtons()
|
updateNavBarButtons()
|
||||||
|
|
|
@ -120,9 +120,7 @@ static const int kYapDatabaseRangeMaxLength = 250000;
|
||||||
focusMessageIdOnOpen:(nullable NSString *)focusMessageIdOnOpen
|
focusMessageIdOnOpen:(nullable NSString *)focusMessageIdOnOpen
|
||||||
delegate:(id<ConversationViewModelDelegate>)delegate NS_DESIGNATED_INITIALIZER;
|
delegate:(id<ConversationViewModelDelegate>)delegate NS_DESIGNATED_INITIALIZER;
|
||||||
|
|
||||||
- (void)ensureDynamicInteractionsAndUpdateIfNecessary:(BOOL)updateIfNecessary;
|
- (void)ensureDynamicInteractionsAndUpdateIfNecessary;
|
||||||
|
|
||||||
- (void)clearUnreadMessagesIndicator;
|
|
||||||
|
|
||||||
- (void)loadAnotherPageOfMessages;
|
- (void)loadAnotherPageOfMessages;
|
||||||
|
|
||||||
|
|
|
@ -295,13 +295,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
object:nil];
|
object:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)signalAccountsDidChange:(NSNotification *)notification
|
|
||||||
{
|
|
||||||
OWSAssertIsOnMainThread();
|
|
||||||
|
|
||||||
[self ensureDynamicInteractionsAndUpdateIfNecessary:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)profileWhitelistDidChange:(NSNotification *)notification
|
- (void)profileWhitelistDidChange:(NSNotification *)notification
|
||||||
{
|
{
|
||||||
OWSAssertIsOnMainThread();
|
OWSAssertIsOnMainThread();
|
||||||
|
@ -334,7 +327,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
self.typingIndicatorsSender = [self.typingIndicators typingRecipientIdForThread:self.thread];
|
self.typingIndicatorsSender = [self.typingIndicators typingRecipientIdForThread:self.thread];
|
||||||
self.collapseCutoffDate = [NSDate new];
|
self.collapseCutoffDate = [NSDate new];
|
||||||
|
|
||||||
[self ensureDynamicInteractionsAndUpdateIfNecessary:NO];
|
[self ensureDynamicInteractionsAndUpdateIfNecessary];
|
||||||
[self.primaryStorage updateUIDatabaseConnectionToLatest];
|
[self.primaryStorage updateUIDatabaseConnectionToLatest];
|
||||||
|
|
||||||
[self createNewMessageMapping];
|
[self createNewMessageMapping];
|
||||||
|
@ -490,7 +483,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
self.collapseCutoffDate = [NSDate new];
|
self.collapseCutoffDate = [NSDate new];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)ensureDynamicInteractionsAndUpdateIfNecessary:(BOOL)updateIfNecessary
|
- (void)ensureDynamicInteractionsAndUpdateIfNecessary
|
||||||
{
|
{
|
||||||
OWSAssertIsOnMainThread();
|
OWSAssertIsOnMainThread();
|
||||||
|
|
||||||
|
@ -508,7 +501,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
BOOL didChange = ![NSObject isNullableObject:self.dynamicInteractions equalTo:dynamicInteractions];
|
BOOL didChange = ![NSObject isNullableObject:self.dynamicInteractions equalTo:dynamicInteractions];
|
||||||
self.dynamicInteractions = dynamicInteractions;
|
self.dynamicInteractions = dynamicInteractions;
|
||||||
|
|
||||||
if (didChange && updateIfNecessary) {
|
if (didChange) {
|
||||||
if (![self reloadViewItems]) {
|
if (![self reloadViewItems]) {
|
||||||
OWSFailDebug(@"Failed to reload view items.");
|
OWSFailDebug(@"Failed to reload view items.");
|
||||||
}
|
}
|
||||||
|
@ -517,37 +510,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)clearUnreadMessagesIndicator
|
|
||||||
{
|
|
||||||
OWSAssertIsOnMainThread();
|
|
||||||
|
|
||||||
// TODO: Remove by making unread indicator a view model concern.
|
|
||||||
id<ConversationViewItem> _Nullable oldIndicatorItem = [self.viewState unreadIndicatorViewItem];
|
|
||||||
if (oldIndicatorItem) {
|
|
||||||
// TODO ideally this would be happening within the *same* transaction that caused the unreadMessageIndicator
|
|
||||||
// to be cleared.
|
|
||||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
|
||||||
[oldIndicatorItem.interaction touchWithTransaction:transaction];
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.hasClearedUnreadMessagesIndicator) {
|
|
||||||
// ensureDynamicInteractionsForThread is somewhat expensive
|
|
||||||
// so we don't want to call it unnecessarily.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once we've cleared the unread messages indicator,
|
|
||||||
// make sure we don't show it again.
|
|
||||||
self.hasClearedUnreadMessagesIndicator = YES;
|
|
||||||
|
|
||||||
if (self.dynamicInteractions.unreadIndicator) {
|
|
||||||
// If we've just cleared the "unread messages" indicator,
|
|
||||||
// update the dynamic interactions.
|
|
||||||
[self ensureDynamicInteractionsAndUpdateIfNecessary:YES];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - Storage access
|
#pragma mark - Storage access
|
||||||
|
|
||||||
- (void)uiDatabaseDidUpdateExternally:(NSNotification *)notification
|
- (void)uiDatabaseDidUpdateExternally:(NSNotification *)notification
|
||||||
|
@ -984,7 +946,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
self.collapseCutoffDate = [NSDate new];
|
self.collapseCutoffDate = [NSDate new];
|
||||||
|
|
||||||
[self ensureDynamicInteractionsAndUpdateIfNecessary:NO];
|
[self ensureDynamicInteractionsAndUpdateIfNecessary];
|
||||||
|
|
||||||
if (![self reloadViewItems]) {
|
if (![self reloadViewItems]) {
|
||||||
OWSFailDebug(@"failed to reload view items in resetMapping.");
|
OWSFailDebug(@"failed to reload view items in resetMapping.");
|
||||||
|
@ -1423,7 +1385,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
self.collapseCutoffDate = [NSDate new];
|
self.collapseCutoffDate = [NSDate new];
|
||||||
|
|
||||||
[self ensureDynamicInteractionsAndUpdateIfNecessary:NO];
|
[self ensureDynamicInteractionsAndUpdateIfNecessary];
|
||||||
|
|
||||||
if (![self reloadViewItems]) {
|
if (![self reloadViewItems]) {
|
||||||
OWSFailDebug(@"failed to reload view items in resetMapping.");
|
OWSFailDebug(@"failed to reload view items in resetMapping.");
|
||||||
|
@ -1447,7 +1409,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
self.collapseCutoffDate = [NSDate new];
|
self.collapseCutoffDate = [NSDate new];
|
||||||
|
|
||||||
[self ensureDynamicInteractionsAndUpdateIfNecessary:NO];
|
[self ensureDynamicInteractionsAndUpdateIfNecessary];
|
||||||
|
|
||||||
if (![self reloadViewItems]) {
|
if (![self reloadViewItems]) {
|
||||||
OWSFailDebug(@"failed to reload view items in resetMapping.");
|
OWSFailDebug(@"failed to reload view items in resetMapping.");
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
final class QuoteView : UIView {
|
final class QuoteView : UIView {
|
||||||
private let mode: Mode
|
private let mode: Mode
|
||||||
|
private let thread: TSThread
|
||||||
private let direction: Direction
|
private let direction: Direction
|
||||||
private let hInset: CGFloat
|
private let hInset: CGFloat
|
||||||
private let maxWidth: CGFloat
|
private let maxWidth: CGFloat
|
||||||
|
@ -34,25 +35,6 @@ final class QuoteView : UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var threadID: String {
|
|
||||||
switch mode {
|
|
||||||
case .regular(let viewItem): return viewItem.interaction.uniqueThreadId
|
|
||||||
case .draft(let model): return model.threadId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isGroupThread: Bool {
|
|
||||||
switch mode {
|
|
||||||
case .regular(let viewItem): return viewItem.isGroupThread
|
|
||||||
case .draft(let model):
|
|
||||||
var result = false
|
|
||||||
Storage.read { transaction in
|
|
||||||
result = TSThread.fetch(uniqueId: model.threadId, transaction: transaction)?.isGroupThread() ?? false
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var authorID: String {
|
private var authorID: String {
|
||||||
switch mode {
|
switch mode {
|
||||||
case .regular(let viewItem): return viewItem.quotedReply!.authorId
|
case .regular(let viewItem): return viewItem.quotedReply!.authorId
|
||||||
|
@ -93,8 +75,9 @@ final class QuoteView : UIView {
|
||||||
static let cancelButtonSize: CGFloat = 33
|
static let cancelButtonSize: CGFloat = 33
|
||||||
|
|
||||||
// MARK: Lifecycle
|
// MARK: Lifecycle
|
||||||
init(for viewItem: ConversationViewItem, direction: Direction, hInset: CGFloat, maxWidth: CGFloat) {
|
init(for viewItem: ConversationViewItem, in thread: TSThread?, direction: Direction, hInset: CGFloat, maxWidth: CGFloat) {
|
||||||
self.mode = .regular(viewItem)
|
self.mode = .regular(viewItem)
|
||||||
|
self.thread = thread ?? TSThread.fetch(uniqueId: viewItem.interaction.uniqueThreadId)!
|
||||||
self.maxWidth = maxWidth
|
self.maxWidth = maxWidth
|
||||||
self.direction = direction
|
self.direction = direction
|
||||||
self.hInset = hInset
|
self.hInset = hInset
|
||||||
|
@ -105,6 +88,7 @@ final class QuoteView : UIView {
|
||||||
|
|
||||||
init(for model: OWSQuotedReplyModel, direction: Direction, hInset: CGFloat, maxWidth: CGFloat, delegate: QuoteViewDelegate) {
|
init(for model: OWSQuotedReplyModel, direction: Direction, hInset: CGFloat, maxWidth: CGFloat, delegate: QuoteViewDelegate) {
|
||||||
self.mode = .draft(model)
|
self.mode = .draft(model)
|
||||||
|
self.thread = TSThread.fetch(uniqueId: model.threadId)!
|
||||||
self.maxWidth = maxWidth
|
self.maxWidth = maxWidth
|
||||||
self.direction = direction
|
self.direction = direction
|
||||||
self.hInset = hInset
|
self.hInset = hInset
|
||||||
|
@ -188,16 +172,15 @@ final class QuoteView : UIView {
|
||||||
bodyLabel.lineBreakMode = .byTruncatingTail
|
bodyLabel.lineBreakMode = .byTruncatingTail
|
||||||
let isOutgoing = (direction == .outgoing)
|
let isOutgoing = (direction == .outgoing)
|
||||||
bodyLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
bodyLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||||
bodyLabel.attributedText = given(body) { MentionUtilities.highlightMentions(in: $0, isOutgoingMessage: isOutgoing, threadID: threadID, attributes: [:]) }
|
bodyLabel.attributedText = given(body) { MentionUtilities.highlightMentions(in: $0, isOutgoingMessage: isOutgoing, threadID: thread.uniqueId!, attributes: [:]) } ?? given(attachments.first?.contentType) { NSAttributedString(string: MIMETypeUtil.isAudio($0) ? "Audio" : "Document") } ?? NSAttributedString(string: "Document")
|
||||||
?? given(attachments.first?.contentType) { NSAttributedString(string: MIMETypeUtil.isAudio($0) ? "Audio" : "Document") } ?? NSAttributedString(string: "Document")
|
|
||||||
bodyLabel.textColor = textColor
|
bodyLabel.textColor = textColor
|
||||||
let bodyLabelSize = bodyLabel.systemLayoutSizeFitting(availableSpace)
|
let bodyLabelSize = bodyLabel.systemLayoutSizeFitting(availableSpace)
|
||||||
// Label stack view
|
// Label stack view
|
||||||
var authorLabelHeight: CGFloat?
|
var authorLabelHeight: CGFloat?
|
||||||
if isGroupThread {
|
if let groupThread = thread as? TSGroupThread {
|
||||||
let authorLabel = UILabel()
|
let authorLabel = UILabel()
|
||||||
authorLabel.lineBreakMode = .byTruncatingTail
|
authorLabel.lineBreakMode = .byTruncatingTail
|
||||||
let context: Contact.Context = (TSGroupThread.fetch(uniqueId: threadID)?.isOpenGroup == true) ? .openGroup : .regular
|
let context: Contact.Context = groupThread.isOpenGroup ? .openGroup : .regular
|
||||||
authorLabel.text = Storage.shared.getContact(with: authorID)?.displayName(for: context) ?? authorID
|
authorLabel.text = Storage.shared.getContact(with: authorID)?.displayName(for: context) ?? authorID
|
||||||
authorLabel.textColor = textColor
|
authorLabel.textColor = textColor
|
||||||
authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
authorLabel.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||||
|
@ -225,7 +208,7 @@ final class QuoteView : UIView {
|
||||||
// Constraints
|
// Constraints
|
||||||
contentView.addSubview(mainStackView)
|
contentView.addSubview(mainStackView)
|
||||||
mainStackView.pin(to: contentView)
|
mainStackView.pin(to: contentView)
|
||||||
if !isGroupThread {
|
if !thread.isGroupThread() {
|
||||||
bodyLabel.set(.width, to: bodyLabelSize.width)
|
bodyLabel.set(.width, to: bodyLabelSize.width)
|
||||||
}
|
}
|
||||||
let bodyLabelHeight = bodyLabelSize.height.clamp(0, maxBodyLabelHeight)
|
let bodyLabelHeight = bodyLabelSize.height.clamp(0, maxBodyLabelHeight)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import SessionMessagingKit
|
||||||
|
|
||||||
public enum SwipeState {
|
public enum SwipeState {
|
||||||
case began
|
case began
|
||||||
|
@ -8,7 +9,16 @@ public enum SwipeState {
|
||||||
|
|
||||||
class MessageCell : UITableViewCell {
|
class MessageCell : UITableViewCell {
|
||||||
weak var delegate: MessageCellDelegate?
|
weak var delegate: MessageCellDelegate?
|
||||||
var viewItem: ConversationViewItem? { didSet { update() } }
|
var thread: TSThread? {
|
||||||
|
didSet {
|
||||||
|
if viewItem != nil { update() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var viewItem: ConversationViewItem? {
|
||||||
|
didSet {
|
||||||
|
if thread != nil { update() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Settings
|
// MARK: Settings
|
||||||
class var identifier: String { preconditionFailure("Must be overridden by subclasses.") }
|
class var identifier: String { preconditionFailure("Must be overridden by subclasses.") }
|
||||||
|
|
|
@ -66,6 +66,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
||||||
lazy var bubbleView: UIView = {
|
lazy var bubbleView: UIView = {
|
||||||
let result = UIView()
|
let result = UIView()
|
||||||
result.layer.cornerRadius = VisibleMessageCell.largeCornerRadius
|
result.layer.cornerRadius = VisibleMessageCell.largeCornerRadius
|
||||||
|
result.set(.width, greaterThanOrEqualTo: VisibleMessageCell.largeCornerRadius * 2)
|
||||||
return result
|
return result
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -257,7 +258,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
||||||
messageStatusImageView.image = image
|
messageStatusImageView.image = image
|
||||||
messageStatusImageView.backgroundColor = backgroundColor
|
messageStatusImageView.backgroundColor = backgroundColor
|
||||||
if let message = message as? TSOutgoingMessage {
|
if let message = message as? TSOutgoingMessage {
|
||||||
messageStatusImageView.isHidden = (message.messageState == .sent && message.thread.lastInteraction != message)
|
messageStatusImageView.isHidden = (message.messageState == .sent && thread?.lastInteraction != message)
|
||||||
} else {
|
} else {
|
||||||
messageStatusImageView.isHidden = true
|
messageStatusImageView.isHidden = true
|
||||||
}
|
}
|
||||||
|
@ -322,10 +323,12 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
||||||
linkPreviewView.linkPreviewState = LinkPreviewSent(linkPreview: linkPreview, imageAttachment: viewItem.linkPreviewAttachment)
|
linkPreviewView.linkPreviewState = LinkPreviewSent(linkPreview: linkPreview, imageAttachment: viewItem.linkPreviewAttachment)
|
||||||
snContentView.addSubview(linkPreviewView)
|
snContentView.addSubview(linkPreviewView)
|
||||||
linkPreviewView.pin(to: snContentView)
|
linkPreviewView.pin(to: snContentView)
|
||||||
|
linkPreviewView.layer.mask = bubbleViewMaskLayer
|
||||||
} else if let openGroupInvitationName = message.openGroupInvitationName, let openGroupInvitationURL = message.openGroupInvitationURL {
|
} else if let openGroupInvitationName = message.openGroupInvitationName, let openGroupInvitationURL = message.openGroupInvitationURL {
|
||||||
let openGroupInvitationView = OpenGroupInvitationView(name: openGroupInvitationName, url: openGroupInvitationURL, textColor: bodyLabelTextColor, isOutgoing: isOutgoing)
|
let openGroupInvitationView = OpenGroupInvitationView(name: openGroupInvitationName, url: openGroupInvitationURL, textColor: bodyLabelTextColor, isOutgoing: isOutgoing)
|
||||||
snContentView.addSubview(openGroupInvitationView)
|
snContentView.addSubview(openGroupInvitationView)
|
||||||
openGroupInvitationView.pin(to: snContentView)
|
openGroupInvitationView.pin(to: snContentView)
|
||||||
|
openGroupInvitationView.layer.mask = bubbleViewMaskLayer
|
||||||
} else {
|
} else {
|
||||||
// Stack view
|
// Stack view
|
||||||
let stackView = UIStackView(arrangedSubviews: [])
|
let stackView = UIStackView(arrangedSubviews: [])
|
||||||
|
@ -335,7 +338,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
||||||
if viewItem.quotedReply != nil {
|
if viewItem.quotedReply != nil {
|
||||||
let direction: QuoteView.Direction = isOutgoing ? .outgoing : .incoming
|
let direction: QuoteView.Direction = isOutgoing ? .outgoing : .incoming
|
||||||
let hInset: CGFloat = 2
|
let hInset: CGFloat = 2
|
||||||
let quoteView = QuoteView(for: viewItem, direction: direction, hInset: hInset, maxWidth: maxWidth)
|
let quoteView = QuoteView(for: viewItem, in: thread, direction: direction, hInset: hInset, maxWidth: maxWidth)
|
||||||
let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset))
|
let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset))
|
||||||
stackView.addArrangedSubview(quoteViewContainer)
|
stackView.addArrangedSubview(quoteViewContainer)
|
||||||
}
|
}
|
||||||
|
@ -349,7 +352,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
||||||
}
|
}
|
||||||
case .mediaMessage:
|
case .mediaMessage:
|
||||||
if viewItem.interaction is TSIncomingMessage,
|
if viewItem.interaction is TSIncomingMessage,
|
||||||
let thread = viewItem.interaction.thread as? TSContactThread,
|
let thread = thread as? TSContactThread,
|
||||||
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
||||||
showMediaPlaceholder()
|
showMediaPlaceholder()
|
||||||
} else {
|
} else {
|
||||||
|
@ -382,18 +385,19 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
||||||
}
|
}
|
||||||
case .audio:
|
case .audio:
|
||||||
if viewItem.interaction is TSIncomingMessage,
|
if viewItem.interaction is TSIncomingMessage,
|
||||||
let thread = viewItem.interaction.thread as? TSContactThread,
|
let thread = thread as? TSContactThread,
|
||||||
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
||||||
showMediaPlaceholder()
|
showMediaPlaceholder()
|
||||||
} else {
|
} else {
|
||||||
let voiceMessageView = VoiceMessageView(viewItem: viewItem)
|
let voiceMessageView = VoiceMessageView(viewItem: viewItem)
|
||||||
snContentView.addSubview(voiceMessageView)
|
snContentView.addSubview(voiceMessageView)
|
||||||
voiceMessageView.pin(to: snContentView)
|
voiceMessageView.pin(to: snContentView)
|
||||||
|
voiceMessageView.layer.mask = bubbleViewMaskLayer
|
||||||
viewItem.lastAudioMessageView = voiceMessageView
|
viewItem.lastAudioMessageView = voiceMessageView
|
||||||
}
|
}
|
||||||
case .genericAttachment:
|
case .genericAttachment:
|
||||||
if viewItem.interaction is TSIncomingMessage,
|
if viewItem.interaction is TSIncomingMessage,
|
||||||
let thread = viewItem.interaction.thread as? TSContactThread,
|
let thread = thread as? TSContactThread,
|
||||||
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
Storage.shared.getContact(with: thread.contactSessionID())?.isTrusted != true {
|
||||||
showMediaPlaceholder()
|
showMediaPlaceholder()
|
||||||
} else {
|
} else {
|
||||||
|
@ -673,7 +677,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
||||||
|
|
||||||
private static func shouldShowProfilePicture(for viewItem: ConversationViewItem) -> Bool {
|
private static func shouldShowProfilePicture(for viewItem: ConversationViewItem) -> Bool {
|
||||||
guard let message = viewItem.interaction as? TSMessage else { preconditionFailure() }
|
guard let message = viewItem.interaction as? TSMessage else { preconditionFailure() }
|
||||||
let isGroupThread = message.thread.isGroupThread()
|
let isGroupThread = viewItem.isGroupThread
|
||||||
let senderSessionID = (message as? TSIncomingMessage)?.authorId
|
let senderSessionID = (message as? TSIncomingMessage)?.authorId
|
||||||
return isGroupThread && viewItem.shouldShowSenderProfilePicture && senderSessionID != nil
|
return isGroupThread && viewItem.shouldShowSenderProfilePicture && senderSessionID != nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var recentSearchResults: [String] = Array(Storage.shared.getRecentSearchResults().reversed())
|
var recentSearchResults: [String] = Array(Storage.shared.getRecentSearchResults().reversed())
|
||||||
|
var defaultSearchResults: HomeScreenSearchResultSet = HomeScreenSearchResultSet.noteToSelfOnly
|
||||||
var searchResultSet: HomeScreenSearchResultSet = HomeScreenSearchResultSet.empty
|
var searchResultSet: HomeScreenSearchResultSet = HomeScreenSearchResultSet.empty
|
||||||
private var lastSearchText: String?
|
private var lastSearchText: String?
|
||||||
var searcher: FullTextSearcher {
|
var searcher: FullTextSearcher {
|
||||||
|
@ -106,30 +107,10 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
||||||
var refreshTimer: Timer?
|
var refreshTimer: Timer?
|
||||||
|
|
||||||
private func refreshSearchResults() {
|
private func refreshSearchResults() {
|
||||||
|
|
||||||
guard !searchResultSet.isEmpty else {
|
|
||||||
// To avoid incorrectly showing the "no results" state,
|
|
||||||
// always search immediately if the current result set is empty.
|
|
||||||
refreshTimer?.invalidate()
|
|
||||||
refreshTimer = nil
|
|
||||||
|
|
||||||
updateSearchResults(searchText: searchText)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if refreshTimer != nil {
|
|
||||||
// Don't start a new refresh timer if there's already one active.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshTimer?.invalidate()
|
refreshTimer?.invalidate()
|
||||||
refreshTimer = WeakTimer.scheduledTimer(timeInterval: 0.1, target: self, userInfo: nil, repeats: false) { [weak self] _ in
|
refreshTimer = WeakTimer.scheduledTimer(timeInterval: 0.1, target: self, userInfo: nil, repeats: false) { [weak self] _ in
|
||||||
guard let self = self else {
|
guard let self = self else { return }
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.updateSearchResults(searchText: self.searchText)
|
self.updateSearchResults(searchText: self.searchText)
|
||||||
self.refreshTimer = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +118,7 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
||||||
|
|
||||||
let searchText = rawSearchText.stripped
|
let searchText = rawSearchText.stripped
|
||||||
guard searchText.count > 0 else {
|
guard searchText.count > 0 else {
|
||||||
searchResultSet = HomeScreenSearchResultSet.noteToSelfOnly
|
searchResultSet = defaultSearchResults
|
||||||
lastSearchText = nil
|
lastSearchText = nil
|
||||||
reloadTableData()
|
reloadTableData()
|
||||||
return
|
return
|
||||||
|
@ -152,13 +133,14 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
// The max search result count is set according to the keyword length. This is just a workaround for performance issue.
|
// The max search result count is set according to the keyword length. This is just a workaround for performance issue.
|
||||||
// The longer and more accurate the keyword is, the less search results should there be.
|
// The longer and more accurate the keyword is, the less search results should there be.
|
||||||
searchResults = self.searcher.searchForHomeScreen(searchText: searchText, maxSearchResults: min(searchText.count * 50, 500), transaction: transaction)
|
searchResults = self.searcher.searchForHomeScreen(searchText: searchText, maxSearchResults: 500, transaction: transaction)
|
||||||
}, completionBlock: { [weak self] in
|
}, completionBlock: { [weak self] in
|
||||||
AssertIsOnMainThread()
|
AssertIsOnMainThread()
|
||||||
guard let self = self, let results = searchResults, self.lastSearchText == searchText else { return }
|
guard let self = self, let results = searchResults, self.lastSearchText == searchText else { return }
|
||||||
self.searchResultSet = results
|
self.searchResultSet = results
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.reloadTableData()
|
self.reloadTableData()
|
||||||
|
self.refreshTimer = nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,12 +192,12 @@ extension GlobalSearchViewController {
|
||||||
SNLog("shouldn't be able to tap 'no results' section")
|
SNLog("shouldn't be able to tap 'no results' section")
|
||||||
case .contacts:
|
case .contacts:
|
||||||
let sectionResults = searchResultSet.conversations
|
let sectionResults = searchResultSet.conversations
|
||||||
guard let searchResult = sectionResults[safe: indexPath.row], let threadId = searchResult.thread.threadRecord.uniqueId, let thread = TSThread.fetch(uniqueId: threadId) else { return }
|
guard let searchResult = sectionResults[safe: indexPath.row] else { return }
|
||||||
show(thread, highlightedMessageID: nil, animated: true)
|
show(searchResult.thread.threadRecord, highlightedMessageID: nil, animated: true)
|
||||||
case .messages:
|
case .messages:
|
||||||
let sectionResults = searchResultSet.messages
|
let sectionResults = searchResultSet.messages
|
||||||
guard let searchResult = sectionResults[safe: indexPath.row], let threadId = searchResult.thread.threadRecord.uniqueId, let thread = TSThread.fetch(uniqueId: threadId) else { return }
|
guard let searchResult = sectionResults[safe: indexPath.row] else { return }
|
||||||
show(thread, highlightedMessageID: searchResult.messageId, animated: true)
|
show(searchResult.thread.threadRecord, highlightedMessageID: searchResult.message?.uniqueId, animated: true)
|
||||||
case .recent:
|
case .recent:
|
||||||
guard let threadId = recentSearchResults[safe: indexPath.row], let thread = TSThread.fetch(uniqueId: threadId) else { return }
|
guard let threadId = recentSearchResults[safe: indexPath.row], let thread = TSThread.fetch(uniqueId: threadId) else { return }
|
||||||
show(thread, highlightedMessageID: nil, animated: true, isFromRecent: true)
|
show(thread, highlightedMessageID: nil, animated: true, isFromRecent: true)
|
||||||
|
@ -354,7 +336,7 @@ extension GlobalSearchViewController {
|
||||||
cell.isShowingGlobalSearchResult = true
|
cell.isShowingGlobalSearchResult = true
|
||||||
let searchResult = sectionResults[safe: indexPath.row]
|
let searchResult = sectionResults[safe: indexPath.row]
|
||||||
cell.threadViewModel = searchResult?.thread
|
cell.threadViewModel = searchResult?.thread
|
||||||
cell.configure(messageDate: searchResult?.messageDate, snippet: searchResult?.snippet, searchText: searchResultSet.searchText)
|
cell.configure(snippet: searchResult?.snippet, searchText: searchResultSet.searchText)
|
||||||
return cell
|
return cell
|
||||||
case .messages:
|
case .messages:
|
||||||
let sectionResults = searchResultSet.messages
|
let sectionResults = searchResultSet.messages
|
||||||
|
@ -362,9 +344,7 @@ extension GlobalSearchViewController {
|
||||||
cell.isShowingGlobalSearchResult = true
|
cell.isShowingGlobalSearchResult = true
|
||||||
let searchResult = sectionResults[safe: indexPath.row]
|
let searchResult = sectionResults[safe: indexPath.row]
|
||||||
cell.threadViewModel = searchResult?.thread
|
cell.threadViewModel = searchResult?.thread
|
||||||
var message: TSMessage? = nil
|
cell.configure(snippet: searchResult?.snippet, searchText: searchResultSet.searchText, message: searchResult?.message)
|
||||||
if let messageId = searchResult?.messageId { message = TSMessage.fetch(uniqueId: messageId) }
|
|
||||||
cell.configure(messageDate: searchResult?.messageDate, snippet: searchResult?.snippet, searchText: searchResultSet.searchText, message: message)
|
|
||||||
return cell
|
return cell
|
||||||
case .recent:
|
case .recent:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
|
||||||
|
|
|
@ -20,6 +20,8 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
||||||
return result
|
return result
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
private var isReloading = false
|
||||||
|
|
||||||
// MARK: UI Components
|
// MARK: UI Components
|
||||||
private lazy var seedReminderView: SeedReminderView = {
|
private lazy var seedReminderView: SeedReminderView = {
|
||||||
let result = SeedReminderView(hasContinueButton: true)
|
let result = SeedReminderView(hasContinueButton: true)
|
||||||
|
@ -135,6 +137,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
||||||
notificationCenter.addObserver(self, selector: #selector(handleLocalProfileDidChangeNotification(_:)), name: Notification.Name(kNSNotificationName_LocalProfileDidChange), object: nil)
|
notificationCenter.addObserver(self, selector: #selector(handleLocalProfileDidChangeNotification(_:)), name: Notification.Name(kNSNotificationName_LocalProfileDidChange), object: nil)
|
||||||
notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil)
|
notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil)
|
||||||
notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, object: nil)
|
notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, object: nil)
|
||||||
|
notificationCenter.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: .OWSApplicationDidBecomeActive, object: nil)
|
||||||
// Threads (part 2)
|
// Threads (part 2)
|
||||||
threads = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup, TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
|
threads = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup, TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
|
||||||
threads.setIsReversed(true, forGroup: TSInboxGroup)
|
threads.setIsReversed(true, forGroup: TSInboxGroup)
|
||||||
|
@ -167,6 +170,10 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
||||||
reload()
|
reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func applicationDidBecomeActive(_ notification: Notification) {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
}
|
}
|
||||||
|
@ -209,6 +216,8 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
||||||
|
|
||||||
private func reload() {
|
private func reload() {
|
||||||
AssertIsOnMainThread()
|
AssertIsOnMainThread()
|
||||||
|
guard !isReloading else { return }
|
||||||
|
isReloading = true
|
||||||
dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
|
dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
|
||||||
dbConnection.read { transaction in
|
dbConnection.read { transaction in
|
||||||
self.threads.update(with: transaction)
|
self.threads.update(with: transaction)
|
||||||
|
@ -216,6 +225,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
||||||
threadViewModelCache.removeAll()
|
threadViewModelCache.removeAll()
|
||||||
tableView.reloadData()
|
tableView.reloadData()
|
||||||
emptyStateView.isHidden = (threadCount != 0)
|
emptyStateView.isHidden = (threadCount != 0)
|
||||||
|
isReloading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) {
|
@objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) {
|
||||||
|
|
|
@ -6,7 +6,11 @@ import SessionMessagingKit
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
|
class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
|
||||||
private var threads: YapDatabaseViewMappings!
|
private var threads: YapDatabaseViewMappings! = {
|
||||||
|
let result = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup ], view: TSThreadDatabaseViewExtensionName)
|
||||||
|
result.setIsReversed(true, forGroup: TSMessageRequestGroup)
|
||||||
|
return result
|
||||||
|
}()
|
||||||
private var threadViewModelCache: [String: ThreadViewModel] = [:] // Thread ID to ThreadViewModel
|
private var threadViewModelCache: [String: ThreadViewModel] = [:] // Thread ID to ThreadViewModel
|
||||||
private var tableViewTopConstraint: NSLayoutConstraint!
|
private var tableViewTopConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
@ -85,16 +89,13 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
||||||
|
|
||||||
ViewControllerUtilities.setUpDefaultSessionStyle(for: self, title: NSLocalizedString("MESSAGE_REQUESTS_TITLE", comment: ""), hasCustomBackButton: false)
|
ViewControllerUtilities.setUpDefaultSessionStyle(for: self, title: NSLocalizedString("MESSAGE_REQUESTS_TITLE", comment: ""), hasCustomBackButton: false)
|
||||||
|
|
||||||
// Threads (part 1)
|
|
||||||
// Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
|
|
||||||
dbConnection.beginLongLivedReadTransaction()
|
|
||||||
|
|
||||||
// Add the UI (MUST be done after the thread freeze so the 'tableView' creation and setting
|
// Add the UI (MUST be done after the thread freeze so the 'tableView' creation and setting
|
||||||
// the dataSource has the correct data)
|
// the dataSource has the correct data)
|
||||||
view.addSubview(tableView)
|
view.addSubview(tableView)
|
||||||
view.addSubview(emptyStateLabel)
|
view.addSubview(emptyStateLabel)
|
||||||
view.addSubview(fadeView)
|
view.addSubview(fadeView)
|
||||||
view.addSubview(clearAllButton)
|
view.addSubview(clearAllButton)
|
||||||
|
setupLayout()
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
|
@ -116,18 +117,11 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
// Threads (part 2)
|
reload()
|
||||||
threads = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
|
|
||||||
dbConnection.read { transaction in
|
|
||||||
self.threads.update(with: transaction) // Perform the initial update
|
|
||||||
}
|
|
||||||
|
|
||||||
setupLayout()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
reload()
|
reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +183,6 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
||||||
tableView.reloadData()
|
tableView.reloadData()
|
||||||
clearAllButton.isHidden = (messageRequestCount == 0)
|
clearAllButton.isHidden = (messageRequestCount == 0)
|
||||||
emptyStateLabel.isHidden = (messageRequestCount != 0)
|
emptyStateLabel.isHidden = (messageRequestCount != 0)
|
||||||
emptyStateLabel.isHidden = (messageRequestCount != 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) {
|
@objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) {
|
||||||
|
|
|
@ -584,12 +584,6 @@ static NSTimeInterval launchStartedAt;
|
||||||
[self.pushRegistrationManager didReceiveVanillaPushToken:deviceToken];
|
[self.pushRegistrationManager didReceiveVanillaPushToken:deviceToken];
|
||||||
|
|
||||||
OWSLogInfo(@"Registering for push notifications with token: %@.", deviceToken);
|
OWSLogInfo(@"Registering for push notifications with token: %@.", deviceToken);
|
||||||
BOOL isUsingFullAPNs = [NSUserDefaults.standardUserDefaults boolForKey:@"isUsingFullAPNs"];
|
|
||||||
if (isUsingFullAPNs) {
|
|
||||||
__unused AnyPromise *promise = [LKPushNotificationAPI registerWithToken:deviceToken hexEncodedPublicKey:self.tsAccountManager.localNumber isForcedUpdate:NO];
|
|
||||||
} else {
|
|
||||||
__unused AnyPromise *promise = [LKPushNotificationAPI unregisterToken:deviceToken];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
|
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
|
||||||
|
|
|
@ -226,6 +226,9 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic
|
||||||
- (void)setMainAppBadgeNumber:(NSInteger)value
|
- (void)setMainAppBadgeNumber:(NSInteger)value
|
||||||
{
|
{
|
||||||
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:value];
|
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:value];
|
||||||
|
NSUserDefaults *sharedUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.loki-project.loki-messenger"];
|
||||||
|
[sharedUserDefaults setInteger:value forKey:@"currentBadgeNumber"];
|
||||||
|
[sharedUserDefaults synchronize];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (nullable UIViewController *)frontmostViewController
|
- (nullable UIViewController *)frontmostViewController
|
||||||
|
|
|
@ -167,16 +167,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
// so just ignore those and if the user has hidden message requests then we want to show the
|
// so just ignore those and if the user has hidden message requests then we want to show the
|
||||||
// notification regardless of how many message requests there are)
|
// notification regardless of how many message requests there are)
|
||||||
if !thread.isGroupThread() && thread.isMessageRequest() && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
if !thread.isGroupThread() && thread.isMessageRequest() && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
||||||
let dbConnection: YapDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
|
let threads = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
||||||
dbConnection.objectCacheLimit = 2
|
|
||||||
dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
|
|
||||||
let threads: YapDatabaseViewMappings = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup ], view: TSThreadDatabaseViewExtensionName)
|
|
||||||
dbConnection.read { transaction in
|
|
||||||
threads.update(with: transaction) // Perform the initial update
|
|
||||||
}
|
|
||||||
|
|
||||||
let numMessageRequests = threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
let numMessageRequests = threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
||||||
dbConnection.endLongLivedReadTransaction()
|
|
||||||
|
|
||||||
// Allow this to show a notification if there are no message requests (ie. this is the first one)
|
// Allow this to show a notification if there are no message requests (ie. this is the first one)
|
||||||
guard numMessageRequests == 0 else { return }
|
guard numMessageRequests == 0 else { return }
|
||||||
|
@ -225,7 +217,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
notificationTitle = (thread.isMessageRequest() ? "Session" : senderName)
|
notificationTitle = (thread.isMessageRequest() ? "Session" : senderName)
|
||||||
|
|
||||||
case is TSGroupThread:
|
case is TSGroupThread:
|
||||||
var groupName = thread.name()
|
var groupName = thread.name(with: transaction)
|
||||||
if groupName.count < 1 {
|
if groupName.count < 1 {
|
||||||
groupName = MessageStrings.newGroupDefaultTitle
|
groupName = MessageStrings.newGroupDefaultTitle
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ enum Onboarding {
|
||||||
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = x25519PublicKey
|
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = x25519PublicKey
|
||||||
Storage.writeSync { transaction in
|
Storage.writeSync { transaction in
|
||||||
let user = Contact(sessionID: x25519PublicKey)
|
let user = Contact(sessionID: x25519PublicKey)
|
||||||
|
user.isApproved = true
|
||||||
|
user.didApproveMe = true
|
||||||
Storage.shared.setContact(user, using: transaction)
|
Storage.shared.setContact(user, using: transaction)
|
||||||
}
|
}
|
||||||
switch self {
|
switch self {
|
||||||
|
|
|
@ -84,7 +84,7 @@ class BaseVC : UIViewController {
|
||||||
|
|
||||||
internal func setUpNavBarSessionHeading() {
|
internal func setUpNavBarSessionHeading() {
|
||||||
let headingImageView = UIImageView()
|
let headingImageView = UIImageView()
|
||||||
headingImageView.tintColor = Colors.sessionHeading
|
headingImageView.tintColor = Colors.text
|
||||||
headingImageView.image = UIImage(named: "SessionHeading")?.withRenderingMode(.alwaysTemplate)
|
headingImageView.image = UIImage(named: "SessionHeading")?.withRenderingMode(.alwaysTemplate)
|
||||||
headingImageView.contentMode = .scaleAspectFit
|
headingImageView.contentMode = .scaleAspectFit
|
||||||
headingImageView.set(.width, to: 150)
|
headingImageView.set(.width, to: 150)
|
||||||
|
|
|
@ -221,10 +221,11 @@ final class ConversationCell : UITableViewCell {
|
||||||
timestampLabel.isHidden = true
|
timestampLabel.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
public func configure(messageDate: Date?, snippet: String?, searchText: String, message: TSMessage? = nil) {
|
public func configure(snippet: String?, searchText: String, message: TSMessage? = nil) {
|
||||||
let normalizedSearchText = searchText.lowercased()
|
let normalizedSearchText = searchText.lowercased()
|
||||||
if let messageDate = messageDate, let snippet = snippet {
|
if let messageTimestamp = message?.timestamp, let snippet = snippet {
|
||||||
// Message
|
// Message
|
||||||
|
let messageDate = NSDate.ows_date(withMillisecondsSince1970: messageTimestamp)
|
||||||
displayNameLabel.attributedText = NSMutableAttributedString(string: getDisplayName(), attributes: [.foregroundColor:Colors.text])
|
displayNameLabel.attributedText = NSMutableAttributedString(string: getDisplayName(), attributes: [.foregroundColor:Colors.text])
|
||||||
timestampLabel.isHidden = false
|
timestampLabel.isHidden = false
|
||||||
timestampLabel.text = DateUtil.formatDate(forDisplay: messageDate)
|
timestampLabel.text = DateUtil.formatDate(forDisplay: messageDate)
|
||||||
|
|
|
@ -10,27 +10,19 @@ public final class MentionUtilities : NSObject {
|
||||||
|
|
||||||
@objc public static func highlightMentions(in string: String, isOutgoingMessage: Bool, threadID: String, attributes: [NSAttributedString.Key:Any]) -> NSAttributedString {
|
@objc public static func highlightMentions(in string: String, isOutgoingMessage: Bool, threadID: String, attributes: [NSAttributedString.Key:Any]) -> NSAttributedString {
|
||||||
let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID)
|
let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID)
|
||||||
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
|
|
||||||
MentionsManager.populateUserPublicKeyCacheIfNeeded(for: threadID, in: transaction)
|
|
||||||
}
|
|
||||||
var string = string
|
var string = string
|
||||||
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]{66}", options: [])
|
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]{66}", options: [])
|
||||||
let knownPublicKeys = MentionsManager.userPublicKeyCache[threadID] ?? [] // Should always be populated at this point
|
|
||||||
var mentions: [(range: NSRange, publicKey: String)] = []
|
var mentions: [(range: NSRange, publicKey: String)] = []
|
||||||
var outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: string.utf16.count))
|
var outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: string.utf16.count))
|
||||||
while let match = outerMatch {
|
while let match = outerMatch {
|
||||||
let publicKey = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @
|
let publicKey = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @
|
||||||
let matchEnd: Int
|
let matchEnd: Int
|
||||||
if knownPublicKeys.contains(publicKey) {
|
let context: Contact.Context = (openGroupV2 != nil) ? .openGroup : .regular
|
||||||
let context: Contact.Context = (openGroupV2 != nil) ? .openGroup : .regular
|
let displayName = Storage.shared.getContact(with: publicKey)?.displayName(for: context)
|
||||||
let displayName = Storage.shared.getContact(with: publicKey)?.displayName(for: context)
|
if let displayName = displayName {
|
||||||
if let displayName = displayName {
|
string = (string as NSString).replacingCharacters(in: match.range, with: "@\(displayName)")
|
||||||
string = (string as NSString).replacingCharacters(in: match.range, with: "@\(displayName)")
|
mentions.append((range: NSRange(location: match.range.location, length: displayName.utf16.count + 1), publicKey: publicKey)) // + 1 to include the @
|
||||||
mentions.append((range: NSRange(location: match.range.location, length: displayName.utf16.count + 1), publicKey: publicKey)) // + 1 to include the @
|
matchEnd = match.range.location + displayName.utf16.count
|
||||||
matchEnd = match.range.location + displayName.utf16.count
|
|
||||||
} else {
|
|
||||||
matchEnd = match.range.location + match.range.length
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
matchEnd = match.range.location + match.range.length
|
matchEnd = match.range.location + match.range.length
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,7 @@ void VerifyRegistrationsForPrimaryStorage(OWSStorage *storage)
|
||||||
[self loadDatabase];
|
[self loadDatabase];
|
||||||
|
|
||||||
_dbReadPool = [[YapDatabaseConnectionPool alloc] initWithDatabase:self.database];
|
_dbReadPool = [[YapDatabaseConnectionPool alloc] initWithDatabase:self.database];
|
||||||
|
_dbReadPool.connectionLimit = 10; // Increase max read connection limit. Default is 3.
|
||||||
_dbReadWriteConnection = [self newDatabaseConnection];
|
_dbReadWriteConnection = [self newDatabaseConnection];
|
||||||
_uiDatabaseConnection = [self newDatabaseConnection];
|
_uiDatabaseConnection = [self newDatabaseConnection];
|
||||||
|
|
||||||
|
|
|
@ -103,14 +103,6 @@ public final class VisibleMessage : Message {
|
||||||
// TODO: Contact
|
// TODO: Contact
|
||||||
// Open group invitation
|
// Open group invitation
|
||||||
if let openGroupInvitation = openGroupInvitation, let openGroupInvitationProto = openGroupInvitation.toProto() { dataMessage.setOpenGroupInvitation(openGroupInvitationProto) }
|
if let openGroupInvitation = openGroupInvitation, let openGroupInvitationProto = openGroupInvitation.toProto() { dataMessage.setOpenGroupInvitation(openGroupInvitationProto) }
|
||||||
// Expiration timer
|
|
||||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
|
||||||
// if it receives a message without the current expiration timer value attached to it...
|
|
||||||
var expiration: UInt32 = 0
|
|
||||||
if let disappearingMessagesConfiguration = OWSDisappearingMessagesConfiguration.fetch(uniqueId: threadID!, transaction: transaction) {
|
|
||||||
expiration = disappearingMessagesConfiguration.isEnabled ? disappearingMessagesConfiguration.durationSeconds : 0
|
|
||||||
}
|
|
||||||
dataMessage.setExpireTimer(expiration)
|
|
||||||
// Group context
|
// Group context
|
||||||
do {
|
do {
|
||||||
try setGroupContextIfNeeded(on: dataMessage, using: transaction)
|
try setGroupContextIfNeeded(on: dataMessage, using: transaction)
|
||||||
|
|
|
@ -185,7 +185,7 @@ NSUInteger const TSAttachmentSchemaVersion = 4;
|
||||||
if (self.isVoiceMessage || !self.sourceFilename || self.sourceFilename.length == 0) {
|
if (self.isVoiceMessage || !self.sourceFilename || self.sourceFilename.length == 0) {
|
||||||
attachmentString = NSLocalizedString(@"ATTACHMENT_TYPE_VOICE_MESSAGE",
|
attachmentString = NSLocalizedString(@"ATTACHMENT_TYPE_VOICE_MESSAGE",
|
||||||
@"Short text label for a voice message attachment, used for thread preview and on the lock screen");
|
@"Short text label for a voice message attachment, used for thread preview and on the lock screen");
|
||||||
return [NSString stringWithFormat:@"🎤 %@", attachmentString];
|
return [NSString stringWithFormat:@"🎙️ %@", attachmentString];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -403,7 +403,7 @@ extension MessageReceiver {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the user if needed
|
// Notify the user if needed
|
||||||
guard (isMainAppAndActive || isBackgroundPoll), let tsIncomingMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) as? TSIncomingMessage,
|
guard let tsIncomingMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction) as? TSIncomingMessage,
|
||||||
let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return tsMessageID }
|
let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return tsMessageID }
|
||||||
// Use the same identifier for notifications when in backgroud polling to prevent spam
|
// Use the same identifier for notifications when in backgroud polling to prevent spam
|
||||||
let notificationIdentifier = isBackgroundPoll ? thread.uniqueId : UUID().uuidString
|
let notificationIdentifier = isBackgroundPoll ? thread.uniqueId : UUID().uuidString
|
||||||
|
@ -467,7 +467,7 @@ extension MessageReceiver {
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Closed Groups
|
// MARK: - Closed Groups
|
||||||
private static func handleClosedGroupControlMessage(_ message: ClosedGroupControlMessage, using transaction: Any) {
|
public static func handleClosedGroupControlMessage(_ message: ClosedGroupControlMessage, using transaction: Any) {
|
||||||
switch message.kind! {
|
switch message.kind! {
|
||||||
case .new: handleNewClosedGroup(message, using: transaction)
|
case .new: handleNewClosedGroup(message, using: transaction)
|
||||||
case .encryptionKeyPair: handleClosedGroupEncryptionKeyPair(message, using: transaction)
|
case .encryptionKeyPair: handleClosedGroupEncryptionKeyPair(message, using: transaction)
|
||||||
|
|
|
@ -59,6 +59,7 @@ public final class MessageSender : NSObject {
|
||||||
signalAttachments.forEach {
|
signalAttachments.forEach {
|
||||||
let attachment = TSAttachmentStream(contentType: $0.mimeType, byteCount: UInt32($0.dataLength), sourceFilename: $0.sourceFilename,
|
let attachment = TSAttachmentStream(contentType: $0.mimeType, byteCount: UInt32($0.dataLength), sourceFilename: $0.sourceFilename,
|
||||||
caption: $0.captionText, albumMessageId: tsMessage.uniqueId!)
|
caption: $0.captionText, albumMessageId: tsMessage.uniqueId!)
|
||||||
|
attachment.attachmentType = $0.isVoiceMessage ? .voiceMessage : .default
|
||||||
attachments.append(attachment)
|
attachments.append(attachment)
|
||||||
attachment.write($0.dataSource)
|
attachment.write($0.dataSource)
|
||||||
attachment.save(with: transaction)
|
attachment.save(with: transaction)
|
||||||
|
|
|
@ -61,13 +61,11 @@ public final class ClosedGroupPoller : NSObject {
|
||||||
|
|
||||||
// MARK: Private API
|
// MARK: Private API
|
||||||
private func setUpPolling(for groupPublicKey: String) {
|
private func setUpPolling(for groupPublicKey: String) {
|
||||||
poll(groupPublicKey).done2 { [weak self] _ in
|
Threading.pollerQueue.async {
|
||||||
DispatchQueue.main.async { // Timers don't do well on background queues
|
self.poll(groupPublicKey).done(on: Threading.pollerQueue) { [weak self] _ in
|
||||||
self?.pollRecursively(groupPublicKey)
|
self?.pollRecursively(groupPublicKey)
|
||||||
}
|
}.catch(on: Threading.pollerQueue) { [weak self] error in
|
||||||
}.catch2 { [weak self] error in
|
// The error is logged in poll(_:)
|
||||||
// The error is logged in poll(_:)
|
|
||||||
DispatchQueue.main.async { // Timers don't do well on background queues
|
|
||||||
self?.pollRecursively(groupPublicKey)
|
self?.pollRecursively(groupPublicKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,15 +85,13 @@ public final class ClosedGroupPoller : NSObject {
|
||||||
let a = (ClosedGroupPoller.maxPollInterval - minPollInterval) / limit
|
let a = (ClosedGroupPoller.maxPollInterval - minPollInterval) / limit
|
||||||
let nextPollInterval = a * min(timeSinceLastMessage, limit) + minPollInterval
|
let nextPollInterval = a * min(timeSinceLastMessage, limit) + minPollInterval
|
||||||
SNLog("Next poll interval for closed group with public key: \(groupPublicKey) is \(nextPollInterval) s.")
|
SNLog("Next poll interval for closed group with public key: \(groupPublicKey) is \(nextPollInterval) s.")
|
||||||
timers[groupPublicKey] = Timer.scheduledTimer(withTimeInterval: nextPollInterval, repeats: false) { [weak self] timer in
|
timers[groupPublicKey] = Timer.scheduledTimerOnMainThread(withTimeInterval: nextPollInterval, repeats: false) { [weak self] timer in
|
||||||
timer.invalidate()
|
timer.invalidate()
|
||||||
self?.poll(groupPublicKey).done2 { _ in
|
Threading.pollerQueue.async {
|
||||||
DispatchQueue.main.async { // Timers don't do well on background queues
|
self?.poll(groupPublicKey).done(on: Threading.pollerQueue) { _ in
|
||||||
self?.pollRecursively(groupPublicKey)
|
self?.pollRecursively(groupPublicKey)
|
||||||
}
|
}.catch(on: Threading.pollerQueue) { error in
|
||||||
}.catch2 { error in
|
// The error is logged in poll(_:)
|
||||||
// The error is logged in poll(_:)
|
|
||||||
DispatchQueue.main.async { // Timers don't do well on background queues
|
|
||||||
self?.pollRecursively(groupPublicKey)
|
self?.pollRecursively(groupPublicKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,14 +19,11 @@ public final class OpenGroupPollerV2 : NSObject {
|
||||||
|
|
||||||
@objc public func startIfNeeded() {
|
@objc public func startIfNeeded() {
|
||||||
guard !hasStarted else { return }
|
guard !hasStarted else { return }
|
||||||
DispatchQueue.main.async { [weak self] in // Timers don't do well on background queues
|
hasStarted = true
|
||||||
guard let strongSelf = self else { return }
|
timer = Timer.scheduledTimerOnMainThread(withTimeInterval: pollInterval, repeats: true) { _ in
|
||||||
strongSelf.hasStarted = true
|
self.poll().retainUntilComplete()
|
||||||
strongSelf.timer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollInterval, repeats: true) { _ in
|
|
||||||
self?.poll().retainUntilComplete()
|
|
||||||
}
|
|
||||||
strongSelf.poll().retainUntilComplete()
|
|
||||||
}
|
}
|
||||||
|
poll().retainUntilComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func stop() {
|
@objc public func stop() {
|
||||||
|
@ -46,15 +43,17 @@ public final class OpenGroupPollerV2 : NSObject {
|
||||||
self.isPolling = true
|
self.isPolling = true
|
||||||
let (promise, seal) = Promise<Void>.pending()
|
let (promise, seal) = Promise<Void>.pending()
|
||||||
promise.retainUntilComplete()
|
promise.retainUntilComplete()
|
||||||
OpenGroupAPIV2.compactPoll(server).done(on: OpenGroupAPIV2.workQueue) { [weak self] bodies in
|
Threading.pollerQueue.async {
|
||||||
guard let self = self else { return }
|
OpenGroupAPIV2.compactPoll(self.server).done(on: OpenGroupAPIV2.workQueue) { [weak self] bodies in
|
||||||
self.isPolling = false
|
guard let self = self else { return }
|
||||||
bodies.forEach { self.handleCompactPollBody($0, isBackgroundPoll: isBackgroundPoll) }
|
self.isPolling = false
|
||||||
seal.fulfill(())
|
bodies.forEach { self.handleCompactPollBody($0, isBackgroundPoll: isBackgroundPoll) }
|
||||||
}.catch(on: OpenGroupAPIV2.workQueue) { error in
|
seal.fulfill(())
|
||||||
SNLog("Open group polling failed due to error: \(error).")
|
}.catch(on: OpenGroupAPIV2.workQueue) { error in
|
||||||
self.isPolling = false
|
SNLog("Open group polling failed due to error: \(error).")
|
||||||
seal.fulfill(()) // The promise is just used to keep track of when we're done
|
self.isPolling = false
|
||||||
|
seal.fulfill(()) // The promise is just used to keep track of when we're done
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,19 +45,22 @@ public final class Poller : NSObject {
|
||||||
// MARK: Private API
|
// MARK: Private API
|
||||||
private func setUpPolling() {
|
private func setUpPolling() {
|
||||||
guard isPolling else { return }
|
guard isPolling else { return }
|
||||||
let _ = SnodeAPI.getSwarm(for: getUserHexEncodedPublicKey()).then2 { [weak self] _ -> Promise<Void> in
|
Threading.pollerQueue.async {
|
||||||
guard let strongSelf = self else { return Promise { $0.fulfill(()) } }
|
let _ = SnodeAPI.getSwarm(for: getUserHexEncodedPublicKey()).then(on: Threading.pollerQueue) { [weak self] _ -> Promise<Void> in
|
||||||
strongSelf.usedSnodes.removeAll()
|
guard let strongSelf = self else { return Promise { $0.fulfill(()) } }
|
||||||
let (promise, seal) = Promise<Void>.pending()
|
strongSelf.usedSnodes.removeAll()
|
||||||
strongSelf.pollNextSnode(seal: seal)
|
let (promise, seal) = Promise<Void>.pending()
|
||||||
return promise
|
strongSelf.pollNextSnode(seal: seal)
|
||||||
}.ensure(on: DispatchQueue.main) { [weak self] in // Timers don't do well on background queues
|
return promise
|
||||||
guard let strongSelf = self, strongSelf.isPolling else { return }
|
}.ensure(on: Threading.pollerQueue) { [weak self] in // Timers don't do well on background queues
|
||||||
Timer.scheduledTimer(withTimeInterval: Poller.retryInterval, repeats: false) { _ in
|
guard let strongSelf = self, strongSelf.isPolling else { return }
|
||||||
guard let strongSelf = self else { return }
|
Timer.scheduledTimerOnMainThread(withTimeInterval: Poller.retryInterval, repeats: false) { _ in
|
||||||
strongSelf.setUpPolling()
|
guard let strongSelf = self else { return }
|
||||||
|
strongSelf.setUpPolling()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func pollNextSnode(seal: Resolver<Void>) {
|
private func pollNextSnode(seal: Resolver<Void>) {
|
||||||
|
@ -77,7 +80,9 @@ public final class Poller : NSObject {
|
||||||
SNLog("Polling \(nextSnode) failed; dropping it and switching to next snode.")
|
SNLog("Polling \(nextSnode) failed; dropping it and switching to next snode.")
|
||||||
SnodeAPI.dropSnodeFromSwarmIfNeeded(nextSnode, publicKey: userPublicKey)
|
SnodeAPI.dropSnodeFromSwarmIfNeeded(nextSnode, publicKey: userPublicKey)
|
||||||
}
|
}
|
||||||
self?.pollNextSnode(seal: seal)
|
Threading.pollerQueue.async {
|
||||||
|
self?.pollNextSnode(seal: seal)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
seal.fulfill(())
|
seal.fulfill(())
|
||||||
|
@ -87,7 +92,7 @@ public final class Poller : NSObject {
|
||||||
private func poll(_ snode: Snode, seal longTermSeal: Resolver<Void>) -> Promise<Void> {
|
private func poll(_ snode: Snode, seal longTermSeal: Resolver<Void>) -> Promise<Void> {
|
||||||
guard isPolling else { return Promise { $0.fulfill(()) } }
|
guard isPolling else { return Promise { $0.fulfill(()) } }
|
||||||
let userPublicKey = getUserHexEncodedPublicKey()
|
let userPublicKey = getUserHexEncodedPublicKey()
|
||||||
return SnodeAPI.getRawMessages(from: snode, associatedWith: userPublicKey).then(on: DispatchQueue.main) { [weak self] rawResponse -> Promise<Void> in
|
return SnodeAPI.getRawMessages(from: snode, associatedWith: userPublicKey).then(on: Threading.pollerQueue) { [weak self] rawResponse -> Promise<Void> in
|
||||||
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
|
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
|
||||||
let messages = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: userPublicKey)
|
let messages = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: userPublicKey)
|
||||||
if !messages.isEmpty {
|
if !messages.isEmpty {
|
||||||
|
@ -109,7 +114,7 @@ public final class Poller : NSObject {
|
||||||
if strongSelf.pollCount == Poller.maxPollCount {
|
if strongSelf.pollCount == Poller.maxPollCount {
|
||||||
throw Error.pollLimitReached
|
throw Error.pollLimitReached
|
||||||
} else {
|
} else {
|
||||||
return withDelay(Poller.pollInterval, completionQueue: DispatchQueue.main) {
|
return withDelay(Poller.pollInterval, completionQueue: Threading.pollerQueue) {
|
||||||
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
|
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
|
||||||
return strongSelf.poll(snode, seal: longTermSeal)
|
return strongSelf.poll(snode, seal: longTermSeal)
|
||||||
}
|
}
|
||||||
|
|
|
@ -287,6 +287,10 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
|
||||||
for (id<OWSReadTracking> readItem in newlyReadList) {
|
for (id<OWSReadTracking> readItem in newlyReadList) {
|
||||||
[readItem markAsReadAtTimestamp:readTimestamp trySendReadReceipt:trySendReadReceipt transaction:transaction];
|
[readItem markAsReadAtTimestamp:readTimestamp trySendReadReceipt:trySendReadReceipt transaction:transaction];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update unread mention.
|
||||||
|
thread.hasUnreadMentionMessage = false;
|
||||||
|
[thread saveWithTransaction:transaction];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - Settings
|
#pragma mark - Settings
|
||||||
|
|
|
@ -16,6 +16,7 @@ BOOL IsNoteToSelfEnabled(void);
|
||||||
*/
|
*/
|
||||||
@interface TSThread : TSYapDatabaseObject
|
@interface TSThread : TSYapDatabaseObject
|
||||||
|
|
||||||
|
@property (nonatomic) BOOL hasUnreadMentionMessage;
|
||||||
@property (nonatomic) BOOL isPinned;
|
@property (nonatomic) BOOL isPinned;
|
||||||
@property (nonatomic) BOOL shouldBeVisible;
|
@property (nonatomic) BOOL shouldBeVisible;
|
||||||
@property (nonatomic, readonly) NSDate *creationDate;
|
@property (nonatomic, readonly) NSDate *creationDate;
|
||||||
|
@ -69,9 +70,6 @@ BOOL IsNoteToSelfEnabled(void);
|
||||||
- (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction
|
- (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||||
NS_SWIFT_NAME(unreadMessageCount(transaction:));
|
NS_SWIFT_NAME(unreadMessageCount(transaction:));
|
||||||
|
|
||||||
- (BOOL)hasUnreadMentionMessageWithTransaction:(YapDatabaseReadTransaction *)transaction
|
|
||||||
NS_SWIFT_NAME(hasUnreadMentionMessage(transaction:));
|
|
||||||
|
|
||||||
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
|
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -261,49 +261,28 @@ BOOL IsNoteToSelfEnabled(void)
|
||||||
|
|
||||||
- (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction
|
- (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||||
{
|
{
|
||||||
__block NSUInteger count = 0;
|
|
||||||
|
|
||||||
YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName];
|
YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName];
|
||||||
[unreadMessages enumerateKeysAndObjectsInGroup:self.uniqueId
|
return [unreadMessages numberOfItemsInGroup:self.uniqueId];
|
||||||
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
|
|
||||||
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
id<OWSReadTracking> unread = (id<OWSReadTracking>)object;
|
|
||||||
if (unread.read) {
|
|
||||||
NSLog(@"Found an already read message in the * unread * messages list.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
count += 1;
|
|
||||||
}];
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)hasUnreadMentionMessageWithTransaction:(YapDatabaseReadTransaction *)transaction
|
|
||||||
{
|
|
||||||
__block BOOL hasUnreadMention = false;
|
|
||||||
|
|
||||||
YapDatabaseViewTransaction *unreadMessages = [transaction ext:TSUnreadDatabaseViewExtensionName];
|
|
||||||
[unreadMessages enumerateKeysAndObjectsInGroup:self.uniqueId
|
|
||||||
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
|
|
||||||
if (![object isKindOfClass:[TSIncomingMessage class]]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
TSIncomingMessage* unreadMessage = (TSIncomingMessage*)object;
|
|
||||||
if (unreadMessage.read) {
|
|
||||||
NSLog(@"Found an already read message in the * unread * messages list.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unreadMessage.isUserMentioned) {
|
|
||||||
hasUnreadMention = true;
|
|
||||||
*stop = YES;
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
return hasUnreadMention;
|
|
||||||
|
|
||||||
|
// FIXME: Why did we have to do as the following?
|
||||||
|
// __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)]) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// id<OWSReadTracking> unread = (id<OWSReadTracking>)object;
|
||||||
|
// if (unread.read) {
|
||||||
|
// NSLog(@"Found an already read message in the * unread * messages list.");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// count += 1;
|
||||||
|
// }];
|
||||||
|
|
||||||
|
// return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||||
|
@ -311,6 +290,10 @@ BOOL IsNoteToSelfEnabled(void)
|
||||||
for (id<OWSReadTracking> message in [self unseenMessagesWithTransaction:transaction]) {
|
for (id<OWSReadTracking> message in [self unseenMessagesWithTransaction:transaction]) {
|
||||||
[message markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] trySendReadReceipt:YES transaction:transaction];
|
[message markAsReadAtTimestamp:[NSDate ows_millisecondTimeStamp] trySendReadReceipt:YES transaction:transaction];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update unread mention.
|
||||||
|
self.hasUnreadMentionMessage = false;
|
||||||
|
[super saveWithTransaction:transaction];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (nullable TSInteraction *)lastInteractionForInboxWithTransaction:(YapDatabaseReadTransaction *)transaction
|
- (nullable TSInteraction *)lastInteractionForInboxWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||||
|
@ -376,6 +359,12 @@ BOOL IsNoteToSelfEnabled(void)
|
||||||
_lastInteractionDate = lastMessage.receivedAtDate;
|
_lastInteractionDate = lastMessage.receivedAtDate;
|
||||||
[super saveWithTransaction:transaction];
|
[super saveWithTransaction:transaction];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update unread mention if there is a new incoming message.
|
||||||
|
if ([lastMessage isKindOfClass:[TSIncomingMessage class]] && ((TSIncomingMessage *)lastMessage).isUserMentioned) {
|
||||||
|
self.hasUnreadMentionMessage = true;
|
||||||
|
[super saveWithTransaction:transaction];
|
||||||
|
}
|
||||||
|
|
||||||
if (!self.shouldBeVisible) {
|
if (!self.shouldBeVisible) {
|
||||||
self.shouldBeVisible = YES;
|
self.shouldBeVisible = YES;
|
||||||
|
|
|
@ -3,4 +3,6 @@ import Foundation
|
||||||
internal enum Threading {
|
internal enum Threading {
|
||||||
|
|
||||||
internal static let jobQueue = DispatchQueue(label: "SessionMessagingKit.jobQueue", qos: .userInitiated)
|
internal static let jobQueue = DispatchQueue(label: "SessionMessagingKit.jobQueue", qos: .userInitiated)
|
||||||
|
|
||||||
|
internal static let pollerQueue = DispatchQueue(label: "SessionMessagingKit.pollerQueue")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import SignalUtilitiesKit
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
|
public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
|
|
||||||
|
public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
|
||||||
|
guard !thread.isMuted else { return }
|
||||||
|
guard let threadID = thread.uniqueId else { return }
|
||||||
|
|
||||||
|
// If the thread is a message request and the user hasn't hidden message requests then we need
|
||||||
|
// to check if this is the only message request thread (group threads can't be message requests
|
||||||
|
// so just ignore those and if the user has hidden message requests then we want to show the
|
||||||
|
// notification regardless of how many message requests there are)
|
||||||
|
if !thread.isGroupThread() && thread.isMessageRequest() && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
||||||
|
let threads = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
||||||
|
let numMessageRequests = threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
||||||
|
|
||||||
|
// Allow this to show a notification if there are no message requests (ie. this is the first one)
|
||||||
|
guard numMessageRequests == 0 else { return }
|
||||||
|
}
|
||||||
|
else if thread.isMessageRequest() && CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
||||||
|
// If there are other interactions on this thread already then don't show the notification
|
||||||
|
if thread.numberOfInteractions() > 1 { return }
|
||||||
|
|
||||||
|
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let senderPublicKey = incomingMessage.authorId
|
||||||
|
let userPublicKey = SNGeneralUtilities.getUserPublicKey()
|
||||||
|
guard senderPublicKey != userPublicKey else {
|
||||||
|
// Ignore PNs for messages sent by the current user
|
||||||
|
// after handling the message. Otherwise the closed
|
||||||
|
// group self-send messages won't show.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = Contact.context(for: thread)
|
||||||
|
let senderName = Storage.shared.getContact(with: senderPublicKey)?.displayName(for: context) ?? senderPublicKey
|
||||||
|
|
||||||
|
var notificationTitle = senderName
|
||||||
|
if let group = thread as? TSGroupThread {
|
||||||
|
if group.isOnlyNotifyingForMentions && !incomingMessage.isUserMentioned {
|
||||||
|
// Ignore PNs if the group is set to only notify for mentions
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupName = thread.name(with: transaction)
|
||||||
|
if groupName.count < 1 {
|
||||||
|
groupName = MessageStrings.newGroupDefaultTitle
|
||||||
|
}
|
||||||
|
notificationTitle = String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, groupName)
|
||||||
|
}
|
||||||
|
|
||||||
|
let snippet = incomingMessage.previewText(with: transaction).filterForDisplay?.replacingMentions(for: threadID, using: transaction)
|
||||||
|
?? "APN_Message".localized()
|
||||||
|
|
||||||
|
var userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ]
|
||||||
|
userInfo[NotificationServiceExtension.threadIdKey] = threadID
|
||||||
|
|
||||||
|
let notificationContent = UNMutableNotificationContent()
|
||||||
|
notificationContent.userInfo = userInfo
|
||||||
|
notificationContent.sound = OWSSounds.notificationSound(for: thread).notificationSound(isQuiet: false)
|
||||||
|
|
||||||
|
// Badge Number
|
||||||
|
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
|
||||||
|
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
||||||
|
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
||||||
|
|
||||||
|
// Title & body
|
||||||
|
let notificationsPreference = Environment.shared.preferences!.notificationPreviewType()
|
||||||
|
switch notificationsPreference {
|
||||||
|
case .namePreview:
|
||||||
|
notificationContent.title = notificationTitle
|
||||||
|
notificationContent.body = snippet
|
||||||
|
case .nameNoPreview:
|
||||||
|
notificationContent.title = notificationTitle
|
||||||
|
notificationContent.body = NotificationStrings.incomingMessageBody
|
||||||
|
case .noNameNoPreview:
|
||||||
|
notificationContent.title = "Session"
|
||||||
|
notificationContent.body = NotificationStrings.incomingMessageBody
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a message request then overwrite the body to be something generic (only show a notification
|
||||||
|
// when receiving a new message request if there aren't any others or the user had hidden them)
|
||||||
|
if thread.isMessageRequest() {
|
||||||
|
notificationContent.body = "MESSAGE_REQUESTS_NOTIFICATION".localized()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add request
|
||||||
|
let identifier = incomingMessage.notificationIdentifier ?? UUID().uuidString
|
||||||
|
let request = UNNotificationRequest(identifier: identifier, content: notificationContent, trigger: nil)
|
||||||
|
SNLog("Add remote notification request")
|
||||||
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func cancelNotification(_ identifier: String) {
|
||||||
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
|
notificationCenter.removePendingNotificationRequests(withIdentifiers: [ identifier ])
|
||||||
|
notificationCenter.removeDeliveredNotifications(withIdentifiers: [ identifier ])
|
||||||
|
}
|
||||||
|
|
||||||
|
public func clearAllNotifications() {
|
||||||
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
|
notificationCenter.removeAllPendingNotificationRequests()
|
||||||
|
notificationCenter.removeAllDeliveredNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
|
||||||
|
func replacingMentions(for threadID: String, using transaction: YapDatabaseReadTransaction) -> String {
|
||||||
|
var result = self
|
||||||
|
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]{66}", options: [])
|
||||||
|
var mentions: [(range: NSRange, publicKey: String)] = []
|
||||||
|
var m0 = regex.firstMatch(in: result, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: result.utf16.count))
|
||||||
|
while let m1 = m0 {
|
||||||
|
let publicKey = String((result as NSString).substring(with: m1.range).dropFirst()) // Drop the @
|
||||||
|
var matchEnd = m1.range.location + m1.range.length
|
||||||
|
let displayName = Storage.shared.getContact(with: publicKey, using: transaction)?.displayName(for: .regular)
|
||||||
|
if let displayName = displayName {
|
||||||
|
result = (result as NSString).replacingCharacters(in: m1.range, with: "@\(displayName)")
|
||||||
|
mentions.append((range: NSRange(location: m1.range.location, length: displayName.utf16.count + 1), publicKey: publicKey)) // + 1 to include the @
|
||||||
|
matchEnd = m1.range.location + displayName.utf16.count
|
||||||
|
}
|
||||||
|
m0 = regex.firstMatch(in: result, options: .withoutAnchoringBounds, range: NSRange(location: matchEnd, length: result.utf16.count - matchEnd))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import SessionMessagingKit
|
import SessionMessagingKit
|
||||||
import SignalUtilitiesKit
|
import SignalUtilitiesKit
|
||||||
|
import PromiseKit
|
||||||
|
|
||||||
public final class NotificationServiceExtension : UNNotificationServiceExtension {
|
public final class NotificationServiceExtension : UNNotificationServiceExtension {
|
||||||
private var didPerformSetup = false
|
private var didPerformSetup = false
|
||||||
|
@ -8,13 +9,14 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
|
||||||
private var contentHandler: ((UNNotificationContent) -> Void)?
|
private var contentHandler: ((UNNotificationContent) -> Void)?
|
||||||
private var notificationContent: UNMutableNotificationContent?
|
private var notificationContent: UNMutableNotificationContent?
|
||||||
|
|
||||||
private static let isFromRemoteKey = "remote"
|
public static let isFromRemoteKey = "remote"
|
||||||
private static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId"
|
public static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId"
|
||||||
|
|
||||||
|
// MARK: Did receive a remote push notification request
|
||||||
|
|
||||||
override public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
override public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||||
self.contentHandler = contentHandler
|
self.contentHandler = contentHandler
|
||||||
self.notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent
|
self.notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent
|
||||||
let userPublicKey = SNGeneralUtilities.getUserPublicKey()
|
|
||||||
|
|
||||||
// Abort if the main app is running
|
// Abort if the main app is running
|
||||||
var isMainAppAndActive = false
|
var isMainAppAndActive = false
|
||||||
|
@ -28,6 +30,12 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
|
||||||
|
|
||||||
// Handle the push notification
|
// Handle the push notification
|
||||||
AppReadiness.runNowOrWhenAppDidBecomeReady {
|
AppReadiness.runNowOrWhenAppDidBecomeReady {
|
||||||
|
let openGorupPollingPromises = self.pollForOpenGroups()
|
||||||
|
defer {
|
||||||
|
when(resolved: openGorupPollingPromises).done { _ in
|
||||||
|
self.completeSilenty()
|
||||||
|
}
|
||||||
|
}
|
||||||
let notificationContent = self.notificationContent!
|
let notificationContent = self.notificationContent!
|
||||||
guard let base64EncodedData = notificationContent.userInfo["ENCRYPTED_DATA"] as! String?, let data = Data(base64Encoded: base64EncodedData),
|
guard let base64EncodedData = notificationContent.userInfo["ENCRYPTED_DATA"] as! String?, let data = Data(base64Encoded: base64EncodedData),
|
||||||
let envelope = try? MessageWrapper.unwrap(data: data), let envelopeAsData = try? envelope.serializedData() else {
|
let envelope = try? MessageWrapper.unwrap(data: data), let envelopeAsData = try? envelope.serializedData() else {
|
||||||
|
@ -36,143 +44,40 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
|
||||||
Storage.write { transaction in // Intentionally capture self
|
Storage.write { transaction in // Intentionally capture self
|
||||||
do {
|
do {
|
||||||
let (message, proto) = try MessageReceiver.parse(envelopeAsData, openGroupMessageServerID: nil, using: transaction)
|
let (message, proto) = try MessageReceiver.parse(envelopeAsData, openGroupMessageServerID: nil, using: transaction)
|
||||||
let senderPublicKey = message.sender!
|
|
||||||
var senderDisplayName = Storage.shared.getContact(with: senderPublicKey)?.displayName(for: .regular) ?? senderPublicKey
|
|
||||||
let snippet: String
|
|
||||||
var userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ]
|
|
||||||
var isMessageRequest: Bool = false
|
|
||||||
|
|
||||||
switch message {
|
switch message {
|
||||||
case let visibleMessage as VisibleMessage:
|
case let visibleMessage as VisibleMessage:
|
||||||
let tsIncomingMessageID = try MessageReceiver.handleVisibleMessage(visibleMessage, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: false, using: transaction)
|
let tsMessageID = try MessageReceiver.handleVisibleMessage(visibleMessage, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: false, using: transaction)
|
||||||
|
|
||||||
guard let tsMessage = TSMessage.fetch(uniqueId: tsIncomingMessageID, transaction: transaction) else {
|
// Remove the notificaitons if there is an outgoing messages from a linked device
|
||||||
return self.completeSilenty()
|
if let tsMessage = TSMessage.fetch(uniqueId: tsMessageID, transaction: transaction), tsMessage.isKind(of: TSOutgoingMessage.self), let threadID = tsMessage.thread(with: transaction).uniqueId {
|
||||||
|
let semaphore = DispatchSemaphore(value: 0)
|
||||||
|
let center = UNUserNotificationCenter.current()
|
||||||
|
center.getDeliveredNotifications { notifications in
|
||||||
|
let matchingNotifications = notifications.filter({ $0.request.content.userInfo[NotificationServiceExtension.threadIdKey] as? String == threadID})
|
||||||
|
center.removeDeliveredNotifications(withIdentifiers: matchingNotifications.map({ $0.request.identifier }))
|
||||||
|
// Hack: removeDeliveredNotifications seems to be async,need to wait for some time before the delivered notifications can be removed.
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { semaphore.signal() }
|
||||||
}
|
}
|
||||||
|
semaphore.wait()
|
||||||
let thread = tsMessage.thread(with: transaction)
|
}
|
||||||
let threadID = thread.uniqueId!
|
|
||||||
userInfo[NotificationServiceExtension.threadIdKey] = threadID
|
case let unsendRequest as UnsendRequest:
|
||||||
snippet = tsMessage.previewText(with: transaction).filterForDisplay?.replacingMentions(for: threadID, using: transaction)
|
MessageReceiver.handleUnsendRequest(unsendRequest, using: transaction)
|
||||||
?? "You've got a new message"
|
case let closedGroupControlMessage as ClosedGroupControlMessage:
|
||||||
|
MessageReceiver.handleClosedGroupControlMessage(closedGroupControlMessage, using: transaction)
|
||||||
if let tsIncomingMessage = tsMessage as? TSIncomingMessage {
|
default: break
|
||||||
// Ignore PNs if the thread is muted
|
|
||||||
if thread.isMuted { return self.completeSilenty() }
|
|
||||||
if let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction), let group = thread as? TSGroupThread,
|
|
||||||
group.groupModel.groupType == .closedGroup { // Should always be true because we don't get PNs for open groups
|
|
||||||
senderDisplayName = String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderDisplayName, group.groupModel.groupName ?? MessageStrings.newGroupDefaultTitle)
|
|
||||||
if group.isOnlyNotifyingForMentions && !tsIncomingMessage.isUserMentioned {
|
|
||||||
// Ignore PNs if the group is set to only notify for mentions
|
|
||||||
return self.completeSilenty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the thread is a message request and the user hasn't hidden message requests then we need
|
|
||||||
// to check if this is the only message request thread (group threads can't be message requests
|
|
||||||
// so just ignore those and if the user has hidden message requests then we want to show the
|
|
||||||
// notification regardless of how many message requests there are)
|
|
||||||
if !thread.isGroupThread() && thread.isMessageRequest() && !CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
|
||||||
let dbConnection: YapDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
|
|
||||||
dbConnection.objectCacheLimit = 2
|
|
||||||
dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
|
|
||||||
let threads: YapDatabaseViewMappings = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup ], view: TSThreadDatabaseViewExtensionName)
|
|
||||||
dbConnection.read { transaction in
|
|
||||||
threads.update(with: transaction) // Perform the initial update
|
|
||||||
}
|
|
||||||
|
|
||||||
let numMessageRequests = threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
|
||||||
dbConnection.endLongLivedReadTransaction()
|
|
||||||
|
|
||||||
// Allow this to show a notification if there are no message requests (ie. this is the first one)
|
|
||||||
guard numMessageRequests == 0 else { return self.completeSilenty() }
|
|
||||||
}
|
|
||||||
else if thread.isMessageRequest() && CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] {
|
|
||||||
// If there are other interactions on this thread already then don't show the notification
|
|
||||||
if thread.numberOfInteractions() > 1 { return }
|
|
||||||
|
|
||||||
CurrentAppContext().appUserDefaults()[.hasHiddenMessageRequests] = false
|
|
||||||
}
|
|
||||||
|
|
||||||
isMessageRequest = thread.isMessageRequest()
|
|
||||||
|
|
||||||
// Store the notification ID for unsend requests to later cancel this notification
|
|
||||||
tsIncomingMessage.setNotificationIdentifier(request.identifier, transaction: transaction)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let semaphore = DispatchSemaphore(value: 0)
|
|
||||||
let center = UNUserNotificationCenter.current()
|
|
||||||
center.getDeliveredNotifications { notifications in
|
|
||||||
let matchingNotifications = notifications.filter({ $0.request.content.userInfo[NotificationServiceExtension.threadIdKey] as? String == threadID})
|
|
||||||
center.removeDeliveredNotifications(withIdentifiers: matchingNotifications.map({ $0.request.identifier }))
|
|
||||||
// Hack: removeDeliveredNotifications seems to be async,need to wait for some time before the delivered notifications can be removed.
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { semaphore.signal() }
|
|
||||||
}
|
|
||||||
semaphore.wait()
|
|
||||||
}
|
|
||||||
notificationContent.sound = OWSSounds.notificationSound(for: thread).notificationSound(isQuiet: false)
|
|
||||||
|
|
||||||
case let unsendRequest as UnsendRequest:
|
|
||||||
MessageReceiver.handleUnsendRequest(unsendRequest, using: transaction)
|
|
||||||
return self.completeSilenty()
|
|
||||||
|
|
||||||
case let closedGroupControlMessage as ClosedGroupControlMessage:
|
|
||||||
// TODO: We could consider actually handling the update here. Not sure if there's enough time though, seeing as though
|
|
||||||
// in some cases we need to send messages (e.g. our sender key) to a number of other users.
|
|
||||||
switch closedGroupControlMessage.kind {
|
|
||||||
case .new(_, let name, _, _, _, _): snippet = "\(senderDisplayName) added you to \(name)"
|
|
||||||
default: return self.completeSilenty()
|
|
||||||
}
|
|
||||||
|
|
||||||
default: return self.completeSilenty()
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
if (senderPublicKey == userPublicKey) {
|
|
||||||
// Ignore PNs for messages sent by the current user
|
|
||||||
// after handling the message. Otherwise the closed
|
|
||||||
// group self-send messages won't show.
|
|
||||||
return self.completeSilenty()
|
|
||||||
}
|
|
||||||
notificationContent.userInfo = userInfo
|
|
||||||
notificationContent.badge = 1
|
|
||||||
|
|
||||||
let notificationsPreference = Environment.shared.preferences!.notificationPreviewType()
|
|
||||||
|
|
||||||
switch notificationsPreference {
|
|
||||||
case .namePreview:
|
|
||||||
notificationContent.title = senderDisplayName
|
|
||||||
notificationContent.body = snippet
|
|
||||||
|
|
||||||
case .nameNoPreview:
|
|
||||||
notificationContent.title = senderDisplayName
|
|
||||||
notificationContent.body = NotificationStrings.incomingMessageBody
|
|
||||||
|
|
||||||
case .noNameNoPreview:
|
|
||||||
notificationContent.title = "Session"
|
|
||||||
notificationContent.body = NotificationStrings.incomingMessageBody
|
|
||||||
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a message request then overwrite the body to be something generic (only show a notification
|
|
||||||
// when receiving a new message request if there aren't any others or the user had hidden them)
|
|
||||||
if isMessageRequest {
|
|
||||||
notificationContent.title = "Session"
|
|
||||||
notificationContent.body = NSLocalizedString("MESSAGE_REQUESTS_NOTIFICATION", comment: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
self.handleSuccess(for: notificationContent)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
if let error = error as? MessageReceiver.Error, error.isRetryable {
|
if let error = error as? MessageReceiver.Error, error.isRetryable {
|
||||||
self.handleFailure(for: notificationContent)
|
self.handleFailure(for: notificationContent)
|
||||||
}
|
}
|
||||||
self.completeSilenty()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Setup
|
||||||
|
|
||||||
private func setUpIfNecessary(completion: @escaping () -> Void) {
|
private func setUpIfNecessary(completion: @escaping () -> Void) {
|
||||||
AssertIsOnMainThread()
|
AssertIsOnMainThread()
|
||||||
|
|
||||||
|
@ -199,7 +104,7 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
|
||||||
|
|
||||||
AppSetup.setupEnvironment(
|
AppSetup.setupEnvironment(
|
||||||
appSpecificSingletonBlock: {
|
appSpecificSingletonBlock: {
|
||||||
SSKEnvironment.shared.notificationsManager = NoopNotificationsManager()
|
SSKEnvironment.shared.notificationsManager = NSENotificationPresenter()
|
||||||
},
|
},
|
||||||
migrationCompletion: { [weak self] in
|
migrationCompletion: { [weak self] in
|
||||||
self?.versionMigrationsDidComplete()
|
self?.versionMigrationsDidComplete()
|
||||||
|
@ -210,18 +115,6 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(storageIsReady), name: .StorageIsReady, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(storageIsReady), name: .StorageIsReady, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func serviceExtensionTimeWillExpire() {
|
|
||||||
// Called just before the extension will be terminated by the system.
|
|
||||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
|
||||||
let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ]
|
|
||||||
let notificationContent = self.notificationContent!
|
|
||||||
notificationContent.userInfo = userInfo
|
|
||||||
notificationContent.badge = 1
|
|
||||||
notificationContent.title = "Session"
|
|
||||||
notificationContent.body = "You've got a new message"
|
|
||||||
handleSuccess(for: notificationContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
private func versionMigrationsDidComplete() {
|
private func versionMigrationsDidComplete() {
|
||||||
AssertIsOnMainThread()
|
AssertIsOnMainThread()
|
||||||
|
@ -254,8 +147,17 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
|
||||||
AppReadiness.setAppIsReady()
|
AppReadiness.setAppIsReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Handle completion
|
||||||
|
|
||||||
|
override public func serviceExtensionTimeWillExpire() {
|
||||||
|
// Called just before the extension will be terminated by the system.
|
||||||
|
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||||
|
completeSilenty()
|
||||||
|
}
|
||||||
|
|
||||||
private func completeSilenty() {
|
private func completeSilenty() {
|
||||||
contentHandler!(.init())
|
SNLog("Complete silenty")
|
||||||
|
self.contentHandler!(.init())
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleSuccess(for content: UNMutableNotificationContent) {
|
private func handleSuccess(for content: UNMutableNotificationContent) {
|
||||||
|
@ -269,30 +171,20 @@ public final class NotificationServiceExtension : UNNotificationServiceExtension
|
||||||
content.userInfo = userInfo
|
content.userInfo = userInfo
|
||||||
contentHandler!(content)
|
contentHandler!(content)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private extension String {
|
|
||||||
|
|
||||||
func replacingMentions(for threadID: String, using transaction: YapDatabaseReadWriteTransaction) -> String {
|
// MARK: Poll for open groups
|
||||||
MentionsManager.populateUserPublicKeyCacheIfNeeded(for: threadID, in: transaction)
|
private func pollForOpenGroups() -> [Promise<Void>] {
|
||||||
var result = self
|
var promises: [Promise<Void>] = []
|
||||||
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]{66}", options: [])
|
let servers = Set(Storage.shared.getAllV2OpenGroups().values.map { $0.server })
|
||||||
let knownPublicKeys = MentionsManager.userPublicKeyCache[threadID] ?? []
|
servers.forEach { server in
|
||||||
var mentions: [(range: NSRange, publicKey: String)] = []
|
let poller = OpenGroupPollerV2(for: server)
|
||||||
var m0 = regex.firstMatch(in: result, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: result.utf16.count))
|
let promise = poller.poll().timeout(seconds: 20, timeoutError: NotificationServiceError.timeout)
|
||||||
while let m1 = m0 {
|
promises.append(promise)
|
||||||
let publicKey = String((result as NSString).substring(with: m1.range).dropFirst()) // Drop the @
|
|
||||||
var matchEnd = m1.range.location + m1.range.length
|
|
||||||
if knownPublicKeys.contains(publicKey) {
|
|
||||||
let displayName = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular)
|
|
||||||
if let displayName = displayName {
|
|
||||||
result = (result as NSString).replacingCharacters(in: m1.range, with: "@\(displayName)")
|
|
||||||
mentions.append((range: NSRange(location: m1.range.location, length: displayName.utf16.count + 1), publicKey: publicKey)) // + 1 to include the @
|
|
||||||
matchEnd = m1.range.location + displayName.utf16.count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m0 = regex.firstMatch(in: result, options: .withoutAnchoringBounds, range: NSRange(location: matchEnd, length: result.utf16.count - matchEnd))
|
|
||||||
}
|
}
|
||||||
return result
|
return promises
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum NotificationServiceError: Error {
|
||||||
|
case timeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,21 @@ public final class SearchBar : UISearchBar {
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
setUpStyle()
|
setUpSessionStyle()
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init?(coder: NSCoder) {
|
public required init?(coder: NSCoder) {
|
||||||
super.init(coder: coder)
|
super.init(coder: coder)
|
||||||
setUpStyle()
|
setUpSessionStyle()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private func setUpStyle() {
|
|
||||||
|
public extension UISearchBar {
|
||||||
|
|
||||||
|
func setUpSessionStyle() {
|
||||||
searchBarStyle = .minimal // Hide the border around the search bar
|
searchBarStyle = .minimal // Hide the border around the search bar
|
||||||
barStyle = .black // Use Apple's black design as a base
|
barStyle = .black // Use Apple's black design as a base
|
||||||
tintColor = Colors.accent // The cursor color
|
tintColor = Colors.text // The cursor color
|
||||||
let searchImage = #imageLiteral(resourceName: "searchbar_search").withTint(Colors.searchBarPlaceholder)!
|
let searchImage = #imageLiteral(resourceName: "searchbar_search").withTint(Colors.searchBarPlaceholder)!
|
||||||
setImage(searchImage, for: .search, state: .normal)
|
setImage(searchImage, for: .search, state: .normal)
|
||||||
let clearImage = #imageLiteral(resourceName: "searchbar_clear").withTint(Colors.searchBarPlaceholder)!
|
let clearImage = #imageLiteral(resourceName: "searchbar_clear").withTint(Colors.searchBarPlaceholder)!
|
||||||
|
|
13
SessionUtilitiesKit/General/Timer+MainThread.swift
Normal file
13
SessionUtilitiesKit/General/Timer+MainThread.swift
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Timer {
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
public static func scheduledTimerOnMainThread(withTimeInterval timeInterval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer {
|
||||||
|
let timer = Timer(timeInterval: timeInterval, repeats: repeats, block: block)
|
||||||
|
RunLoop.main.add(timer, forMode: .common)
|
||||||
|
return timer
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,13 @@ import PromiseKit
|
||||||
|
|
||||||
/// Delay the execution of the promise constructed in `body` by `delay` seconds.
|
/// Delay the execution of the promise constructed in `body` by `delay` seconds.
|
||||||
public func withDelay<T>(_ delay: TimeInterval, completionQueue: DispatchQueue, body: @escaping () -> Promise<T>) -> Promise<T> {
|
public func withDelay<T>(_ delay: TimeInterval, completionQueue: DispatchQueue, body: @escaping () -> Promise<T>) -> Promise<T> {
|
||||||
#if DEBUG
|
|
||||||
assert(Thread.current.isMainThread) // Timers don't do well on background queues
|
|
||||||
#endif
|
|
||||||
let (promise, seal) = Promise<T>.pending()
|
let (promise, seal) = Promise<T>.pending()
|
||||||
Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { _ in
|
Timer.scheduledTimerOnMainThread(withTimeInterval: delay, repeats: false) { _ in
|
||||||
body().done(on: completionQueue) { seal.fulfill($0) }.catch(on: completionQueue) { seal.reject($0) }
|
body().done(on: completionQueue) {
|
||||||
|
seal.fulfill($0)
|
||||||
|
}.catch(on: completionQueue) {
|
||||||
|
seal.reject($0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
19
SessionUtilitiesKit/PromiseKit/Promise+Timeout.swift
Normal file
19
SessionUtilitiesKit/PromiseKit/Promise+Timeout.swift
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import PromiseKit
|
||||||
|
|
||||||
|
public extension Promise {
|
||||||
|
|
||||||
|
func timeout(seconds: TimeInterval, timeoutError: Error) -> Promise<T> {
|
||||||
|
return Promise<T> { seal in
|
||||||
|
after(seconds: seconds).done {
|
||||||
|
seal.reject(timeoutError)
|
||||||
|
}
|
||||||
|
self.done { result in
|
||||||
|
seal.fulfill(result)
|
||||||
|
}.catch { err in
|
||||||
|
seal.reject(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,13 @@ public class MessageRequestsMigration : OWSDatabaseMigration {
|
||||||
threads.append(thread)
|
threads.append(thread)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let user = Storage.shared.getUser() {
|
||||||
|
user.isApproved = true
|
||||||
|
user.didApproveMe = true
|
||||||
|
contacts.insert(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Storage.write(with: { transaction in
|
Storage.write(with: { transaction in
|
||||||
contacts.forEach { contact in
|
contacts.forEach { contact in
|
||||||
Storage.shared.setContact(contact, using: transaction)
|
Storage.shared.setContact(contact, using: transaction)
|
||||||
|
|
|
@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
- (NSArray<OWSDatabaseMigration *> *)allMigrations
|
- (NSArray<OWSDatabaseMigration *> *)allMigrations
|
||||||
{
|
{
|
||||||
return @[
|
return @[
|
||||||
|
[SNUnreadMentionMigration new],
|
||||||
[SNMessageRequestsMigration new],
|
[SNMessageRequestsMigration new],
|
||||||
[SNContactsMigration new]
|
[SNContactsMigration new]
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
@objc(SNUnreadMentionMigration)
|
||||||
|
public class UnreadMentionMigration : OWSDatabaseMigration {
|
||||||
|
|
||||||
|
@objc
|
||||||
|
class func migrationId() -> String {
|
||||||
|
return "003" // leave "002" for message request migration
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) {
|
||||||
|
self.doMigrationAsync(completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) {
|
||||||
|
var threads: [TSThread] = []
|
||||||
|
Storage.read { transaction in
|
||||||
|
TSThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
||||||
|
guard let thread = object as? TSThread, let threadID = thread.uniqueId else { return }
|
||||||
|
let unreadMessages = transaction.ext(TSUnreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
||||||
|
unreadMessages.enumerateKeysAndObjects(inGroup: threadID) { collection, key, object, index, stop in
|
||||||
|
guard let unreadMessage = object as? TSIncomingMessage else { return }
|
||||||
|
if unreadMessage.wasRead { return }
|
||||||
|
if unreadMessage.isUserMentioned {
|
||||||
|
thread.hasUnreadMentionMessage = true
|
||||||
|
stop.pointee = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
threads.append(thread)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Storage.write(with: { transaction in
|
||||||
|
threads.forEach { thread in
|
||||||
|
thread.save(with: transaction)
|
||||||
|
}
|
||||||
|
self.save(with: transaction) // Intentionally capture self
|
||||||
|
}, completion: {
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,18 +22,16 @@ public struct ConversationSortKey: Comparable {
|
||||||
public class ConversationSearchResult<SortKey>: Comparable where SortKey: Comparable {
|
public class ConversationSearchResult<SortKey>: Comparable where SortKey: Comparable {
|
||||||
public let thread: ThreadViewModel
|
public let thread: ThreadViewModel
|
||||||
|
|
||||||
public let messageId: String?
|
public let message: TSMessage?
|
||||||
public let messageDate: Date?
|
|
||||||
|
|
||||||
public let snippet: String?
|
public let snippet: String?
|
||||||
|
|
||||||
private let sortKey: SortKey
|
private let sortKey: SortKey
|
||||||
|
|
||||||
init(thread: ThreadViewModel, sortKey: SortKey, messageId: String? = nil, messageDate: Date? = nil, snippet: String? = nil) {
|
init(thread: ThreadViewModel, sortKey: SortKey, message: TSMessage? = nil, snippet: String? = nil) {
|
||||||
self.thread = thread
|
self.thread = thread
|
||||||
self.sortKey = sortKey
|
self.sortKey = sortKey
|
||||||
self.messageId = messageId
|
self.message = message
|
||||||
self.messageDate = messageDate
|
|
||||||
self.snippet = snippet
|
self.snippet = snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +45,7 @@ public class ConversationSearchResult<SortKey>: Comparable where SortKey: Compar
|
||||||
|
|
||||||
public static func == (lhs: ConversationSearchResult, rhs: ConversationSearchResult) -> Bool {
|
public static func == (lhs: ConversationSearchResult, rhs: ConversationSearchResult) -> Bool {
|
||||||
return lhs.thread.threadRecord.uniqueId == rhs.thread.threadRecord.uniqueId &&
|
return lhs.thread.threadRecord.uniqueId == rhs.thread.threadRecord.uniqueId &&
|
||||||
lhs.messageId == rhs.messageId
|
lhs.message?.uniqueId == rhs.message?.uniqueId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,8 +268,7 @@ public class FullTextSearcher: NSObject {
|
||||||
let sortKey = message.sortId
|
let sortKey = message.sortId
|
||||||
let searchResult = ConversationSearchResult(thread: threadViewModel,
|
let searchResult = ConversationSearchResult(thread: threadViewModel,
|
||||||
sortKey: sortKey,
|
sortKey: sortKey,
|
||||||
messageId: message.uniqueId,
|
message: message,
|
||||||
messageDate: NSDate.ows_date(withMillisecondsSince1970: message.timestamp),
|
|
||||||
snippet: snippet)
|
snippet: snippet)
|
||||||
|
|
||||||
messages.append(searchResult)
|
messages.append(searchResult)
|
||||||
|
|
|
@ -91,6 +91,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
NSLog(@"Found an already read message in the * unread * messages list.");
|
NSLog(@"Found an already read message in the * unread * messages list.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// We have to filter those unread messages for groups that only notifiy for mentions
|
||||||
if ([object isKindOfClass:TSIncomingMessage.class] && isGroupThread) {
|
if ([object isKindOfClass:TSIncomingMessage.class] && isGroupThread) {
|
||||||
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)object;
|
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)object;
|
||||||
if (((TSGroupThread *)thread).isOnlyNotifyingForMentions && !incomingMessage.isUserMentioned) {
|
if (((TSGroupThread *)thread).isOnlyNotifyingForMentions && !incomingMessage.isUserMentioned) {
|
||||||
|
@ -103,13 +104,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
}];
|
}];
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
|
|
||||||
__block NSUInteger numberOfItems;
|
|
||||||
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
||||||
numberOfItems = [[transaction ext:TSUnreadDatabaseViewExtensionName] numberOfItemsInAllGroups];
|
|
||||||
}];
|
|
||||||
|
|
||||||
return numberOfItems;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSUInteger)unreadMessageRequestCount {
|
- (NSUInteger)unreadMessageRequestCount {
|
||||||
|
@ -124,20 +118,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
// Only increase the count for message requests
|
// Only increase the count for message requests
|
||||||
if (![thread isMessageRequestUsingTransaction:transaction]) { continue; }
|
if (![thread isMessageRequestUsingTransaction:transaction]) { continue; }
|
||||||
|
if ([unreadMessages numberOfItemsInGroup:groupID] > 0) {
|
||||||
[unreadMessages enumerateKeysAndObjectsInGroup:groupID
|
|
||||||
usingBlock:^(NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
|
|
||||||
if (![object conformsToProtocol:@protocol(OWSReadTracking)]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
id<OWSReadTracking> unread = (id<OWSReadTracking>)object;
|
|
||||||
if (unread.read) {
|
|
||||||
NSLog(@"Found an already read message in the * unread * messages list.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
count += 1;
|
count += 1;
|
||||||
*stop = YES;
|
}
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
|
|
@ -91,12 +91,15 @@ extension MessageSender {
|
||||||
let errors = results.compactMap { result -> Swift.Error? in
|
let errors = results.compactMap { result -> Swift.Error? in
|
||||||
if case .rejected(let error) = result { return error } else { return nil }
|
if case .rejected(let error) = result { return error } else { return nil }
|
||||||
}
|
}
|
||||||
if let error = errors.first { seal.reject(error) }
|
if let error = errors.first {
|
||||||
Storage.write{ transaction in
|
seal.reject(error)
|
||||||
sendNonDurably(message, in: thread, using: transaction).done {
|
} else {
|
||||||
seal.fulfill(())
|
Storage.write{ transaction in
|
||||||
}.catch { error in
|
sendNonDurably(message, in: thread, using: transaction).done {
|
||||||
seal.reject(error)
|
seal.fulfill(())
|
||||||
|
}.catch { error in
|
||||||
|
seal.reject(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return promise
|
return promise
|
||||||
|
|
|
@ -52,7 +52,7 @@ public class ThreadViewModel: NSObject {
|
||||||
|
|
||||||
self.unreadCount = thread.unreadMessageCount(transaction: transaction)
|
self.unreadCount = thread.unreadMessageCount(transaction: transaction)
|
||||||
self.hasUnreadMessages = unreadCount > 0
|
self.hasUnreadMessages = unreadCount > 0
|
||||||
self.hasUnreadMentions = thread.hasUnreadMentionMessage(transaction: transaction)
|
self.hasUnreadMentions = thread.hasUnreadMentionMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
|
|
|
@ -82,7 +82,6 @@ public final class ProfilePictureView : UIView {
|
||||||
update()
|
update()
|
||||||
} else { // A one-to-one chat
|
} else { // A one-to-one chat
|
||||||
let thread = thread as! TSContactThread
|
let thread = thread as! TSContactThread
|
||||||
hasTappableProfilePicture = OWSProfileManager.shared().profileAvatar(forRecipientId: thread.contactSessionID()) != nil
|
|
||||||
update(for: thread.contactSessionID())
|
update(for: thread.contactSessionID())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,8 +91,10 @@ public final class ProfilePictureView : UIView {
|
||||||
func getProfilePicture(of size: CGFloat, for publicKey: String) -> UIImage? {
|
func getProfilePicture(of size: CGFloat, for publicKey: String) -> UIImage? {
|
||||||
guard !publicKey.isEmpty else { return nil }
|
guard !publicKey.isEmpty else { return nil }
|
||||||
if let profilePicture = OWSProfileManager.shared().profileAvatar(forRecipientId: publicKey) {
|
if let profilePicture = OWSProfileManager.shared().profileAvatar(forRecipientId: publicKey) {
|
||||||
|
hasTappableProfilePicture = true
|
||||||
return profilePicture
|
return profilePicture
|
||||||
} else {
|
} else {
|
||||||
|
hasTappableProfilePicture = false
|
||||||
// TODO: Pass in context?
|
// TODO: Pass in context?
|
||||||
let displayName = Storage.shared.getContact(with: publicKey)?.name ?? publicKey
|
let displayName = Storage.shared.getContact(with: publicKey)?.name ?? publicKey
|
||||||
return Identicon.generatePlaceholderIcon(seed: publicKey, text: displayName, size: size)
|
return Identicon.generatePlaceholderIcon(seed: publicKey, text: displayName, size: size)
|
||||||
|
|
Loading…
Reference in a new issue