Merge remote-tracking branch 'upstream/dev' into feature/theming
# Conflicts: # Session.xcodeproj/project.pbxproj # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewModel.swift # Session/Conversations/Message Cells/VisibleMessageCell.swift # Session/Home/HomeVC.swift # Session/Home/Message Requests/MessageRequestsViewController.swift # Session/Media Viewing & Editing/MediaTileViewController.swift # Session/Meta/AppDelegate.swift # Session/Meta/Translations/de.lproj/Localizable.strings # Session/Meta/Translations/en.lproj/Localizable.strings # Session/Meta/Translations/es.lproj/Localizable.strings # Session/Meta/Translations/fa.lproj/Localizable.strings # Session/Meta/Translations/fi.lproj/Localizable.strings # Session/Meta/Translations/fr.lproj/Localizable.strings # Session/Meta/Translations/hi.lproj/Localizable.strings # Session/Meta/Translations/hr.lproj/Localizable.strings # Session/Meta/Translations/id-ID.lproj/Localizable.strings # Session/Meta/Translations/it.lproj/Localizable.strings # Session/Meta/Translations/ja.lproj/Localizable.strings # Session/Meta/Translations/nl.lproj/Localizable.strings # Session/Meta/Translations/pl.lproj/Localizable.strings # Session/Meta/Translations/pt_BR.lproj/Localizable.strings # Session/Meta/Translations/ru.lproj/Localizable.strings # Session/Meta/Translations/si.lproj/Localizable.strings # Session/Meta/Translations/sk.lproj/Localizable.strings # Session/Meta/Translations/sv.lproj/Localizable.strings # Session/Meta/Translations/th.lproj/Localizable.strings # Session/Meta/Translations/vi-VN.lproj/Localizable.strings # Session/Meta/Translations/zh-Hant.lproj/Localizable.strings # Session/Meta/Translations/zh_CN.lproj/Localizable.strings # Session/Onboarding/LandingVC.swift # SessionMessagingKitTests/_TestUtilities/MockGeneralCache.swift
This commit is contained in:
commit
1bc6b0bdba
|
@ -23,6 +23,7 @@ enum RemoteModel {
|
|||
let sortOrder: UInt
|
||||
let category: EmojiCategory
|
||||
let skinVariations: [String: SkinVariation]?
|
||||
let shortNames: [String]?
|
||||
}
|
||||
|
||||
struct SkinVariation: Codable {
|
||||
|
@ -64,6 +65,7 @@ struct EmojiModel {
|
|||
let category: RemoteModel.EmojiCategory
|
||||
let rawName: String
|
||||
let enumName: String
|
||||
var shortNames: Set<String>
|
||||
let variants: [Emoji]
|
||||
var baseEmoji: Character { variants[0].base }
|
||||
|
||||
|
@ -91,7 +93,10 @@ struct EmojiModel {
|
|||
category = remoteItem.category
|
||||
rawName = remoteItem.name
|
||||
enumName = Self.parseEnumNameFromRemoteItem(remoteItem)
|
||||
|
||||
shortNames = Set((remoteItem.shortNames ?? []))
|
||||
shortNames.insert(rawName.lowercased())
|
||||
shortNames.insert(enumName.lowercased())
|
||||
|
||||
let baseEmojiChar = try Self.codePointsToCharacter(Self.parseCodePointString(remoteItem.unified))
|
||||
let baseEmoji = Emoji(emojiChar: baseEmojiChar, base: baseEmojiChar, skintoneSequence: .none)
|
||||
|
||||
|
@ -509,7 +514,7 @@ extension EmojiGenerator {
|
|||
fileHandle.indent {
|
||||
fileHandle.writeLine("switch self {")
|
||||
emojiModel.definitions.forEach {
|
||||
fileHandle.writeLine("case .\($0.enumName): return \"\($0.rawName)\"")
|
||||
fileHandle.writeLine("case .\($0.enumName): return \"\($0.shortNames.joined(separator:", "))\"")
|
||||
}
|
||||
fileHandle.writeLine("}")
|
||||
}
|
||||
|
|
|
@ -117,8 +117,10 @@
|
|||
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; };
|
||||
7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; };
|
||||
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; };
|
||||
7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; };
|
||||
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; };
|
||||
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; };
|
||||
7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; };
|
||||
7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; };
|
||||
7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; };
|
||||
7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; };
|
||||
|
@ -126,6 +128,8 @@
|
|||
7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; };
|
||||
7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; };
|
||||
7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; };
|
||||
7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; };
|
||||
7B81682C28B72F480069F315 /* PendingChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682B28B72F480069F315 /* PendingChange.swift */; };
|
||||
7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; };
|
||||
7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; };
|
||||
7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */; };
|
||||
|
@ -150,6 +154,7 @@
|
|||
7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */; };
|
||||
7BAF54D827ACD0E3003D12F8 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */; };
|
||||
7BAF54DC27ACD12B003D12F8 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */; };
|
||||
7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */; };
|
||||
7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */; };
|
||||
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; };
|
||||
7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -1191,8 +1196,10 @@
|
|||
7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.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>"; };
|
||||
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.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>"; };
|
||||
7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = "<group>"; };
|
||||
7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = "<group>"; };
|
||||
7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
|
||||
7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = "<group>"; };
|
||||
|
@ -1200,6 +1207,8 @@
|
|||
7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = "<group>"; };
|
||||
7B81682228A4C1210069F315 /* UpdateTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTypes.swift; sourceTree = "<group>"; };
|
||||
7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = "<group>"; };
|
||||
7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = "<group>"; };
|
||||
7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = "<group>"; };
|
||||
7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = "<group>"; };
|
||||
7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
|
||||
7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1224,6 +1233,7 @@
|
|||
7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = "<group>"; };
|
||||
7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
|
||||
7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
|
||||
7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTitleViewController.swift; sourceTree = "<group>"; };
|
||||
7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableLabel.swift; sourceTree = "<group>"; };
|
||||
7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
|
||||
|
@ -2144,6 +2154,7 @@
|
|||
34074FC5203E5435004596AE /* messageReceivedSounds */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7B50D64C28AC7CF80086CCEC /* silence.aiff */,
|
||||
45B74A5B2044AAB300CD42F8 /* aurora-quiet.aifc */,
|
||||
45B74A6F2044AAB500CD42F8 /* aurora.aifc */,
|
||||
45B74A5F2044AAB400CD42F8 /* bamboo-quiet.aifc */,
|
||||
|
@ -3007,6 +3018,7 @@
|
|||
FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */,
|
||||
45F32C1D205718B000A300D5 /* MediaPageViewController.swift */,
|
||||
454A84032059C787008B8C75 /* MediaTileViewController.swift */,
|
||||
7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */,
|
||||
34E3E5671EC4B19400495BAC /* AudioProgressView.swift */,
|
||||
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */,
|
||||
34969559219B605E00DCFE74 /* ImagePickerController.swift */,
|
||||
|
@ -3017,6 +3029,7 @@
|
|||
4C21D5D7223AC60F00EF8A77 /* PhotoCapture.swift */,
|
||||
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
|
||||
4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */,
|
||||
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */,
|
||||
);
|
||||
path = "Media Viewing & Editing";
|
||||
sourceTree = "<group>";
|
||||
|
@ -4029,6 +4042,8 @@
|
|||
FDC438A327BB107F00C60D73 /* UserBanRequest.swift */,
|
||||
FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */,
|
||||
FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */,
|
||||
7B81682928B6F1420069F315 /* ReactionResponse.swift */,
|
||||
7B81682B28B72F480069F315 /* PendingChange.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
@ -4756,6 +4771,7 @@
|
|||
45B74A872044AAB600CD42F8 /* complete-quiet.aifc in Resources */,
|
||||
45B74A772044AAB600CD42F8 /* hello.aifc in Resources */,
|
||||
45B74A7C2044AAB600CD42F8 /* hello-quiet.aifc in Resources */,
|
||||
7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */,
|
||||
45B74A792044AAB600CD42F8 /* input.aifc in Resources */,
|
||||
C3CA3ABE255CDB0D00F4C6D4 /* portuguese.txt in Resources */,
|
||||
45B74A8C2044AAB600CD42F8 /* input-quiet.aifc in Resources */,
|
||||
|
@ -5451,6 +5467,7 @@
|
|||
C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */,
|
||||
FDF40CDE2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift in Sources */,
|
||||
FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */,
|
||||
7B81682C28B72F480069F315 /* PendingChange.swift in Sources */,
|
||||
FD77289A284AF1BD0018502F /* Sodium+Utilities.swift in Sources */,
|
||||
FD5C7309285007920029977D /* BlindedIdLookup.swift in Sources */,
|
||||
FD71161C28D194FB00B47552 /* MentionInfo.swift in Sources */,
|
||||
|
@ -5465,6 +5482,7 @@
|
|||
FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */,
|
||||
FD716E6A2850327900C96BF4 /* EndCallMode.swift in Sources */,
|
||||
FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */,
|
||||
7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */,
|
||||
FD09799727FFA84A00936362 /* RecipientState.swift in Sources */,
|
||||
FDA8EB00280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift in Sources */,
|
||||
FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */,
|
||||
|
@ -5690,6 +5708,7 @@
|
|||
FDFDE124282D04F20098B17F /* MediaDismissAnimationController.swift in Sources */,
|
||||
7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */,
|
||||
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
||||
7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */,
|
||||
C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */,
|
||||
FD52090728B49738006098F6 /* ConfirmationModal.swift in Sources */,
|
||||
C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */,
|
||||
|
@ -5699,6 +5718,9 @@
|
|||
B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */,
|
||||
B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */,
|
||||
7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */,
|
||||
B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */,
|
||||
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */,
|
||||
7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */,
|
||||
B877E24226CA12910007970A /* CallVC.swift in Sources */,
|
||||
7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */,
|
||||
C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */,
|
||||
|
@ -6071,7 +6093,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 372;
|
||||
CURRENT_PROJECT_VERSION = 374;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -6144,7 +6166,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 372;
|
||||
CURRENT_PROJECT_VERSION = 374;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -6210,7 +6232,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 372;
|
||||
CURRENT_PROJECT_VERSION = 374;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -6284,7 +6306,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 372;
|
||||
CURRENT_PROJECT_VERSION = 374;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -7222,7 +7244,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 372;
|
||||
CURRENT_PROJECT_VERSION = 374;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -7294,7 +7316,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 372;
|
||||
CURRENT_PROJECT_VERSION = 374;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
|
|
@ -71,6 +71,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
|
|||
var connectingDate: Date? {
|
||||
didSet {
|
||||
stateDidChange?()
|
||||
resetTimeoutTimerIfNeeded()
|
||||
hasStartedConnectingDidChange?()
|
||||
}
|
||||
}
|
||||
|
@ -113,12 +114,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
|
|||
set { connectingDate = newValue ? Date() : nil }
|
||||
}
|
||||
|
||||
var hasConnected: Bool {
|
||||
public var hasConnected: Bool {
|
||||
get { return connectedDate != nil }
|
||||
set { connectedDate = newValue ? Date() : nil }
|
||||
}
|
||||
|
||||
var hasEnded: Bool {
|
||||
public var hasEnded: Bool {
|
||||
get { return endDate != nil }
|
||||
set { endDate = newValue ? Date() : nil }
|
||||
}
|
||||
|
@ -277,55 +278,60 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
|
|||
let duration: TimeInterval = self.duration
|
||||
let hasStartedConnecting: Bool = self.hasStartedConnecting
|
||||
|
||||
Storage.shared.writeAsync { db in
|
||||
guard let interaction: Interaction = try? Interaction.fetchOne(db, id: callInteractionId) else {
|
||||
return
|
||||
}
|
||||
|
||||
let updateToMissedIfNeeded: () throws -> () = {
|
||||
let missedCallInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed)
|
||||
|
||||
guard
|
||||
let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8),
|
||||
let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode(
|
||||
CallMessage.MessageInfo.self,
|
||||
from: infoMessageData
|
||||
),
|
||||
messageInfo.state == .incoming,
|
||||
let missedCallInfoData: Data = try? JSONEncoder().encode(missedCallInfo)
|
||||
else { return }
|
||||
|
||||
_ = try interaction
|
||||
.with(body: String(data: missedCallInfoData, encoding: .utf8))
|
||||
.saved(db)
|
||||
}
|
||||
let shouldMarkAsRead: Bool = try {
|
||||
if duration > 0 { return true }
|
||||
if hasStartedConnecting { return true }
|
||||
|
||||
switch mode {
|
||||
case .local:
|
||||
try updateToMissedIfNeeded()
|
||||
return true
|
||||
|
||||
case .remote, .unanswered:
|
||||
try updateToMissedIfNeeded()
|
||||
return false
|
||||
|
||||
case .answeredElsewhere: return true
|
||||
Storage.shared.writeAsync(
|
||||
updates: { db in
|
||||
guard let interaction: Interaction = try? Interaction.fetchOne(db, id: callInteractionId) else {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
guard shouldMarkAsRead else { return }
|
||||
|
||||
try Interaction.markAsRead(
|
||||
db,
|
||||
interactionId: interaction.id,
|
||||
threadId: interaction.threadId,
|
||||
includingOlder: false,
|
||||
trySendReadReceipt: false
|
||||
)
|
||||
}
|
||||
|
||||
let updateToMissedIfNeeded: () throws -> () = {
|
||||
let missedCallInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed)
|
||||
|
||||
guard
|
||||
let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8),
|
||||
let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode(
|
||||
CallMessage.MessageInfo.self,
|
||||
from: infoMessageData
|
||||
),
|
||||
messageInfo.state == .incoming,
|
||||
let missedCallInfoData: Data = try? JSONEncoder().encode(missedCallInfo)
|
||||
else { return }
|
||||
|
||||
_ = try interaction
|
||||
.with(body: String(data: missedCallInfoData, encoding: .utf8))
|
||||
.saved(db)
|
||||
}
|
||||
let shouldMarkAsRead: Bool = try {
|
||||
if duration > 0 { return true }
|
||||
if hasStartedConnecting { return true }
|
||||
|
||||
switch mode {
|
||||
case .local:
|
||||
try updateToMissedIfNeeded()
|
||||
return true
|
||||
|
||||
case .remote, .unanswered:
|
||||
try updateToMissedIfNeeded()
|
||||
return false
|
||||
|
||||
case .answeredElsewhere: return true
|
||||
}
|
||||
}()
|
||||
|
||||
guard shouldMarkAsRead else { return }
|
||||
|
||||
try Interaction.markAsRead(
|
||||
db,
|
||||
interactionId: interaction.id,
|
||||
threadId: interaction.threadId,
|
||||
includingOlder: false,
|
||||
trySendReadReceipt: false
|
||||
)
|
||||
},
|
||||
completion: { _, _ in
|
||||
SessionCallManager.suspendDatabaseIfCallEndedInBackground()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Renderer
|
||||
|
@ -421,6 +427,11 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
public func resetTimeoutTimerIfNeeded() {
|
||||
if self.timeOutTimer == nil { return }
|
||||
setupTimeoutTimer()
|
||||
}
|
||||
|
||||
public func invalidateTimeoutTimer() {
|
||||
timeOutTimer?.invalidate()
|
||||
timeOutTimer = nil
|
||||
|
|
|
@ -73,13 +73,19 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
|||
// MARK: - Report calls
|
||||
|
||||
public static func reportFakeCall(info: String) {
|
||||
SessionCallManager.sharedProvider(useSystemCallLog: false)
|
||||
.reportNewIncomingCall(
|
||||
with: UUID(),
|
||||
update: CXCallUpdate()
|
||||
) { _ in
|
||||
SNLog("[Calls] Reported fake incoming call to CallKit due to: \(info)")
|
||||
}
|
||||
let callId = UUID()
|
||||
let provider = SessionCallManager.sharedProvider(useSystemCallLog: false)
|
||||
provider.reportNewIncomingCall(
|
||||
with: callId,
|
||||
update: CXCallUpdate()
|
||||
) { _ in
|
||||
SNLog("[Calls] Reported fake incoming call to CallKit due to: \(info)")
|
||||
}
|
||||
provider.reportCall(
|
||||
with: callId,
|
||||
endedAt: nil,
|
||||
reason: .failed
|
||||
)
|
||||
}
|
||||
|
||||
public func reportOutgoingCall(_ call: SessionCall) {
|
||||
|
@ -98,30 +104,22 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
|||
}
|
||||
|
||||
public func reportIncomingCall(_ call: SessionCall, callerName: String, completion: @escaping (Error?) -> Void) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
if let provider = provider {
|
||||
// Construct a CXCallUpdate describing the incoming call, including the caller.
|
||||
let update = CXCallUpdate()
|
||||
update.localizedCallerName = callerName
|
||||
update.remoteHandle = CXHandle(type: .generic, value: call.callId.uuidString)
|
||||
update.hasVideo = false
|
||||
let provider = provider ?? Self.sharedProvider(useSystemCallLog: false)
|
||||
// Construct a CXCallUpdate describing the incoming call, including the caller.
|
||||
let update = CXCallUpdate()
|
||||
update.localizedCallerName = callerName
|
||||
update.remoteHandle = CXHandle(type: .generic, value: call.callId.uuidString)
|
||||
update.hasVideo = false
|
||||
|
||||
disableUnsupportedFeatures(callUpdate: update)
|
||||
disableUnsupportedFeatures(callUpdate: update)
|
||||
|
||||
// Report the incoming call to the system
|
||||
provider.reportNewIncomingCall(with: call.callId, update: update) { error in
|
||||
guard error == nil else {
|
||||
self.reportCurrentCallEnded(reason: .failed)
|
||||
completion(error)
|
||||
return
|
||||
}
|
||||
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing")
|
||||
completion(nil)
|
||||
// Report the incoming call to the system
|
||||
provider.reportNewIncomingCall(with: call.callId, update: update) { error in
|
||||
guard error == nil else {
|
||||
self.reportCurrentCallEnded(reason: .failed)
|
||||
completion(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
else {
|
||||
SessionCallManager.reportFakeCall(info: "No CXProvider instance")
|
||||
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing")
|
||||
completion(nil)
|
||||
}
|
||||
|
@ -135,7 +133,16 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
|||
return
|
||||
}
|
||||
|
||||
guard let call = currentCall else { return }
|
||||
func handleCallEnded() {
|
||||
WebRTCSession.current = nil
|
||||
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing")
|
||||
}
|
||||
|
||||
guard let call = currentCall else {
|
||||
handleCallEnded()
|
||||
Self.suspendDatabaseIfCallEndedInBackground()
|
||||
return
|
||||
}
|
||||
|
||||
if let reason = reason {
|
||||
self.provider?.reportCall(with: call.callId, endedAt: nil, reason: reason)
|
||||
|
@ -153,8 +160,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
|||
|
||||
call.webRTCSession.dropConnection()
|
||||
self.currentCall = nil
|
||||
WebRTCSession.current = nil
|
||||
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing")
|
||||
handleCallEnded()
|
||||
}
|
||||
|
||||
// MARK: - Util
|
||||
|
@ -172,15 +178,18 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
|||
callUpdate.supportsDTMF = false
|
||||
}
|
||||
|
||||
public static func suspendDatabaseIfCallEndedInBackground() {
|
||||
if CurrentAppContext().isInBackground() {
|
||||
// Stop all jobs except for message sending and when completed suspend the database
|
||||
JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) {
|
||||
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
public func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) {
|
||||
guard Thread.isMainThread else {
|
||||
DispatchQueue.main.async {
|
||||
self.showCallUIForCall(caller: caller, uuid: uuid, mode: mode, interactionId: interactionId)
|
||||
}
|
||||
return
|
||||
}
|
||||
guard let call: SessionCall = Storage.shared.read({ db in SessionCall(db, for: caller, uuid: uuid, mode: mode) }) else {
|
||||
return
|
||||
}
|
||||
|
@ -193,20 +202,23 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
|||
}
|
||||
|
||||
guard CurrentAppContext().isMainAppAndActive else { return }
|
||||
guard let presentingVC = CurrentAppContext().frontmostViewController() else {
|
||||
preconditionFailure() // FIXME: Handle more gracefully
|
||||
}
|
||||
|
||||
if let conversationVC: ConversationVC = presentingVC as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId {
|
||||
let callVC = CallVC(for: call)
|
||||
callVC.conversationVC = conversationVC
|
||||
conversationVC.inputAccessoryView?.isHidden = true
|
||||
conversationVC.inputAccessoryView?.alpha = 0
|
||||
presentingVC.present(callVC, animated: true, completion: nil)
|
||||
}
|
||||
else if !Preferences.isCallKitSupported {
|
||||
let incomingCallBanner = IncomingCallBanner(for: call)
|
||||
incomingCallBanner.show()
|
||||
DispatchQueue.main.async {
|
||||
guard let presentingVC = CurrentAppContext().frontmostViewController() else {
|
||||
preconditionFailure() // FIXME: Handle more gracefully
|
||||
}
|
||||
|
||||
if let conversationVC: ConversationVC = presentingVC as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId {
|
||||
let callVC = CallVC(for: call)
|
||||
callVC.conversationVC = conversationVC
|
||||
conversationVC.inputAccessoryView?.isHidden = true
|
||||
conversationVC.inputAccessoryView?.alpha = 0
|
||||
presentingVC.present(callVC, animated: true, completion: nil)
|
||||
}
|
||||
else if !Preferences.isCallKitSupported {
|
||||
let incomingCallBanner = IncomingCallBanner(for: call)
|
||||
incomingCallBanner.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -747,7 +747,7 @@ extension ConversationVC:
|
|||
.elements
|
||||
.firstIndex(of: cellViewModel),
|
||||
let cell = tableView.cellForRow(at: IndexPath(row: index, section: sectionIndex)) as? VisibleMessageCell,
|
||||
let snapshot = cell.bubbleView.snapshotView(afterScreenUpdates: false),
|
||||
let snapshot = cell.snContentView.snapshotView(afterScreenUpdates: false),
|
||||
contextMenuWindow == nil,
|
||||
let actions: [ContextMenuVC.Action] = ContextMenuVC.actions(
|
||||
for: cellViewModel,
|
||||
|
@ -769,7 +769,7 @@ extension ConversationVC:
|
|||
self.contextMenuWindow = ContextMenuWindow()
|
||||
self.contextMenuVC = ContextMenuVC(
|
||||
snapshot: snapshot,
|
||||
frame: cell.convert(cell.bubbleView.frame, to: keyWindow),
|
||||
frame: cell.convert(cell.snContentView.frame, to: keyWindow),
|
||||
cellViewModel: cellViewModel,
|
||||
actions: actions
|
||||
) { [weak self] in
|
||||
|
@ -1145,6 +1145,15 @@ extension ConversationVC:
|
|||
return Promise(error: StorageError.objectNotFound)
|
||||
}
|
||||
|
||||
let pendingChange = OpenGroupManager
|
||||
.addPendingReaction(
|
||||
emoji: emoji,
|
||||
id: openGroupServerMessageId,
|
||||
in: openGroup.roomToken,
|
||||
on: openGroup.server,
|
||||
type: .removeAll
|
||||
)
|
||||
|
||||
return OpenGroupAPI
|
||||
.reactionDeleteAll(
|
||||
db,
|
||||
|
@ -1153,7 +1162,13 @@ extension ConversationVC:
|
|||
in: openGroup.roomToken,
|
||||
on: openGroup.server
|
||||
)
|
||||
.map { _ in () }
|
||||
.map { _, response in
|
||||
OpenGroupManager
|
||||
.updatePendingChange(
|
||||
pendingChange,
|
||||
seqNo: response.seqNo
|
||||
)
|
||||
}
|
||||
}
|
||||
.done { _ in
|
||||
Storage.shared.writeAsync { db in
|
||||
|
@ -1201,29 +1216,42 @@ extension ConversationVC:
|
|||
.filter(id: thread.id)
|
||||
.updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
|
||||
let pendingReaction: Reaction? = {
|
||||
if remove {
|
||||
return try? Reaction
|
||||
.filter(Reaction.Columns.interactionId == cellViewModel.id)
|
||||
.filter(Reaction.Columns.authorId == cellViewModel.currentUserPublicKey)
|
||||
.filter(Reaction.Columns.emoji == emoji)
|
||||
.fetchOne(db)
|
||||
} else {
|
||||
let sortId = Reaction.getSortId(
|
||||
db,
|
||||
interactionId: cellViewModel.id,
|
||||
emoji: emoji
|
||||
)
|
||||
|
||||
return Reaction(
|
||||
interactionId: cellViewModel.id,
|
||||
serverHash: nil,
|
||||
timestampMs: sentTimestamp,
|
||||
authorId: cellViewModel.currentUserPublicKey,
|
||||
emoji: emoji,
|
||||
count: 1,
|
||||
sortId: sortId
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
// Update the database
|
||||
if remove {
|
||||
_ = try Reaction
|
||||
try Reaction
|
||||
.filter(Reaction.Columns.interactionId == cellViewModel.id)
|
||||
.filter(Reaction.Columns.authorId == cellViewModel.currentUserPublicKey)
|
||||
.filter(Reaction.Columns.emoji == emoji)
|
||||
.deleteAll(db)
|
||||
}
|
||||
else {
|
||||
let sortId = Reaction.getSortId(
|
||||
db,
|
||||
interactionId: cellViewModel.id,
|
||||
emoji: emoji
|
||||
)
|
||||
try Reaction(
|
||||
interactionId: cellViewModel.id,
|
||||
serverHash: nil,
|
||||
timestampMs: sentTimestamp,
|
||||
authorId: cellViewModel.currentUserPublicKey,
|
||||
emoji: emoji,
|
||||
count: 1,
|
||||
sortId: sortId
|
||||
).insert(db)
|
||||
try pendingReaction?.insert(db)
|
||||
|
||||
// Add it to the recent list
|
||||
Emoji.addRecent(db, emoji: emoji)
|
||||
|
@ -1242,6 +1270,14 @@ extension ConversationVC:
|
|||
else { return }
|
||||
|
||||
if remove {
|
||||
let pendingChange = OpenGroupManager
|
||||
.addPendingReaction(
|
||||
emoji: emoji,
|
||||
id: openGroupServerMessageId,
|
||||
in: openGroup.roomToken,
|
||||
on: openGroup.server,
|
||||
type: .remove
|
||||
)
|
||||
OpenGroupAPI
|
||||
.reactionDelete(
|
||||
db,
|
||||
|
@ -1250,9 +1286,34 @@ extension ConversationVC:
|
|||
in: openGroup.roomToken,
|
||||
on: openGroup.server
|
||||
)
|
||||
.map { _, response in
|
||||
OpenGroupManager
|
||||
.updatePendingChange(
|
||||
pendingChange,
|
||||
seqNo: response.seqNo
|
||||
)
|
||||
}
|
||||
.catch { [weak self] _ in
|
||||
OpenGroupManager.removePendingChange(pendingChange)
|
||||
|
||||
self?.handleReactionSentFailure(
|
||||
pendingReaction,
|
||||
remove: remove
|
||||
)
|
||||
|
||||
}
|
||||
.retainUntilComplete()
|
||||
}
|
||||
else {
|
||||
let pendingChange = OpenGroupManager
|
||||
.addPendingReaction(
|
||||
emoji: emoji,
|
||||
id: openGroupServerMessageId,
|
||||
in: openGroup.roomToken,
|
||||
on: openGroup.server,
|
||||
type: .add
|
||||
)
|
||||
|
||||
OpenGroupAPI
|
||||
.reactionAdd(
|
||||
db,
|
||||
|
@ -1261,6 +1322,21 @@ extension ConversationVC:
|
|||
in: openGroup.roomToken,
|
||||
on: openGroup.server
|
||||
)
|
||||
.map { _, response in
|
||||
OpenGroupManager
|
||||
.updatePendingChange(
|
||||
pendingChange,
|
||||
seqNo: response.seqNo
|
||||
)
|
||||
}
|
||||
.catch { [weak self] _ in
|
||||
OpenGroupManager.removePendingChange(pendingChange)
|
||||
|
||||
self?.handleReactionSentFailure(
|
||||
pendingReaction,
|
||||
remove: remove
|
||||
)
|
||||
}
|
||||
.retainUntilComplete()
|
||||
}
|
||||
}
|
||||
|
@ -1292,6 +1368,23 @@ extension ConversationVC:
|
|||
)
|
||||
}
|
||||
|
||||
func handleReactionSentFailure(_ pendingReaction: Reaction?, remove: Bool) {
|
||||
guard let pendingReaction = pendingReaction else { return }
|
||||
Storage.shared.writeAsync { db in
|
||||
// Reverse the database
|
||||
if remove {
|
||||
try pendingReaction.insert(db)
|
||||
}
|
||||
else {
|
||||
try Reaction
|
||||
.filter(Reaction.Columns.interactionId == pendingReaction.interactionId)
|
||||
.filter(Reaction.Columns.authorId == pendingReaction.authorId)
|
||||
.filter(Reaction.Columns.emoji == pendingReaction.emoji)
|
||||
.deleteAll(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel) {
|
||||
hideInputAccessoryView()
|
||||
|
||||
|
@ -2113,6 +2206,35 @@ extension ConversationVC {
|
|||
preferredStyle: .actionSheet
|
||||
)
|
||||
alertVC.addAction(UIAlertAction(title: "TXT_DELETE_TITLE".localized(), style: .destructive) { _ in
|
||||
// Delete the request
|
||||
Storage.shared.writeAsync(
|
||||
updates: { db in
|
||||
_ = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.deleteAll(db)
|
||||
},
|
||||
completion: { db, _ in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil))
|
||||
|
||||
self.present(alertVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func block() {
|
||||
guard self.viewModel.threadData.threadVariant == .contact else { return }
|
||||
|
||||
let threadId: String = self.viewModel.threadData.threadId
|
||||
let alertVC: UIAlertController = UIAlertController(
|
||||
title: "MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON".localized(),
|
||||
message: nil,
|
||||
preferredStyle: .actionSheet
|
||||
)
|
||||
alertVC.addAction(UIAlertAction(title: "BLOCK_LIST_BLOCK_BUTTON".localized(), style: .destructive) { _ in
|
||||
// Delete the request
|
||||
Storage.shared.writeAsync(
|
||||
updates: { db in
|
||||
|
|
|
@ -245,11 +245,23 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
private lazy var messageRequestDeleteButton: UIButton = {
|
||||
let result: OutlineButton = OutlineButton(style: .destructive, size: .medium)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle("TXT_DELETE_TITLE".localized(), for: .normal)
|
||||
result.setTitle("TXT_DECLINE_TITLE".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(deleteMessageRequest), for: .touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var messageRequestBlockButton: UIButton = {
|
||||
let result: UIButton = UIButton()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
|
||||
result.setTitle("TXT_BLOCK_USER_TITLE".localized(), for: .normal)
|
||||
result.setTitleColor(Colors.destructive, for: .normal)
|
||||
result.addTarget(self, action: #selector(block), for: .touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
|
@ -305,6 +317,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
view.addSubview(scrollButton)
|
||||
view.addSubview(messageRequestView)
|
||||
|
||||
messageRequestView.addSubview(messageRequestBlockButton)
|
||||
messageRequestView.addSubview(messageRequestDescriptionLabel)
|
||||
messageRequestView.addSubview(messageRequestAcceptButton)
|
||||
messageRequestView.addSubview(messageRequestDeleteButton)
|
||||
|
@ -317,7 +330,10 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
self.scrollButtonBottomConstraint?.isActive = false // Note: Need to disable this to avoid a conflict with the other bottom constraint
|
||||
self.scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestView, withInset: -16)
|
||||
|
||||
messageRequestDescriptionLabel.pin(.top, to: .top, of: messageRequestView, withInset: 10)
|
||||
messageRequestBlockButton.pin(.top, to: .top, of: messageRequestView, withInset: 10)
|
||||
messageRequestBlockButton.center(.horizontal, in: messageRequestView)
|
||||
|
||||
messageRequestDescriptionLabel.pin(.top, to: .bottom, of: messageRequestBlockButton, withInset: 5)
|
||||
messageRequestDescriptionLabel.pin(.left, to: .left, of: messageRequestView, withInset: 40)
|
||||
messageRequestDescriptionLabel.pin(.right, to: .right, of: messageRequestView, withInset: -40)
|
||||
|
||||
|
|
|
@ -228,5 +228,15 @@ extension EmojiPickerSheet: UISearchBarDelegate {
|
|||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
collectionView.searchText = searchText
|
||||
}
|
||||
|
||||
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
|
||||
searchBar.showsCancelButton = true
|
||||
return true
|
||||
}
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.showsCancelButton = false
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@ public class MediaAlbumView: UIStackView {
|
|||
public let itemViews: [MediaView]
|
||||
public var moreItemsView: MediaView?
|
||||
|
||||
private static let kSpacingPts: CGFloat = 2
|
||||
private static let kMaxItems = 5
|
||||
private static let kSpacingPts: CGFloat = 4
|
||||
private static let kMaxItems = 3
|
||||
|
||||
@available(*, unavailable, message: "use other init() instead.")
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
|
@ -70,8 +70,8 @@ public class MediaAlbumView: UIStackView {
|
|||
self.axis = .horizontal
|
||||
self.distribution = .fillEqually
|
||||
self.spacing = MediaAlbumView.kSpacingPts
|
||||
|
||||
case 3:
|
||||
|
||||
default:
|
||||
// x
|
||||
// X x
|
||||
// Big on left, 2 small on right.
|
||||
|
@ -95,64 +95,9 @@ public class MediaAlbumView: UIStackView {
|
|||
)
|
||||
self.axis = .horizontal
|
||||
self.spacing = MediaAlbumView.kSpacingPts
|
||||
|
||||
case 4:
|
||||
// X X
|
||||
// X X
|
||||
// Square
|
||||
let imageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2
|
||||
|
||||
let topViews = Array(itemViews[0..<2])
|
||||
addArrangedSubview(
|
||||
newRow(
|
||||
rowViews: topViews,
|
||||
axis: .horizontal,
|
||||
viewSize: imageSize
|
||||
)
|
||||
)
|
||||
|
||||
let bottomViews = Array(itemViews[2..<4])
|
||||
addArrangedSubview(
|
||||
newRow(
|
||||
rowViews: bottomViews,
|
||||
axis: .horizontal,
|
||||
viewSize: imageSize
|
||||
)
|
||||
)
|
||||
|
||||
self.axis = .vertical
|
||||
self.spacing = MediaAlbumView.kSpacingPts
|
||||
|
||||
default:
|
||||
// X X
|
||||
// xxx
|
||||
// 2 big on top, 3 small on bottom.
|
||||
let bigImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2
|
||||
let smallImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts * 2) / 3
|
||||
|
||||
let topViews = Array(itemViews[0..<2])
|
||||
addArrangedSubview(
|
||||
newRow(
|
||||
rowViews: topViews,
|
||||
axis: .horizontal,
|
||||
viewSize: bigImageSize
|
||||
)
|
||||
)
|
||||
|
||||
let bottomViews = Array(itemViews[2..<5])
|
||||
addArrangedSubview(
|
||||
newRow(
|
||||
rowViews: bottomViews,
|
||||
axis: .horizontal,
|
||||
viewSize: smallImageSize
|
||||
)
|
||||
)
|
||||
|
||||
self.axis = .vertical
|
||||
self.spacing = MediaAlbumView.kSpacingPts
|
||||
|
||||
if items.count > MediaAlbumView.kMaxItems {
|
||||
guard let lastView = bottomViews.last else {
|
||||
guard let lastView = rightViews.last else {
|
||||
owsFailDebug("Missing lastView")
|
||||
return
|
||||
}
|
||||
|
@ -267,13 +212,8 @@ public class MediaAlbumView: UIStackView {
|
|||
let itemCount = itemsToDisplay(forItems: items).count
|
||||
|
||||
switch itemCount {
|
||||
case 0, 1, 4:
|
||||
case 0, 1:
|
||||
// X
|
||||
//
|
||||
// or
|
||||
//
|
||||
// XX
|
||||
// XX
|
||||
// Square
|
||||
return CGSize(width: maxMessageWidth, height: maxMessageWidth)
|
||||
|
||||
|
@ -283,21 +223,13 @@ public class MediaAlbumView: UIStackView {
|
|||
let imageSize = (maxMessageWidth - kSpacingPts) / 2
|
||||
return CGSize(width: maxMessageWidth, height: imageSize)
|
||||
|
||||
case 3:
|
||||
default:
|
||||
// x
|
||||
// X x
|
||||
// Big on left, 2 small on right.
|
||||
let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3
|
||||
let bigImageSize = smallImageSize * 2 + kSpacingPts
|
||||
return CGSize(width: maxMessageWidth, height: bigImageSize)
|
||||
|
||||
default:
|
||||
// X X
|
||||
// xxx
|
||||
// 2 big on top, 3 small on bottom.
|
||||
let bigImageSize = (maxMessageWidth - kSpacingPts) / 2
|
||||
let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3
|
||||
return CGSize(width: maxMessageWidth, height: bigImageSize + smallImageSize + kSpacingPts)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,8 @@ public class MediaView: UIView {
|
|||
|
||||
themeBackgroundColor = .backgroundSecondary
|
||||
clipsToBounds = true
|
||||
layer.masksToBounds = true
|
||||
layer.cornerRadius = VisibleMessageCell.largeCornerRadius
|
||||
|
||||
createContents()
|
||||
}
|
||||
|
|
|
@ -20,15 +20,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0)
|
||||
private lazy var profilePictureViewLeftConstraint = profilePictureView.pin(.left, to: .left, of: self, withInset: VisibleMessageCell.groupThreadHSpacing)
|
||||
private lazy var profilePictureViewWidthConstraint = profilePictureView.set(.width, to: Values.verySmallProfilePictureSize)
|
||||
private lazy var contentViewLeftConstraint1 = snContentView.pin(.left, to: .right, of: profilePictureView, withInset: VisibleMessageCell.groupThreadHSpacing)
|
||||
private lazy var contentViewLeftConstraint2 = snContentView.leftAnchor.constraint(greaterThanOrEqualTo: leftAnchor, constant: VisibleMessageCell.gutterSize)
|
||||
private lazy var contentViewTopConstraint = snContentView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing)
|
||||
private lazy var contentViewRightConstraint1 = snContentView.pin(.right, to: .right, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing)
|
||||
private lazy var contentViewRightConstraint2 = snContentView.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -VisibleMessageCell.gutterSize)
|
||||
|
||||
private lazy var bubbleViewLeftConstraint1 = bubbleView.pin(.left, to: .right, of: profilePictureView, withInset: VisibleMessageCell.groupThreadHSpacing)
|
||||
private lazy var bubbleViewLeftConstraint2 = bubbleView.leftAnchor.constraint(greaterThanOrEqualTo: leftAnchor, constant: VisibleMessageCell.gutterSize)
|
||||
private lazy var bubbleViewTopConstraint = bubbleView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing)
|
||||
private lazy var bubbleViewRightConstraint1 = bubbleView.pin(.right, to: .right, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing)
|
||||
private lazy var bubbleViewRightConstraint2 = bubbleView.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -VisibleMessageCell.gutterSize)
|
||||
|
||||
private lazy var reactionContainerViewLeftConstraint = reactionContainerView.pin(.left, to: .left, of: bubbleView)
|
||||
private lazy var reactionContainerViewRightConstraint = reactionContainerView.pin(.right, to: .right, of: bubbleView)
|
||||
private lazy var reactionContainerViewLeftConstraint = reactionContainerView.pin(.left, to: .left, of: snContentView)
|
||||
private lazy var reactionContainerViewRightConstraint = reactionContainerView.pin(.right, to: .right, of: snContentView)
|
||||
|
||||
private lazy var messageStatusImageViewTopConstraint = messageStatusImageView.pin(.top, to: .bottom, of: reactionContainerView, withInset: 0)
|
||||
private lazy var messageStatusImageViewWidthConstraint = messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize)
|
||||
|
@ -46,9 +45,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
// MARK: - UI Components
|
||||
|
||||
private lazy var viewsToMoveForReply: [UIView] = [
|
||||
bubbleView,
|
||||
bubbleBackgroundView,
|
||||
snContentView,
|
||||
profilePictureView,
|
||||
moderatorIconImageView,
|
||||
replyButton,
|
||||
timerView,
|
||||
messageStatusImageView,
|
||||
|
@ -87,7 +86,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var snContentView = UIView()
|
||||
lazy var snContentView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [])
|
||||
result.axis = .vertical
|
||||
result.spacing = Values.verySmallSpacing
|
||||
result.alignment = .leading
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var reactionContainerView = ReactionContainerView()
|
||||
|
||||
internal lazy var messageStatusImageView: UIImageView = {
|
||||
|
@ -149,6 +155,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
static var leftGutterSize: CGFloat { groupThreadHSpacing + profilePictureSize + groupThreadHSpacing }
|
||||
|
||||
// MARK: Direction & Position
|
||||
|
||||
enum Direction { case incoming, outgoing }
|
||||
|
@ -180,35 +188,31 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureView, withInset: 1)
|
||||
moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: 4.5)
|
||||
|
||||
// Bubble background view (used for the 'highlighted' animation)
|
||||
addSubview(bubbleBackgroundView)
|
||||
// Content view
|
||||
addSubview(snContentView)
|
||||
contentViewLeftConstraint1.isActive = true
|
||||
contentViewTopConstraint.isActive = true
|
||||
contentViewRightConstraint1.isActive = true
|
||||
snContentView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: -1)
|
||||
|
||||
// Bubble view
|
||||
addSubview(bubbleView)
|
||||
bubbleViewLeftConstraint1.isActive = true
|
||||
bubbleViewTopConstraint.isActive = true
|
||||
bubbleViewRightConstraint1.isActive = true
|
||||
bubbleView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: -1)
|
||||
// Bubble background view
|
||||
bubbleBackgroundView.addSubview(bubbleView)
|
||||
bubbleBackgroundView.pin(to: bubbleView)
|
||||
|
||||
// Timer view
|
||||
addSubview(timerView)
|
||||
timerView.center(.vertical, in: bubbleView)
|
||||
timerView.center(.vertical, in: snContentView)
|
||||
timerViewOutgoingMessageConstraint.isActive = true
|
||||
|
||||
// Content view
|
||||
bubbleView.addSubview(snContentView)
|
||||
snContentView.pin(to: bubbleView)
|
||||
|
||||
// Reaction view
|
||||
addSubview(reactionContainerView)
|
||||
reactionContainerView.pin(.top, to: .bottom, of: bubbleView, withInset: Values.verySmallSpacing)
|
||||
reactionContainerView.pin(.top, to: .bottom, of: snContentView, withInset: Values.verySmallSpacing)
|
||||
reactionContainerViewLeftConstraint.isActive = true
|
||||
|
||||
// Message status image view
|
||||
addSubview(messageStatusImageView)
|
||||
messageStatusImageViewTopConstraint.isActive = true
|
||||
messageStatusImageView.pin(.right, to: .right, of: bubbleView, withInset: -1)
|
||||
messageStatusImageView.pin(.right, to: .right, of: snContentView, withInset: -1)
|
||||
messageStatusImageView.pin(.bottom, to: .bottom, of: self, withInset: -1)
|
||||
messageStatusImageViewWidthConstraint.isActive = true
|
||||
messageStatusImageViewHeightConstraint.isActive = true
|
||||
|
@ -217,11 +221,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
addSubview(replyButton)
|
||||
replyButton.addSubview(replyIconImageView)
|
||||
replyIconImageView.center(in: replyButton)
|
||||
replyButton.pin(.left, to: .right, of: bubbleView, withInset: Values.smallSpacing)
|
||||
replyButton.center(.vertical, in: bubbleView)
|
||||
replyButton.pin(.left, to: .right, of: snContentView, withInset: Values.smallSpacing)
|
||||
replyButton.center(.vertical, in: snContentView)
|
||||
|
||||
// Remaining constraints
|
||||
authorLabel.pin(.left, to: .left, of: bubbleView, withInset: VisibleMessageCell.authorLabelInset)
|
||||
authorLabel.pin(.left, to: .left, of: snContentView, withInset: VisibleMessageCell.authorLabelInset)
|
||||
}
|
||||
|
||||
override func setUpGestureRecognizers() {
|
||||
|
@ -270,15 +274,15 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
moderatorIconImageView.isHidden = (!cellViewModel.isSenderOpenGroupModerator || !cellViewModel.shouldShowProfile)
|
||||
|
||||
// Bubble view
|
||||
bubbleViewLeftConstraint1.isActive = (
|
||||
contentViewLeftConstraint1.isActive = (
|
||||
cellViewModel.variant == .standardIncoming ||
|
||||
cellViewModel.variant == .standardIncomingDeleted
|
||||
)
|
||||
bubbleViewLeftConstraint1.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : VisibleMessageCell.contactThreadHSpacing)
|
||||
bubbleViewLeftConstraint2.isActive = (cellViewModel.variant == .standardOutgoing)
|
||||
bubbleViewTopConstraint.constant = (cellViewModel.senderName == nil ? 0 : VisibleMessageCell.authorLabelBottomSpacing)
|
||||
bubbleViewRightConstraint1.isActive = (cellViewModel.variant == .standardOutgoing)
|
||||
bubbleViewRightConstraint2.isActive = (
|
||||
contentViewLeftConstraint1.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : VisibleMessageCell.contactThreadHSpacing)
|
||||
contentViewLeftConstraint2.isActive = (cellViewModel.variant == .standardOutgoing)
|
||||
contentViewTopConstraint.constant = (cellViewModel.senderName == nil ? 0 : VisibleMessageCell.authorLabelBottomSpacing)
|
||||
contentViewRightConstraint1.isActive = (cellViewModel.variant == .standardOutgoing)
|
||||
contentViewRightConstraint2.isActive = (
|
||||
cellViewModel.variant == .standardIncoming ||
|
||||
cellViewModel.variant == .standardIncomingDeleted
|
||||
)
|
||||
|
@ -406,23 +410,36 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
.messageBubble_incomingText
|
||||
)
|
||||
|
||||
snContentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
snContentView.alignment = (cellViewModel.variant == .standardOutgoing ?
|
||||
.trailing :
|
||||
.leading
|
||||
)
|
||||
|
||||
for subview in snContentView.arrangedSubviews {
|
||||
snContentView.removeArrangedSubview(subview)
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
for subview in bubbleView.subviews {
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
albumView = nil
|
||||
bodyTappableLabel = nil
|
||||
|
||||
// Handle the deleted state first (it's much simpler than the others)
|
||||
guard cellViewModel.variant != .standardIncomingDeleted else {
|
||||
let deletedMessageView: DeletedMessageView = DeletedMessageView(textColor: bodyLabelTextColor)
|
||||
snContentView.addSubview(deletedMessageView)
|
||||
deletedMessageView.pin(to: snContentView)
|
||||
bubbleView.addSubview(deletedMessageView)
|
||||
deletedMessageView.pin(to: bubbleView)
|
||||
snContentView.addArrangedSubview(bubbleBackgroundView)
|
||||
return
|
||||
}
|
||||
|
||||
// If it's an incoming media message and the thread isn't trusted then show the placeholder view
|
||||
if cellViewModel.cellType != .textOnlyMessage && cellViewModel.variant == .standardIncoming && !cellViewModel.threadIsTrusted {
|
||||
let mediaPlaceholderView = MediaPlaceholderView(cellViewModel: cellViewModel, textColor: bodyLabelTextColor)
|
||||
snContentView.addSubview(mediaPlaceholderView)
|
||||
mediaPlaceholderView.pin(to: snContentView)
|
||||
bubbleView.addSubview(mediaPlaceholderView)
|
||||
mediaPlaceholderView.pin(to: bubbleView)
|
||||
snContentView.addArrangedSubview(bubbleBackgroundView)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -448,8 +465,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
bodyLabelTextColor: bodyLabelTextColor,
|
||||
lastSearchText: lastSearchText
|
||||
)
|
||||
snContentView.addSubview(linkPreviewView)
|
||||
linkPreviewView.pin(to: snContentView)
|
||||
bubbleView.addSubview(linkPreviewView)
|
||||
linkPreviewView.pin(to: bubbleView, withInset: 0)
|
||||
snContentView.addArrangedSubview(bubbleBackgroundView)
|
||||
self.bodyTappableLabel = linkPreviewView.bodyTappableLabel
|
||||
|
||||
case .openGroupInvitation:
|
||||
|
@ -459,9 +477,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
textColor: bodyLabelTextColor,
|
||||
isOutgoing: (cellViewModel.variant == .standardOutgoing)
|
||||
)
|
||||
|
||||
snContentView.addSubview(openGroupInvitationView)
|
||||
openGroupInvitationView.pin(to: snContentView)
|
||||
bubbleView.addSubview(openGroupInvitationView)
|
||||
bubbleView.pin(to: openGroupInvitationView)
|
||||
snContentView.addArrangedSubview(bubbleBackgroundView)
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -504,15 +522,29 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
stackView.addArrangedSubview(bodyTappableLabel)
|
||||
|
||||
// Constraints
|
||||
snContentView.addSubview(stackView)
|
||||
stackView.pin(to: snContentView, withInset: inset)
|
||||
bubbleView.addSubview(stackView)
|
||||
stackView.pin(to: bubbleView, withInset: inset)
|
||||
snContentView.addArrangedSubview(bubbleBackgroundView)
|
||||
}
|
||||
|
||||
case .mediaMessage:
|
||||
// Stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.smallSpacing
|
||||
// Body text view
|
||||
if let body: String = cellViewModel.body, !body.isEmpty {
|
||||
let inset: CGFloat = 12
|
||||
let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset)
|
||||
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
|
||||
for: cellViewModel,
|
||||
with: maxWidth,
|
||||
textColor: bodyLabelTextColor,
|
||||
searchText: lastSearchText,
|
||||
delegate: self
|
||||
)
|
||||
|
||||
self.bodyTappableLabel = bodyTappableLabel
|
||||
bubbleView.addSubview(bodyTappableLabel)
|
||||
bodyTappableLabel.pin(to: bubbleView, withInset: inset)
|
||||
snContentView.addArrangedSubview(bubbleBackgroundView)
|
||||
}
|
||||
|
||||
// Album view
|
||||
let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth(for: cellViewModel)
|
||||
|
@ -529,29 +561,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
albumView.set(.width, to: size.width)
|
||||
albumView.set(.height, to: size.height)
|
||||
albumView.loadMedia()
|
||||
stackView.addArrangedSubview(albumView)
|
||||
|
||||
// Body text view
|
||||
if let body: String = cellViewModel.body, !body.isEmpty {
|
||||
let inset: CGFloat = 12
|
||||
let maxWidth: CGFloat = (size.width - (2 * inset))
|
||||
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
|
||||
for: cellViewModel,
|
||||
with: maxWidth,
|
||||
textColor: bodyLabelTextColor,
|
||||
searchText: lastSearchText,
|
||||
delegate: self
|
||||
)
|
||||
|
||||
self.bodyTappableLabel = bodyTappableLabel
|
||||
stackView.addArrangedSubview(UIView(wrapping: bodyTappableLabel, withInsets: UIEdgeInsets(top: 0, left: inset, bottom: inset, right: inset)))
|
||||
}
|
||||
snContentView.addArrangedSubview(albumView)
|
||||
|
||||
unloadContent = { albumView.unloadMedia() }
|
||||
|
||||
// Constraints
|
||||
snContentView.addSubview(stackView)
|
||||
stackView.pin(to: snContentView)
|
||||
|
||||
case .audio:
|
||||
guard let attachment: Attachment = cellViewModel.attachments?.first(where: { $0.isAudio }) else {
|
||||
return
|
||||
|
@ -565,9 +578,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
playbackRate: (playbackInfo?.playbackRate ?? 1),
|
||||
oldPlaybackRate: (playbackInfo?.oldPlaybackRate ?? 1)
|
||||
)
|
||||
|
||||
snContentView.addSubview(voiceMessageView)
|
||||
voiceMessageView.pin(to: snContentView)
|
||||
|
||||
bubbleView.addSubview(voiceMessageView)
|
||||
voiceMessageView.pin(to: bubbleView)
|
||||
snContentView.addArrangedSubview(bubbleBackgroundView)
|
||||
self.voiceMessageView = voiceMessageView
|
||||
|
||||
case .genericAttachment:
|
||||
|
@ -575,7 +589,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
|
||||
let inset: CGFloat = 12
|
||||
let maxWidth = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset)
|
||||
|
||||
|
||||
// Stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [])
|
||||
stackView.axis = .vertical
|
||||
|
@ -584,7 +598,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
// Document view
|
||||
let documentView = DocumentView(attachment: attachment, textColor: bodyLabelTextColor)
|
||||
stackView.addArrangedSubview(documentView)
|
||||
|
||||
|
||||
// Body text view
|
||||
if let body: String = cellViewModel.body, !body.isEmpty { // delegate should always be set at this point
|
||||
let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel(
|
||||
|
@ -599,9 +613,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
stackView.addArrangedSubview(bodyTappableLabel)
|
||||
}
|
||||
|
||||
// Constraints
|
||||
snContentView.addSubview(stackView)
|
||||
stackView.pin(to: snContentView, withInset: inset)
|
||||
bubbleView.addSubview(stackView)
|
||||
stackView.pin(to: bubbleView, withInset: inset)
|
||||
snContentView.addArrangedSubview(bubbleBackgroundView)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -652,7 +666,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
}
|
||||
|
||||
private func updateBubbleViewCorners() {
|
||||
let cornersToRound: UIRectCorner = getCornersToRound()
|
||||
let cornersToRound: UIRectCorner = .allCorners
|
||||
|
||||
bubbleBackgroundView.layer.cornerRadius = VisibleMessageCell.largeCornerRadius
|
||||
bubbleBackgroundView.layer.maskedCorners = getCornerMask(from: cornersToRound)
|
||||
|
@ -830,7 +844,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
delegate?.needsLayout(for: cellViewModel, expandingReactions: false)
|
||||
}
|
||||
}
|
||||
else if bubbleView.frame.contains(location) {
|
||||
else if snContentView.frame.contains(location) {
|
||||
delegate?.handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
@ -899,22 +913,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
|
||||
// MARK: - Convenience
|
||||
|
||||
private func getCornersToRound() -> UIRectCorner {
|
||||
guard viewModel?.isOnlyMessageInCluster == false else { return .allCorners }
|
||||
|
||||
let direction: Direction = (viewModel?.variant == .standardOutgoing ? .outgoing : .incoming)
|
||||
|
||||
switch (viewModel?.positionInCluster, direction) {
|
||||
case (.top, .outgoing): return [ .bottomLeft, .topLeft, .topRight ]
|
||||
case (.middle, .outgoing): return [ .bottomLeft, .topLeft ]
|
||||
case (.bottom, .outgoing): return [ .bottomRight, .bottomLeft, .topLeft ]
|
||||
case (.top, .incoming): return [ .topLeft, .topRight, .bottomRight ]
|
||||
case (.middle, .incoming): return [ .topRight, .bottomRight ]
|
||||
case (.bottom, .incoming): return [ .topRight, .bottomRight, .bottomLeft ]
|
||||
case (.none, _): return .allCorners
|
||||
}
|
||||
}
|
||||
|
||||
private func getCornerMask(from rectCorner: UIRectCorner) -> CACornerMask {
|
||||
guard !rectCorner.contains(.allCorners) else {
|
||||
return [ .layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner, .layerMinXMaxYCorner]
|
||||
|
@ -999,7 +997,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
cellViewModel.threadVariant == .openGroup ||
|
||||
cellViewModel.threadVariant == .closedGroup
|
||||
)
|
||||
let leftGutterSize = (isGroupThread ? gutterSize : contactThreadHSpacing)
|
||||
let leftGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing)
|
||||
|
||||
return (screen.width - leftGutterSize - gutterSize)
|
||||
|
||||
|
|
|
@ -281,7 +281,7 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
|
|||
title: MediaStrings.allMedia,
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).all_media",
|
||||
action: .push(showChevron: false) {
|
||||
return MediaGalleryViewModel.createTileViewController(
|
||||
return MediaGalleryViewModel.createAllMediaViewController(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
focusedAttachmentId: nil
|
||||
|
|
|
@ -94,6 +94,7 @@ final class ReactionListSheet: BaseVC {
|
|||
result.dataSource = self
|
||||
result.delegate = self
|
||||
result.register(view: UserCell.self)
|
||||
result.register(view: FooterCell.self)
|
||||
result.separatorStyle = .none
|
||||
result.themeBackgroundColor = .clear
|
||||
result.showsVerticalScrollIndicator = false
|
||||
|
@ -130,6 +131,15 @@ final class ReactionListSheet: BaseVC {
|
|||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
reactionContainer.scrollToItem(
|
||||
at: IndexPath(item: lastSelectedReactionIndex, section: 0),
|
||||
at: .centeredHorizontally,
|
||||
animated: false
|
||||
)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
|
@ -139,7 +149,9 @@ final class ReactionListSheet: BaseVC {
|
|||
private func setUpViewHierarchy() {
|
||||
view.addSubview(contentView)
|
||||
contentView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: view)
|
||||
contentView.set(.height, to: 440)
|
||||
// Emoji collectionView height + seleted emoji detail height + 5 × user cell height + footer cell height + bottom safe area inset
|
||||
let contentViewHeight: CGFloat = 100 + 5 * 65 + 45 + UIApplication.shared.keyWindow!.safeAreaInsets.bottom
|
||||
contentView.set(.height, to: contentViewHeight)
|
||||
populateContentView()
|
||||
}
|
||||
|
||||
|
@ -322,7 +334,19 @@ final class ReactionListSheet: BaseVC {
|
|||
deleteRowsAnimation: .none,
|
||||
insertRowsAnimation: .none,
|
||||
reloadRowsAnimation: .none,
|
||||
interrupt: { $0.changeCount > 100 }
|
||||
interrupt: { [weak self] changeset in
|
||||
/// This is the case where there were 6 reactors in total and locally we only have 5 including current user,
|
||||
/// and current user remove the reaction. There would be 4 reactors locally and we need to show more
|
||||
/// reactors cell at this moment. After update from sogs, we'll get the all 5 reactors and update the table
|
||||
/// with 5 reactors and not showing the more reactors cell.
|
||||
changeset.elementInserted.count == 1 && self?.selectedReactionUserList.count == 4 ||
|
||||
/// This is the case where there were 5 reactors without current user, and current user reacted. Before we got
|
||||
/// the update from sogs, we'll have 6 reactors locally and not showing the more reactors cell. After the update,
|
||||
/// we'll need to update the table and show 5 reactors with the more reactors cell.
|
||||
changeset.elementDeleted.count == 1 && self?.selectedReactionUserList.count == 6 ||
|
||||
/// To many changes to make
|
||||
changeset.changeCount > 100
|
||||
}
|
||||
) { [weak self] updatedData in
|
||||
self?.selectedReactionUserList = updatedData
|
||||
}
|
||||
|
@ -383,10 +407,23 @@ extension ReactionListSheet: UICollectionViewDataSource, UICollectionViewDelegat
|
|||
|
||||
extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return self.selectedReactionUserList.count
|
||||
let moreReactorCount = self.reactionSummaries[lastSelectedReactionIndex].number - self.selectedReactionUserList.count
|
||||
return moreReactorCount > 0 ? self.selectedReactionUserList.count + 1 : self.selectedReactionUserList.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard indexPath.row < self.selectedReactionUserList.count else {
|
||||
let moreReactorCount = self.reactionSummaries[lastSelectedReactionIndex].number - self.selectedReactionUserList.count
|
||||
let footerCell: FooterCell = tableView.dequeue(type: FooterCell.self, for: indexPath)
|
||||
footerCell.update(
|
||||
moreReactorCount: moreReactorCount,
|
||||
emoji: self.reactionSummaries[lastSelectedReactionIndex].emoji.rawValue
|
||||
)
|
||||
footerCell.selectionStyle = .none
|
||||
|
||||
return footerCell
|
||||
}
|
||||
|
||||
let cell: UserCell = tableView.dequeue(type: UserCell.self, for: indexPath)
|
||||
let cellViewModel: MessageViewModel.ReactionInfo = self.selectedReactionUserList[indexPath.row]
|
||||
cell.update(
|
||||
|
@ -407,6 +444,8 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource {
|
|||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
guard indexPath.row < self.selectedReactionUserList.count else { return }
|
||||
|
||||
let cellViewModel: MessageViewModel.ReactionInfo = self.selectedReactionUserList[indexPath.row]
|
||||
|
||||
guard
|
||||
|
@ -500,6 +539,44 @@ extension ReactionListSheet {
|
|||
snContentView.themeBorderColor = (isCurrentSelection ? .primary : .clear)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate final class FooterCell: UITableViewCell {
|
||||
|
||||
private lazy var label: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textAlignment = .center
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textColor = Colors.grey.withAlphaComponent(0.8)
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
// Background color
|
||||
backgroundColor = Colors.cellBackground
|
||||
|
||||
contentView.addSubview(label)
|
||||
label.pin(to: contentView)
|
||||
label.set(.height, to: 45)
|
||||
}
|
||||
|
||||
func update(moreReactorCount: Int, emoji: String) {
|
||||
label.text = (moreReactorCount == 1) ?
|
||||
String(format: "EMOJI_REACTS_MORE_REACTORS_ONE".localized(), "\(emoji)") :
|
||||
String(format: "EMOJI_REACTS_MORE_REACTORS_MUTIPLE".localized(), "\(moreReactorCount)" ,"\(emoji)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delegate
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -134,7 +134,7 @@ public class HomeViewModel {
|
|||
joinToPagedType: {
|
||||
let typingIndicator: TypedTableAlias<ThreadTypingIndicator> = TypedTableAlias()
|
||||
|
||||
return SQL("LEFT JOIN \(typingIndicator[.threadId]) = \(thread[.id])")
|
||||
return SQL("LEFT JOIN \(ThreadTypingIndicator.self) ON \(typingIndicator[.threadId]) = \(thread[.id])")
|
||||
}()
|
||||
),
|
||||
PagedData.ObservedChanges(
|
||||
|
|
|
@ -347,7 +347,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
switch section.model {
|
||||
case .threads:
|
||||
let threadId: String = section.elements[indexPath.row].threadId
|
||||
let delete = UIContextualAction(
|
||||
let delete: UIContextualAction = UIContextualAction(
|
||||
style: .destructive,
|
||||
title: "TXT_DELETE_TITLE".localized()
|
||||
) { [weak self] _, _, completionHandler in
|
||||
|
@ -355,8 +355,17 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
completionHandler(true)
|
||||
}
|
||||
delete.themeBackgroundColor = .conversationButton_swipeDestructive
|
||||
|
||||
let block: UIContextualAction = UIContextualAction(
|
||||
style: .normal,
|
||||
title: "BLOCK_LIST_BLOCK_BUTTON".localized()
|
||||
) { [weak self] _, _, completionHandler in
|
||||
self?.block(threadId)
|
||||
completionHandler(true)
|
||||
}
|
||||
block.themeBackgroundColor = .conversationButton_swipeSecondary
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [ delete ])
|
||||
return UISwipeActionsConfiguration(actions: [ delete, block ])
|
||||
|
||||
default: return nil
|
||||
}
|
||||
|
@ -388,19 +397,6 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
_ = try SessionThread
|
||||
.filter(ids: threadIds)
|
||||
.deleteAll(db)
|
||||
|
||||
try threadIds.forEach { threadId in
|
||||
_ = try Contact
|
||||
.fetchOrCreate(db, id: threadId)
|
||||
.with(
|
||||
isApproved: false,
|
||||
isBlocked: true
|
||||
)
|
||||
.saved(db)
|
||||
}
|
||||
|
||||
// Force a config sync
|
||||
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||
}
|
||||
})
|
||||
alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil))
|
||||
|
@ -416,6 +412,27 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
alertVC.addAction(UIAlertAction(
|
||||
title: "TXT_DELETE_TITLE".localized(),
|
||||
style: .destructive
|
||||
) { _ in
|
||||
Storage.shared.write { db in
|
||||
_ = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.deleteAll(db)
|
||||
}
|
||||
})
|
||||
|
||||
alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil))
|
||||
self.present(alertVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func block(_ threadId: String) {
|
||||
let alertVC: UIAlertController = UIAlertController(
|
||||
title: "MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON".localized(),
|
||||
message: nil,
|
||||
preferredStyle: .actionSheet
|
||||
)
|
||||
alertVC.addAction(UIAlertAction(
|
||||
title: "BLOCK_LIST_BLOCK_BUTTON".localized(),
|
||||
style: .destructive
|
||||
) { _ in
|
||||
Storage.shared.write { db in
|
||||
_ = try SessionThread
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import QuartzCore
|
||||
import GRDB
|
||||
import DifferenceKit
|
||||
import SessionUIKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
public class AllMediaViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
|
||||
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||
private var pages: [UIViewController] = []
|
||||
private var targetVCIndex: Int?
|
||||
|
||||
// MARK: Components
|
||||
private lazy var tabBar: TabBar = {
|
||||
let tabs = [
|
||||
TabBar.Tab(title: MediaStrings.media) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil)
|
||||
self.updateSelectButton(updatedData: self.mediaTitleViewController.viewModel.galleryData, inBatchSelectMode: self.mediaTitleViewController.isInBatchSelectMode)
|
||||
},
|
||||
TabBar.Tab(title: MediaStrings.document) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil)
|
||||
self.endSelectMode()
|
||||
self.navigationItem.rightBarButtonItem = nil
|
||||
}
|
||||
]
|
||||
return TabBar(tabs: tabs)
|
||||
}()
|
||||
|
||||
private var mediaTitleViewController: MediaTileViewController
|
||||
private var documentTitleViewController: DocumentTileViewController
|
||||
|
||||
init(mediaTitleViewController: MediaTileViewController, documentTitleViewController: DocumentTileViewController) {
|
||||
self.mediaTitleViewController = mediaTitleViewController
|
||||
self.documentTitleViewController = documentTitleViewController
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.mediaTitleViewController.delegate = self
|
||||
self.documentTitleViewController.delegate = self
|
||||
|
||||
addChild(self.mediaTitleViewController)
|
||||
addChild(self.documentTitleViewController)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: Lifecycle
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.themeBackgroundColor = .backgroundPrimary
|
||||
|
||||
// Add a custom back button if this is the only view controller
|
||||
if self.navigationController?.viewControllers.first == self {
|
||||
let backButton = OWSViewController.createOWSBackButton(withTarget: self, selector: #selector(didPressDismissButton))
|
||||
self.navigationItem.leftBarButtonItem = backButton
|
||||
}
|
||||
|
||||
ViewControllerUtilities.setUpDefaultSessionStyle(
|
||||
for: self,
|
||||
title: MediaStrings.allMedia,
|
||||
hasCustomBackButton: false
|
||||
)
|
||||
|
||||
// Set up page VC
|
||||
pages = [ mediaTitleViewController, documentTitleViewController ]
|
||||
pageVC.dataSource = self
|
||||
pageVC.delegate = self
|
||||
pageVC.setViewControllers([ mediaTitleViewController ], direction: .forward, animated: false, completion: nil)
|
||||
addChild(pageVC)
|
||||
|
||||
// Set up tab bar
|
||||
view.addSubview(tabBar)
|
||||
tabBar.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: view)
|
||||
// Set up page VC constraints
|
||||
let pageVCView = pageVC.view!
|
||||
view.addSubview(pageVCView)
|
||||
pageVCView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: view)
|
||||
pageVCView.pin(.top, to: .bottom, of: tabBar)
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||
guard let index = pages.firstIndex(of: viewController), index != 0 else { return nil }
|
||||
return pages[index - 1]
|
||||
}
|
||||
|
||||
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
||||
guard let index = pages.firstIndex(of: viewController), index != (pages.count - 1) else { return nil }
|
||||
return pages[index + 1]
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
||||
guard let targetVC = pendingViewControllers.first, let index = pages.firstIndex(of: targetVC) else { return }
|
||||
targetVCIndex = index
|
||||
}
|
||||
|
||||
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating isFinished: Bool, previousViewControllers: [UIViewController], transitionCompleted isCompleted: Bool) {
|
||||
guard isCompleted, let index = targetVCIndex else { return }
|
||||
tabBar.selectTab(at: index)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc public func didPressDismissButton() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Batch Selection
|
||||
@objc func didTapSelect(_ sender: Any) {
|
||||
self.mediaTitleViewController.didTapSelect(sender)
|
||||
|
||||
// Don't allow the user to leave mid-selection, so they realized they have
|
||||
// to cancel (lose) their selection if they leave.
|
||||
self.navigationItem.hidesBackButton = true
|
||||
}
|
||||
|
||||
@objc func didCancelSelect(_ sender: Any) {
|
||||
endSelectMode()
|
||||
}
|
||||
|
||||
func endSelectMode() {
|
||||
self.mediaTitleViewController.endSelectMode()
|
||||
self.navigationItem.hidesBackButton = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIDocumentInteractionControllerDelegate
|
||||
|
||||
extension AllMediaViewController: UIDocumentInteractionControllerDelegate {
|
||||
public func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DocumentTitleViewControllerDelegate
|
||||
|
||||
extension AllMediaViewController: DocumentTileViewControllerDelegate {
|
||||
public func share(fileUrl: URL) {
|
||||
let shareVC = UIActivityViewController(activityItems: [ fileUrl ], applicationActivities: nil)
|
||||
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = self.view
|
||||
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
|
||||
navigationController?.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
public func preview(fileUrl: URL) {
|
||||
let interactionController: UIDocumentInteractionController = UIDocumentInteractionController(url: fileUrl)
|
||||
interactionController.delegate = self
|
||||
interactionController.presentPreview(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DocumentTitleViewControllerDelegate
|
||||
|
||||
extension AllMediaViewController: MediaTileViewControllerDelegate {
|
||||
public func presentdetailViewController(_ detailViewController: UIViewController, animated: Bool) {
|
||||
self.present(detailViewController, animated: animated)
|
||||
}
|
||||
|
||||
public func updateSelectButton(updatedData: [MediaGalleryViewModel.SectionModel], inBatchSelectMode: Bool) {
|
||||
guard !updatedData.isEmpty else {
|
||||
self.navigationItem.rightBarButtonItem = nil
|
||||
return
|
||||
}
|
||||
|
||||
if inBatchSelectMode {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
barButtonSystemItem: .cancel,
|
||||
target: self,
|
||||
action: #selector(didCancelSelect)
|
||||
)
|
||||
}
|
||||
else {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: "BUTTON_SELECT".localized(),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(didTapSelect)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIViewControllerTransitioningDelegate
|
||||
|
||||
extension AllMediaViewController: UIViewControllerTransitioningDelegate {
|
||||
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
return self.mediaTitleViewController.animationController(forPresented: presented, presenting: presenting, source: source)
|
||||
}
|
||||
|
||||
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
return self.mediaTitleViewController.animationController(forDismissed: dismissed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - MediaPresentationContextProvider
|
||||
|
||||
extension AllMediaViewController: MediaPresentationContextProvider {
|
||||
func mediaPresentationContext(mediaItem: Media, in coordinateSpace: UICoordinateSpace) -> MediaPresentationContext? {
|
||||
return self.mediaTitleViewController.mediaPresentationContext(mediaItem: mediaItem, in: coordinateSpace)
|
||||
}
|
||||
|
||||
func snapshotOverlayView(in coordinateSpace: UICoordinateSpace) -> (UIView, CGRect)? {
|
||||
return self.mediaTitleViewController.snapshotOverlayView(in: coordinateSpace)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,503 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import QuartzCore
|
||||
import GRDB
|
||||
import DifferenceKit
|
||||
import SessionUIKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
public class DocumentTileViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
/// This should be larger than one screen size so we don't have to call it multiple times in rapid succession, but not
|
||||
/// so large that loading get's really chopping
|
||||
static let itemPageSize: Int = Int(11 * itemsPerPortraitRow)
|
||||
static let itemsPerPortraitRow: CGFloat = 4
|
||||
static let interItemSpacing: CGFloat = 2
|
||||
static let footerBarHeight: CGFloat = 40
|
||||
static let loadMoreHeaderHeight: CGFloat = 100
|
||||
|
||||
private let viewModel: MediaGalleryViewModel
|
||||
private var hasLoadedInitialData: Bool = false
|
||||
private var didFinishInitialLayout: Bool = false
|
||||
private var isAutoLoadingNextPage: Bool = false
|
||||
private var currentTargetOffset: CGPoint?
|
||||
|
||||
public var delegate: DocumentTileViewControllerDelegate?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(viewModel: MediaGalleryViewModel) {
|
||||
self.viewModel = viewModel
|
||||
Storage.shared.addObserver(viewModel.pagedDataObserver)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .allButUpsideDown
|
||||
}
|
||||
|
||||
lazy var tableView: UITableView = {
|
||||
let result = UITableView(frame: .zero, style: .grouped)
|
||||
result.backgroundColor = Colors.navigationBarBackground
|
||||
result.separatorStyle = .none
|
||||
result.showsVerticalScrollIndicator = false
|
||||
result.register(view: DocumentCell.self)
|
||||
result.delegate = self
|
||||
result.dataSource = self
|
||||
// Feels a bit weird to have content smashed all the way to the bottom edge.
|
||||
result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Add a custom back button if this is the only view controller
|
||||
if self.navigationController?.viewControllers.first == self {
|
||||
let backButton = OWSViewController.createOWSBackButton(withTarget: self, selector: #selector(didPressDismissButton))
|
||||
self.navigationItem.leftBarButtonItem = backButton
|
||||
}
|
||||
|
||||
ViewControllerUtilities.setUpDefaultSessionStyle(
|
||||
for: self,
|
||||
title: MediaStrings.document,
|
||||
hasCustomBackButton: false
|
||||
)
|
||||
|
||||
view.addSubview(self.tableView)
|
||||
tableView.autoPin(toEdgesOf: view)
|
||||
|
||||
// Notifications
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(applicationDidBecomeActive(_:)),
|
||||
name: UIApplication.didBecomeActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(applicationDidResignActive(_:)),
|
||||
name: UIApplication.didEnterBackgroundNotification, object: nil
|
||||
)
|
||||
}
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
startObservingChanges()
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.didFinishInitialLayout = true
|
||||
}
|
||||
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
stopObservingChanges()
|
||||
}
|
||||
|
||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||
startObservingChanges()
|
||||
}
|
||||
|
||||
@objc func applicationDidResignActive(_ notification: Notification) {
|
||||
stopObservingChanges()
|
||||
}
|
||||
|
||||
// MARK: - Updating
|
||||
|
||||
private func performInitialScrollIfNeeded() {
|
||||
// Ensure this hasn't run before and that we have data (The 'galleryData' will always
|
||||
// contain something as the 'empty' state is a section within 'galleryData')
|
||||
guard !self.didFinishInitialLayout && self.hasLoadedInitialData else { return }
|
||||
|
||||
// If we have a focused item then we want to scroll to it
|
||||
guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return }
|
||||
|
||||
Logger.debug("scrolling to focused item at indexPath: \(focusedIndexPath)")
|
||||
self.view.layoutIfNeeded()
|
||||
self.tableView.scrollToRow(at: focusedIndexPath, at: .middle, animated: false)
|
||||
|
||||
// Now that the data has loaded we need to check if either of the "load more" sections are
|
||||
// visible and trigger them if so
|
||||
//
|
||||
// Note: We do it this way as we want to trigger the load behaviour for the first section
|
||||
// if it has one before trying to trigger the load behaviour for the last section
|
||||
self.autoLoadNextPageIfNeeded()
|
||||
}
|
||||
|
||||
private func autoLoadNextPageIfNeeded() {
|
||||
guard !self.isAutoLoadingNextPage else { return }
|
||||
|
||||
self.isAutoLoadingNextPage = true
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + PagedData.autoLoadNextPageDelay) { [weak self] in
|
||||
self?.isAutoLoadingNextPage = false
|
||||
|
||||
// Note: We sort the headers as we want to prioritise loading newer pages over older ones
|
||||
let sortedVisibleIndexPaths: [IndexPath] = (self?.tableView.indexPathsForVisibleRows ?? []).sorted()
|
||||
|
||||
for headerIndexPath in sortedVisibleIndexPaths {
|
||||
let section: MediaGalleryViewModel.SectionModel? = self?.viewModel.galleryData[safe: headerIndexPath.section]
|
||||
|
||||
switch section?.model {
|
||||
case .loadNewer, .loadOlder:
|
||||
// Attachments are loaded in descending order so 'loadOlder' actually corresponds with
|
||||
// 'pageAfter' in this case
|
||||
self?.viewModel.pagedDataObserver?.load(section?.model == .loadOlder ?
|
||||
.pageAfter :
|
||||
.pageBefore
|
||||
)
|
||||
return
|
||||
|
||||
default: continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startObservingChanges() {
|
||||
// Start observing for data changes (will callback on the main thread)
|
||||
self.viewModel.onGalleryChange = { [weak self] updatedGalleryData in
|
||||
self?.handleUpdates(updatedGalleryData)
|
||||
}
|
||||
}
|
||||
|
||||
private func stopObservingChanges() {
|
||||
// Note: The 'pagedDataObserver' will continue to get changes but
|
||||
// we don't want to trigger any UI updates
|
||||
self.viewModel.onGalleryChange = nil
|
||||
}
|
||||
|
||||
private func handleUpdates(_ updatedGalleryData: [MediaGalleryViewModel.SectionModel]) {
|
||||
// Ensure the first load runs without animations (if we don't do this the cells will animate
|
||||
// in from a frame of CGRect.zero)
|
||||
guard hasLoadedInitialData else {
|
||||
self.hasLoadedInitialData = true
|
||||
self.viewModel.updateGalleryData(updatedGalleryData)
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.reloadData()
|
||||
self.performInitialScrollIfNeeded()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let isInsertingAtTop: Bool = {
|
||||
let oldFirstSectionIsLoadMore: Bool = (
|
||||
self.viewModel.galleryData.first?.model == .loadNewer ||
|
||||
self.viewModel.galleryData.first?.model == .loadOlder
|
||||
)
|
||||
let oldTargetSectionIndex: Int = (oldFirstSectionIsLoadMore ? 1 : 0)
|
||||
|
||||
guard
|
||||
let newTargetSectionIndex = updatedGalleryData
|
||||
.firstIndex(where: { $0.model == self.viewModel.galleryData[safe: oldTargetSectionIndex]?.model }),
|
||||
let oldFirstItem: MediaGalleryViewModel.Item = self.viewModel.galleryData[safe: oldTargetSectionIndex]?.elements.first,
|
||||
let newFirstItemIndex = updatedGalleryData[safe: newTargetSectionIndex]?.elements.firstIndex(of: oldFirstItem)
|
||||
else { return false }
|
||||
|
||||
return (newTargetSectionIndex > oldTargetSectionIndex || newFirstItemIndex > 0)
|
||||
}()
|
||||
|
||||
CATransaction.begin()
|
||||
|
||||
if isInsertingAtTop { CATransaction.setDisableActions(true) }
|
||||
|
||||
self.tableView.reload(
|
||||
using: StagedChangeset(source: self.viewModel.galleryData, target: updatedGalleryData),
|
||||
with: .automatic,
|
||||
interrupt: { $0.changeCount > MediaTileViewController.itemPageSize }
|
||||
) { [weak self] updatedData in
|
||||
self?.viewModel.updateGalleryData(updatedData)
|
||||
}
|
||||
|
||||
CATransaction.setCompletionBlock { [weak self] in
|
||||
// If one of the "load more" sections is still visible once the animation completes then
|
||||
// trigger another "load more" (after a small delay to minimize animation bugginess)
|
||||
self?.autoLoadNextPageIfNeeded()
|
||||
}
|
||||
CATransaction.commit()
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Interactions
|
||||
|
||||
@objc public func didPressDismissButton() {
|
||||
let presentedNavController: UINavigationController? = (self.presentingViewController as? UINavigationController)
|
||||
let mediaPageViewController: MediaPageViewController? = (
|
||||
(presentedNavController?.viewControllers.last as? MediaPageViewController) ??
|
||||
(self.presentingViewController as? MediaPageViewController)
|
||||
)
|
||||
|
||||
// If the album was presented from a 'MediaPageViewController' and it has no more data (ie.
|
||||
// all album items had been deleted) then dismiss to the screen before that one
|
||||
guard mediaPageViewController?.viewModel.albumData.isEmpty != true else {
|
||||
presentedNavController?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
return
|
||||
}
|
||||
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
public func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return self.viewModel.galleryData.count
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return self.viewModel.galleryData[section].elements.count
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: DocumentCell = tableView.dequeue(type: DocumentCell.self, for: indexPath)
|
||||
cell.update(with: self.viewModel.galleryData[indexPath.section].elements[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
let section: MediaGalleryViewModel.SectionModel = self.viewModel.galleryData[section]
|
||||
|
||||
switch section.model {
|
||||
case .emptyGallery, .loadOlder, .loadNewer:
|
||||
let headerView: DocumentStaticHeaderView = DocumentStaticHeaderView()
|
||||
headerView.configure(
|
||||
title: {
|
||||
switch section.model {
|
||||
case .emptyGallery: return "DOCUMENT_TILES_EMPTY_DOCUMENT".localized()
|
||||
case .loadOlder: return "DOCUMENT_TILES_LOADING_OLDER_LABEL".localized()
|
||||
case .loadNewer: return "DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL".localized()
|
||||
case .galleryMonth: return "" // Impossible case
|
||||
}
|
||||
}()
|
||||
)
|
||||
return headerView
|
||||
|
||||
case .galleryMonth(let date):
|
||||
let headerView: DocumentSectionHeaderView = DocumentSectionHeaderView()
|
||||
headerView.configure(title: date.localizedString)
|
||||
return headerView
|
||||
}
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
let section: MediaGalleryViewModel.SectionModel = self.viewModel.galleryData[section]
|
||||
|
||||
switch section.model {
|
||||
case .emptyGallery, .loadOlder, .loadNewer:
|
||||
return MediaTileViewController.loadMoreHeaderHeight
|
||||
|
||||
case .galleryMonth:
|
||||
return 50
|
||||
}
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: false)
|
||||
let attachment: Attachment = self.viewModel.galleryData[indexPath.section].elements[indexPath.row].attachment
|
||||
guard let originalFilePath: String = attachment.originalFilePath else { return }
|
||||
|
||||
let fileUrl: URL = URL(fileURLWithPath: originalFilePath)
|
||||
|
||||
// Open a preview of the document for text, pdf or microsoft files
|
||||
if
|
||||
attachment.isText ||
|
||||
attachment.isMicrosoftDoc ||
|
||||
attachment.contentType == OWSMimeTypeApplicationPdf
|
||||
{
|
||||
|
||||
delegate?.preview(fileUrl: fileUrl)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise share the file
|
||||
delegate?.share(fileUrl: fileUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View
|
||||
|
||||
class DocumentCell: UITableViewCell {
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
setUpViewHierarchy()
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
|
||||
setUpViewHierarchy()
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private static let iconImageViewSize: CGSize = CGSize(width: 31, height: 40)
|
||||
|
||||
private let iconImageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView(image: #imageLiteral(resourceName: "File").withRenderingMode(.alwaysTemplate))
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.tintColor = Colors.text
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let titleLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
result.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
result.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let detailLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
result.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.textColor = Colors.text
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
backgroundColor = Colors.cellBackground
|
||||
selectedBackgroundView = UIView()
|
||||
selectedBackgroundView?.backgroundColor = Colors.cellSelected
|
||||
|
||||
|
||||
contentView.addSubview(iconImageView)
|
||||
contentView.addSubview(titleLabel)
|
||||
contentView.addSubview(detailLabel)
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private func setupLayout() {
|
||||
NSLayoutConstraint.activate([
|
||||
contentView.heightAnchor.constraint(equalToConstant: 68),
|
||||
|
||||
iconImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: Values.mediumSpacing),
|
||||
iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
iconImageView.widthAnchor.constraint(equalToConstant: Self.iconImageViewSize.width),
|
||||
iconImageView.heightAnchor.constraint(equalToConstant: Self.iconImageViewSize.height),
|
||||
|
||||
titleLabel.leftAnchor.constraint(equalTo: iconImageView.rightAnchor, constant: Values.mediumSpacing),
|
||||
titleLabel.rightAnchor.constraint(lessThanOrEqualTo: contentView.rightAnchor, constant: -Values.mediumSpacing),
|
||||
titleLabel.topAnchor.constraint(equalTo: iconImageView.topAnchor),
|
||||
|
||||
detailLabel.leftAnchor.constraint(equalTo: iconImageView.rightAnchor, constant: Values.mediumSpacing),
|
||||
detailLabel.rightAnchor.constraint(lessThanOrEqualTo: contentView.rightAnchor, constant: -Values.mediumSpacing),
|
||||
detailLabel.bottomAnchor.constraint(equalTo: iconImageView.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
func update(with item: MediaGalleryViewModel.Item) {
|
||||
let attachment = item.attachment
|
||||
titleLabel.text = attachment.sourceFilename ?? "File"
|
||||
detailLabel.text = "\(OWSFormat.formatFileSize(UInt(attachment.byteCount)))"
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentSectionHeaderView: UIView {
|
||||
|
||||
let label: UILabel
|
||||
|
||||
override init(frame: CGRect) {
|
||||
label = UILabel()
|
||||
label.textColor = Colors.text
|
||||
|
||||
let blurEffect = UIBlurEffect(style: .dark)
|
||||
let blurEffectView = UIVisualEffectView(effect: blurEffect)
|
||||
|
||||
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = isLightMode ? Colors.cellBackground : UIColor.ows_black.withAlphaComponent(OWSNavigationBar.backgroundBlurMutingFactor)
|
||||
|
||||
self.addSubview(blurEffectView)
|
||||
self.addSubview(label)
|
||||
|
||||
blurEffectView.autoPinEdgesToSuperviewEdges()
|
||||
blurEffectView.isHidden = isLightMode
|
||||
label.autoPinEdge(toSuperviewMargin: .trailing)
|
||||
label.autoPinEdge(toSuperviewMargin: .leading)
|
||||
label.autoVCenterInSuperview()
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Unimplemented")
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
public func configure(title: String) {
|
||||
self.label.text = title
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentStaticHeaderView: UIView {
|
||||
|
||||
let label = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(label)
|
||||
|
||||
label.textColor = Colors.text
|
||||
label.textAlignment = .center
|
||||
label.numberOfLines = 0
|
||||
label.autoPinEdgesToSuperviewMargins(with: UIEdgeInsets(top: 0, leading: Values.largeSpacing, bottom: 0, trailing: Values.largeSpacing))
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "Unimplemented")
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
public func configure(title: String) {
|
||||
self.label.text = title
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DocumentTitleViewControllerDelegate
|
||||
|
||||
public protocol DocumentTileViewControllerDelegate: AnyObject {
|
||||
func share(fileUrl: URL)
|
||||
func preview(fileUrl: URL)
|
||||
}
|
|
@ -18,12 +18,19 @@ public class MediaGalleryViewModel {
|
|||
case loadNewer
|
||||
}
|
||||
|
||||
// MARK: Media type
|
||||
public enum MediaType {
|
||||
case media
|
||||
case document
|
||||
}
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
public let threadId: String
|
||||
public let threadVariant: SessionThread.Variant
|
||||
private var focusedAttachmentId: String?
|
||||
public private(set) var focusedIndexPath: IndexPath?
|
||||
public var mediaType: MediaType
|
||||
|
||||
/// This value is the current state of an album view
|
||||
private var cachedInteractionIdBefore: Atomic<[Int64: Int64]> = Atomic([:])
|
||||
|
@ -54,6 +61,7 @@ public class MediaGalleryViewModel {
|
|||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
isPagedData: Bool,
|
||||
mediaType: MediaType,
|
||||
pageSize: Int = 1,
|
||||
focusedAttachmentId: String? = nil,
|
||||
performInitialQuerySync: Bool = false
|
||||
|
@ -62,6 +70,7 @@ public class MediaGalleryViewModel {
|
|||
self.threadVariant = threadVariant
|
||||
self.focusedAttachmentId = focusedAttachmentId
|
||||
self.pagedDataObserver = nil
|
||||
self.mediaType = mediaType
|
||||
|
||||
guard isPagedData else { return }
|
||||
|
||||
|
@ -80,7 +89,7 @@ public class MediaGalleryViewModel {
|
|||
)
|
||||
],
|
||||
joinSQL: Item.joinSQL,
|
||||
filterSQL: Item.filterSQL(threadId: threadId),
|
||||
filterSQL: Item.filterSQL(threadId: threadId, mediaType: self.mediaType),
|
||||
orderSQL: Item.galleryOrderSQL,
|
||||
dataQuery: Item.baseQuery(orderSQL: Item.galleryOrderSQL),
|
||||
onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in
|
||||
|
@ -243,15 +252,27 @@ public class MediaGalleryViewModel {
|
|||
"""
|
||||
}()
|
||||
|
||||
fileprivate static func filterSQL(threadId: String) -> SQL {
|
||||
fileprivate static func filterSQL(threadId: String, mediaType: MediaType) -> SQL {
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let attachment: TypedTableAlias<Attachment> = TypedTableAlias()
|
||||
|
||||
return SQL("""
|
||||
\(attachment[.isVisualMedia]) = true AND
|
||||
\(attachment[.isValid]) = true AND
|
||||
\(interaction[.threadId]) = \(threadId)
|
||||
""")
|
||||
switch (mediaType) {
|
||||
case .media:
|
||||
return SQL("""
|
||||
\(attachment[.isVisualMedia]) = true AND
|
||||
\(attachment[.isValid]) = true AND
|
||||
\(interaction[.threadId]) = \(threadId)
|
||||
""")
|
||||
case .document:
|
||||
// FIXME: Remove "\(attachment[.sourceFilename]) <> 'session-audio-message'" when all platforms send the voice message properly
|
||||
return SQL("""
|
||||
\(attachment[.isVisualMedia]) = false AND
|
||||
\(attachment[.isValid]) = true AND
|
||||
\(interaction[.threadId]) = \(threadId) AND
|
||||
\(attachment[.variant]) = \(Attachment.Variant.standard) AND
|
||||
\(attachment[.sourceFilename]) <> 'session-audio-message'
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate static let galleryOrderSQL: SQL = {
|
||||
|
@ -509,7 +530,8 @@ public class MediaGalleryViewModel {
|
|||
let viewModel: MediaGalleryViewModel = MediaGalleryViewModel(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
isPagedData: false
|
||||
isPagedData: false,
|
||||
mediaType: .media
|
||||
)
|
||||
viewModel.loadAndCacheAlbumData(for: interactionId, in: threadId)
|
||||
viewModel.replaceAlbumObservation(toObservationFor: interactionId)
|
||||
|
@ -534,7 +556,7 @@ public class MediaGalleryViewModel {
|
|||
return navController
|
||||
}
|
||||
|
||||
public static func createTileViewController(
|
||||
public static func createMediaTileViewController(
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
focusedAttachmentId: String?,
|
||||
|
@ -544,6 +566,7 @@ public class MediaGalleryViewModel {
|
|||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
isPagedData: true,
|
||||
mediaType: .media,
|
||||
pageSize: MediaTileViewController.itemPageSize,
|
||||
focusedAttachmentId: focusedAttachmentId,
|
||||
performInitialQuerySync: performInitialQuerySync
|
||||
|
@ -553,6 +576,53 @@ public class MediaGalleryViewModel {
|
|||
viewModel: viewModel
|
||||
)
|
||||
}
|
||||
|
||||
public static func createDocumentTitleViewController(
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
focusedAttachmentId: String?,
|
||||
performInitialQuerySync: Bool = false
|
||||
) -> DocumentTileViewController {
|
||||
let viewModel: MediaGalleryViewModel = MediaGalleryViewModel(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
isPagedData: true,
|
||||
mediaType: .document,
|
||||
pageSize: MediaTileViewController.itemPageSize,
|
||||
focusedAttachmentId: focusedAttachmentId,
|
||||
performInitialQuerySync: performInitialQuerySync
|
||||
)
|
||||
|
||||
return DocumentTileViewController(
|
||||
viewModel: viewModel
|
||||
)
|
||||
}
|
||||
|
||||
public static func createAllMediaViewController(
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
focusedAttachmentId: String?,
|
||||
performInitialQuerySync: Bool = false
|
||||
) -> AllMediaViewController {
|
||||
let mediaTitleViewController = createMediaTileViewController(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
focusedAttachmentId: focusedAttachmentId,
|
||||
performInitialQuerySync: performInitialQuerySync
|
||||
)
|
||||
|
||||
let documentTitleViewController = createDocumentTitleViewController(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
focusedAttachmentId: focusedAttachmentId,
|
||||
performInitialQuerySync: performInitialQuerySync
|
||||
)
|
||||
|
||||
return AllMediaViewController(
|
||||
mediaTitleViewController: mediaTitleViewController,
|
||||
documentTitleViewController: documentTitleViewController
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Objective-C Support
|
||||
|
@ -564,7 +634,7 @@ public class SNMediaGallery: NSObject {
|
|||
@objc(pushTileViewWithSliderEnabledForThreadId:isClosedGroup:isOpenGroup:fromNavController:)
|
||||
static func pushTileView(threadId: String, isClosedGroup: Bool, isOpenGroup: Bool, fromNavController: OWSNavigationController) {
|
||||
fromNavController.pushViewController(
|
||||
MediaGalleryViewModel.createTileViewController(
|
||||
MediaGalleryViewModel.createAllMediaViewController(
|
||||
threadId: threadId,
|
||||
threadVariant: {
|
||||
if isClosedGroup { return .closedGroup }
|
||||
|
|
|
@ -458,7 +458,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
// MediaTileViewController then just pop/dismiss the screen
|
||||
guard
|
||||
let presentingNavController: UINavigationController = (self.presentingViewController as? UINavigationController),
|
||||
!(presentingNavController.viewControllers.last is MediaTileViewController)
|
||||
!(presentingNavController.viewControllers.last is AllMediaViewController)
|
||||
else {
|
||||
guard self.navigationController?.viewControllers.count == 1 else {
|
||||
self.navigationController?.popViewController(animated: true)
|
||||
|
@ -471,7 +471,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
|
||||
// Otherwise if we came via the conversation screen we need to push a new
|
||||
// instance of MediaTileViewController
|
||||
let tileViewController: MediaTileViewController = MediaGalleryViewModel.createTileViewController(
|
||||
let allMediaViewController: AllMediaViewController = MediaGalleryViewModel.createAllMediaViewController(
|
||||
threadId: self.viewModel.threadId,
|
||||
threadVariant: self.viewModel.threadVariant,
|
||||
focusedAttachmentId: currentItem.attachment.id,
|
||||
|
@ -479,9 +479,9 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
)
|
||||
|
||||
let navController: MediaGalleryNavigationController = MediaGalleryNavigationController()
|
||||
navController.viewControllers = [tileViewController]
|
||||
navController.viewControllers = [allMediaViewController]
|
||||
navController.modalPresentationStyle = .overFullScreen
|
||||
navController.transitioningDelegate = tileViewController
|
||||
navController.transitioningDelegate = allMediaViewController
|
||||
|
||||
self.navigationController?.present(navController, animated: true)
|
||||
}
|
||||
|
|
|
@ -17,12 +17,14 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
static let footerBarHeight: CGFloat = 40
|
||||
static let loadMoreHeaderHeight: CGFloat = 100
|
||||
|
||||
private let viewModel: MediaGalleryViewModel
|
||||
public let viewModel: MediaGalleryViewModel
|
||||
private var hasLoadedInitialData: Bool = false
|
||||
private var didFinishInitialLayout: Bool = false
|
||||
private var isAutoLoadingNextPage: Bool = false
|
||||
private var currentTargetOffset: CGPoint?
|
||||
|
||||
public var delegate: MediaTileViewControllerDelegate?
|
||||
|
||||
var isInBatchSelectMode = false {
|
||||
didSet {
|
||||
collectionView.allowsMultipleSelection = isInBatchSelectMode
|
||||
|
@ -199,8 +201,35 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return }
|
||||
|
||||
Logger.debug("scrolling to focused item at indexPath: \(focusedIndexPath)")
|
||||
|
||||
// Note: For some reason 'scrollToItem' doesn't always work properly so we need to manually
|
||||
// calculate what the offset should be to do the initial scroll
|
||||
self.view.layoutIfNeeded()
|
||||
self.collectionView.scrollToItem(at: focusedIndexPath, at: .centeredVertically, animated: false)
|
||||
|
||||
let availableHeight: CGFloat = {
|
||||
// Note: This height will be set before we have properly performed a layout and fitted
|
||||
// this screen within it's parent UIPagedViewController so we need to try to calculate
|
||||
// the "actual" height of the collection view
|
||||
var finalHeight: CGFloat = self.collectionView.frame.height
|
||||
|
||||
if let navController: UINavigationController = self.parent?.navigationController {
|
||||
finalHeight -= navController.navigationBar.frame.height
|
||||
finalHeight -= (UIApplication.shared.keyWindow?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0)
|
||||
}
|
||||
|
||||
if let tabBar: TabBar = self.parent?.parent?.view.subviews.first as? TabBar {
|
||||
finalHeight -= tabBar.frame.height
|
||||
}
|
||||
|
||||
return finalHeight
|
||||
}()
|
||||
let focusedRect: CGRect = (self.collectionView.layoutAttributesForItem(at: focusedIndexPath)?.frame)
|
||||
.defaulting(to: .zero)
|
||||
self.collectionView.contentOffset = CGPoint(
|
||||
x: 0,
|
||||
y: (focusedRect.origin.y - (availableHeight / 2) + (focusedRect.height / 2))
|
||||
)
|
||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
||||
|
||||
// Now that the data has loaded we need to check if either of the "load more" sections are
|
||||
// visible and trigger them if so
|
||||
|
@ -269,6 +298,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
guard hasLoadedInitialData else {
|
||||
self.hasLoadedInitialData = true
|
||||
self.viewModel.updateGalleryData(updatedGalleryData)
|
||||
self.updateSelectButton(updatedData: updatedGalleryData, inBatchSelectMode: isInBatchSelectMode)
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
self.collectionView.reloadData()
|
||||
|
@ -492,12 +522,6 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
// [ConversationSettingsView]
|
||||
// [ConversationView]
|
||||
//
|
||||
guard
|
||||
let viewControllers: [UIViewController] = self.navigationController?.viewControllers,
|
||||
viewControllers.count > 1,
|
||||
(viewControllers[viewControllers.count - 2] as? SettingsViewModelAccessible)?.viewModelType == ThreadSettingsViewModel.self
|
||||
else { return }
|
||||
|
||||
let detailViewController: UIViewController? = MediaGalleryViewModel.createDetailViewController(
|
||||
for: self.viewModel.threadId,
|
||||
threadVariant: self.viewModel.threadVariant,
|
||||
|
@ -508,7 +532,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
|
||||
guard let detailViewController: UIViewController = detailViewController else { return }
|
||||
|
||||
self.present(detailViewController, animated: true)
|
||||
delegate?.presentdetailViewController(detailViewController, animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -590,26 +614,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
}
|
||||
|
||||
func updateSelectButton(updatedData: [MediaGalleryViewModel.SectionModel], inBatchSelectMode: Bool) {
|
||||
guard !updatedData.isEmpty else {
|
||||
self.navigationItem.rightBarButtonItem = nil
|
||||
return
|
||||
}
|
||||
|
||||
if inBatchSelectMode {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
barButtonSystemItem: .cancel,
|
||||
target: self,
|
||||
action: #selector(didCancelSelect)
|
||||
)
|
||||
}
|
||||
else {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: "BUTTON_SELECT".localized(),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(didTapSelect)
|
||||
)
|
||||
}
|
||||
delegate?.updateSelectButton(updatedData: updatedData, inBatchSelectMode: inBatchSelectMode)
|
||||
}
|
||||
|
||||
@objc func didTapSelect(_ sender: Any) {
|
||||
|
@ -624,13 +629,6 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
// Ensure toolbar doesn't cover bottom row.
|
||||
self?.collectionView.contentInset.bottom += MediaTileViewController.footerBarHeight
|
||||
}, completion: nil)
|
||||
|
||||
// disabled until at least one item is selected
|
||||
self.deleteButton.isEnabled = false
|
||||
|
||||
// Don't allow the user to leave mid-selection, so they realized they have
|
||||
// to cancel (lose) their selection if they leave.
|
||||
self.navigationItem.hidesBackButton = true
|
||||
}
|
||||
|
||||
@objc func didCancelSelect(_ sender: Any) {
|
||||
|
@ -650,8 +648,6 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
|
|||
self?.collectionView.contentInset.bottom -= MediaTileViewController.footerBarHeight
|
||||
}, completion: nil)
|
||||
|
||||
self.navigationItem.hidesBackButton = false
|
||||
|
||||
// Deselect any selected
|
||||
collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false)}
|
||||
}
|
||||
|
@ -863,7 +859,12 @@ class GalleryGridCellItem: PhotoGridItem {
|
|||
|
||||
extension MediaTileViewController: UIViewControllerTransitioningDelegate {
|
||||
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
guard self == presented || self.navigationController == presented else { return nil }
|
||||
guard
|
||||
self == presented ||
|
||||
self.navigationController == presented ||
|
||||
self.parent == presented ||
|
||||
self.parent?.navigationController == presented
|
||||
else { return nil }
|
||||
guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return nil }
|
||||
|
||||
return MediaDismissAnimationController(
|
||||
|
@ -872,7 +873,12 @@ extension MediaTileViewController: UIViewControllerTransitioningDelegate {
|
|||
}
|
||||
|
||||
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
guard self == dismissed || self.navigationController == dismissed else { return nil }
|
||||
guard
|
||||
self == dismissed ||
|
||||
self.navigationController == dismissed ||
|
||||
self.parent == dismissed ||
|
||||
self.parent?.navigationController == dismissed
|
||||
else { return nil }
|
||||
guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return nil }
|
||||
|
||||
return MediaZoomAnimationController(
|
||||
|
@ -923,3 +929,10 @@ extension MediaTileViewController: MediaPresentationContextProvider {
|
|||
return self.navigationController?.navigationBar.generateSnapshot(in: coordinateSpace)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MediaTileViewControllerDelegate
|
||||
|
||||
public protocol MediaTileViewControllerDelegate: AnyObject {
|
||||
func presentdetailViewController(_ detailViewController: UIViewController, animated: Bool)
|
||||
func updateSelectButton(updatedData: [MediaGalleryViewModel.SectionModel], inBatchSelectMode: Bool)
|
||||
}
|
||||
|
|
|
@ -79,6 +79,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
}
|
||||
)
|
||||
|
||||
SNAppearance.switchToSessionAppearance()
|
||||
|
||||
if Environment.shared?.callManager.wrappedValue?.currentCall == nil {
|
||||
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing")
|
||||
}
|
||||
|
||||
// No point continuing if we are running tests
|
||||
guard !CurrentAppContext().isRunningTests else { return true }
|
||||
|
||||
|
@ -131,21 +137,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
|
||||
// NOTE: Fix an edge case where user taps on the callkit notification
|
||||
// but answers the call on another device
|
||||
stopPollers(shouldStopUserPoller: !self.hasIncomingCallWaiting())
|
||||
stopPollers(shouldStopUserPoller: !self.hasCallOngoing())
|
||||
|
||||
// Stop all jobs except for message sending and when completed suspend the database
|
||||
JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend) {
|
||||
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
||||
if !self.hasCallOngoing() {
|
||||
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
|
||||
Logger.info("applicationDidReceiveMemoryWarning")
|
||||
}
|
||||
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
DDLog.flushLog()
|
||||
|
||||
|
||||
stopPollers()
|
||||
}
|
||||
|
||||
|
@ -638,6 +646,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
return !call.hasStartedConnecting
|
||||
}
|
||||
|
||||
func hasCallOngoing() -> Bool {
|
||||
guard let call = AppEnvironment.shared.callManager.currentCall else { return false }
|
||||
|
||||
return !call.hasEnded
|
||||
}
|
||||
|
||||
func handleAppActivatedWithOngoingCallIfNeeded() {
|
||||
guard
|
||||
let call: SessionCall = (AppEnvironment.shared.callManager.currentCall as? SessionCall),
|
||||
|
|
Binary file not shown.
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Möchten Sie wirklich alle Nachrichten löschen?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Fehler";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Error";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Fallo";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "خطاء";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Oletko varma että haluat poistaa kaikki viestipyynnöt?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Poista";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Oletko varma että haluat poistaa tämän viestipyynnön?";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Viestin lähettäminen tälle henkilölle hyväksyy automaattisesti viestipyynnön.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Viestipyyntösi hyväksyttiin.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "Sinulla on uusi viestipyyntö";
|
||||
"TXT_HIDE_TITLE" = "Piilota";
|
||||
"TXT_DELETE_ACCEPT" = "Hyväksy";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Virhe";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Avoin ryhmä";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Yksityisviesti";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Êtes-vous sûr de vouloir supprimer toutes les demandes de messages ?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Effacer";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Êtes-vous sûr de vouloir supprimer cette demande de message ?";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Envoyer un message à cet utilisateur acceptera automatiquement sa demande de message.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Votre demande de message a été réceptionnée.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "Vous avez une nouvelle demande de message";
|
||||
"TXT_HIDE_TITLE" = "Masquer";
|
||||
"TXT_DELETE_ACCEPT" = "Accepter";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Erreur";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Groupe public";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Message privé";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Error";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Greška";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Galat";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Eliminare veramente tutte le richieste di messaggio?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Cancella";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Sei sicuro di voler eliminare questa richiesta di messaggio?";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "L'invio di un messaggio a questo utente accetterà automaticamente la richiesta di messaggio.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "La tua richiesta di messaggio è stata accettata.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "Hai una nuova richiesta di messaggio";
|
||||
"TXT_HIDE_TITLE" = "Nascondi";
|
||||
"TXT_DELETE_ACCEPT" = "Accetta";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Errore";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Gruppo Aperto";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Messaggio Privato";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "本当に全てのリクエストを消去しますか?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "消去";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "本当にこのリクエストを削除しますか?";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "このユーザーにメッセージを送信すると、自動的にリクエストが承認されます。";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "リクエストが承認されました";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "新しいリクエストがあります";
|
||||
"TXT_HIDE_TITLE" = "非表示";
|
||||
"TXT_DELETE_ACCEPT" = "許可";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "エラー";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "公開グループ";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "ダイレクトメッセージ";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Fout";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Czy na pewno chcesz wyczyścić wszystkie żądania wiadomości?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Wyczyść";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Czy na pewno chcesz usunąć to żądanie wiadomości?";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Wysyłanie wiadomości do tego użytkownika automatycznie zaakceptuje ich żądanie wiadomości.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Twoje żądanie wiadomości zostało zaakceptowane.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "Masz nowe żądanie wiadomości";
|
||||
"TXT_HIDE_TITLE" = "Ukryj";
|
||||
"TXT_DELETE_ACCEPT" = "Zaakceptuj";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Błąd";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Otwórz grupę";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Wiadomość prywatna";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Erro";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Вы уверены, что хотите очистить все запросы сообщений?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Очистить";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Вы уверены, что хотите удалить это сообщение?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Скрыть";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Ошибка";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Error";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Naozaj chcete vymazať všetky žiadosti o správu?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Vymazať";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Naozaj chcete vymazať túto žiadosť o správu?";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Poslanie správy tomuto používateľovi automaticky príjme ich žiadosť o správu.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Vaša žiadosť o správu bola prijatá.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "Máte novú žiadosť o správu";
|
||||
"TXT_HIDE_TITLE" = "Skryť";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Error";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Fel";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "ข้อผิดพลาด";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Error";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request.";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID.";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request";
|
||||
"TXT_HIDE_TITLE" = "Hide";
|
||||
"TXT_DELETE_ACCEPT" = "Accept";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "Error";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -632,11 +632,14 @@
|
|||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "您确定要清除所有消息请求吗?";
|
||||
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "清除";
|
||||
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "您确定要删除此消息请求吗?";
|
||||
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?";
|
||||
"MESSAGE_REQUESTS_INFO" = "发送消息给此用户将自动接受他们的消息请求。";
|
||||
"MESSAGE_REQUESTS_ACCEPTED" = "您的消息请求已被接受。";
|
||||
"MESSAGE_REQUESTS_NOTIFICATION" = "您有一个新的消息请求";
|
||||
"TXT_HIDE_TITLE" = "隐藏";
|
||||
"TXT_DELETE_ACCEPT" = "接受";
|
||||
"TXT_DECLINE_TITLE" = "Decline";
|
||||
"TXT_BLOCK_USER_TITLE" = "Block User";
|
||||
"ALERT_ERROR_TITLE" = "错误";
|
||||
"NEW_CONVERSATION_MENU_OPEN_GROUP" = "公开群组";
|
||||
"NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "私信";
|
||||
|
@ -678,6 +681,11 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…";
|
||||
"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…";
|
||||
/* The name for the emoji category 'Activities' */
|
||||
"EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities";
|
||||
/* The name for the emoji category 'Animals & Nature' */
|
||||
|
@ -696,7 +704,9 @@
|
|||
"EMOJI_CATEGORY_SYMBOLS_NAME" = "Symbols";
|
||||
/* The name for the emoji category 'Travel & Places' */
|
||||
"EMOJI_CATEGORY_TRAVEL_NAME" = "Travel & Places";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@";
|
||||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"PRIVACY_TITLE" = "Privacy";
|
||||
"PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security";
|
||||
"PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session";
|
||||
|
|
|
@ -293,21 +293,25 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
AppNotificationUserInfoKey.threadId: thread.id
|
||||
]
|
||||
|
||||
let notificationTitle: String = interaction.previewText(db)
|
||||
let threadName: String = SessionThread.displayName(
|
||||
threadId: thread.id,
|
||||
variant: thread.variant,
|
||||
closedGroupName: nil, // Not supported
|
||||
openGroupName: nil // Not supported
|
||||
)
|
||||
var notificationBody: String?
|
||||
let notificationTitle: String = "Session"
|
||||
let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
|
||||
let notificationBody: String? = {
|
||||
switch messageInfo.state {
|
||||
case .permissionDenied:
|
||||
return String(
|
||||
format: "modal_call_missed_tips_explanation".localized(),
|
||||
senderName
|
||||
)
|
||||
case .missed:
|
||||
return String(
|
||||
format: "call_missed".localized(),
|
||||
senderName
|
||||
)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
if messageInfo.state == .permissionDenied {
|
||||
notificationBody = String(
|
||||
format: "modal_call_missed_tips_explanation".localized(),
|
||||
threadName
|
||||
)
|
||||
}
|
||||
let fallbackSound: Preferences.Sound = db[.defaultNotificationSound]
|
||||
.defaulting(to: Preferences.Sound.defaultNotificationSound)
|
||||
|
||||
|
@ -325,7 +329,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
previewType: previewType,
|
||||
sound: sound,
|
||||
threadVariant: thread.variant,
|
||||
threadName: threadName,
|
||||
threadName: senderName,
|
||||
replacingIdentifier: UUID().uuidString
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import Foundation
|
|||
import PromiseKit
|
||||
import PushKit
|
||||
import SignalUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
import GRDB
|
||||
|
||||
public enum PushRegistrationError: Error {
|
||||
case assertionError(description: String)
|
||||
|
@ -251,6 +251,9 @@ public enum PushRegistrationError: Error {
|
|||
return
|
||||
}
|
||||
|
||||
// Resume database
|
||||
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
|
||||
|
||||
let maybeCall: SessionCall? = Storage.shared.write { db in
|
||||
let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(
|
||||
state: (caller == getUserHexEncodedPublicKey(db) ?
|
||||
|
@ -259,7 +262,13 @@ public enum PushRegistrationError: Error {
|
|||
)
|
||||
)
|
||||
|
||||
guard let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) else { return nil }
|
||||
let messageInfoString: String? = {
|
||||
if let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) {
|
||||
return String(data: messageInfoData, encoding: .utf8)
|
||||
} else {
|
||||
return "Incoming call." // TODO: We can do better here.
|
||||
}
|
||||
}()
|
||||
|
||||
let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer)
|
||||
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: caller, variant: .contact)
|
||||
|
@ -269,7 +278,7 @@ public enum PushRegistrationError: Error {
|
|||
threadId: thread.id,
|
||||
authorId: caller,
|
||||
variant: .infoCall,
|
||||
body: String(data: messageInfoData, encoding: .utf8),
|
||||
body: messageInfoString,
|
||||
timestampMs: timestampMs
|
||||
).inserted(db)
|
||||
call.callInteractionId = interaction.id
|
||||
|
|
|
@ -74,7 +74,7 @@ final class LandingVC: BaseVC {
|
|||
linkButtonContainer.addSubview(linkButton)
|
||||
linkButton.center(.horizontal, in: linkButtonContainer)
|
||||
|
||||
let isIPhoneX = (UIApplication.shared.keyWindow!.safeAreaInsets.bottom > 0)
|
||||
let isIPhoneX = ((UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0) > 0)
|
||||
linkButton.centerYAnchor.constraint(equalTo: linkButtonContainer.centerYAnchor, constant: isIPhoneX ? -4 : 0).isActive = true
|
||||
|
||||
// Button stack view
|
||||
|
|
|
@ -9,6 +9,7 @@ public protocol CurrentCallProtocol {
|
|||
var callId: UUID { get }
|
||||
var webRTCSession: WebRTCSession { get }
|
||||
var hasStartedConnecting: Bool { get set }
|
||||
var hasEnded: Bool { get set }
|
||||
|
||||
func updateCallMessage(mode: EndCallMode)
|
||||
func didReceiveRemoteSDP(sdp: RTCSessionDescription)
|
||||
|
|
|
@ -348,10 +348,14 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
).insert(db)
|
||||
|
||||
case .closedGroup:
|
||||
guard
|
||||
let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db),
|
||||
let members: [GroupMember] = try? closedGroup.members.fetchAll(db)
|
||||
else {
|
||||
let closedGroupMemberIds: Set<String> = (try? GroupMember
|
||||
.select(.profileId)
|
||||
.filter(GroupMember.Columns.groupId == thread.id)
|
||||
.asRequest(of: String.self)
|
||||
.fetchSet(db))
|
||||
.defaulting(to: [])
|
||||
|
||||
guard !closedGroupMemberIds.isEmpty else {
|
||||
SNLog("Inserted an interaction but couldn't find it's associated thread members")
|
||||
return
|
||||
}
|
||||
|
@ -359,12 +363,12 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
// Exclude the current user when creating recipient states (as they will never
|
||||
// receive the message resulting in the message getting flagged as failed)
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
try members
|
||||
.filter { member -> Bool in member.profileId != userPublicKey }
|
||||
.forEach { member in
|
||||
try closedGroupMemberIds
|
||||
.filter { memberId -> Bool in memberId != userPublicKey }
|
||||
.forEach { memberId in
|
||||
try RecipientState(
|
||||
interactionId: interactionId,
|
||||
recipientId: member.profileId,
|
||||
recipientId: memberId,
|
||||
state: .sending
|
||||
).insert(db)
|
||||
}
|
||||
|
|
|
@ -353,6 +353,7 @@ public extension Message {
|
|||
_ db: Database,
|
||||
openGroupId: String,
|
||||
message: OpenGroupAPI.Message,
|
||||
associatedPendingChanges: [OpenGroupAPI.PendingChange],
|
||||
dependencies: SMKDependencies = SMKDependencies()
|
||||
) -> [Reaction] {
|
||||
var results: [Reaction] = []
|
||||
|
@ -364,16 +365,59 @@ public extension Message {
|
|||
threadVariant: .openGroup
|
||||
)
|
||||
for (encodedEmoji, rawReaction) in reactions {
|
||||
if let emoji = encodedEmoji.removingPercentEncoding,
|
||||
if let decodedEmoji = encodedEmoji.removingPercentEncoding,
|
||||
rawReaction.count > 0,
|
||||
let reactors = rawReaction.reactors
|
||||
{
|
||||
// Decide whether we need to ignore all reactions
|
||||
let pendingChangeRemoveAllReaction: Bool = associatedPendingChanges.contains { pendingChange in
|
||||
if case .reaction(_, let emoji, let action) = pendingChange.metadata {
|
||||
return emoji == decodedEmoji && action == .removeAll
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Decide whether we need to add an extra reaction from current user
|
||||
let pendingChangeSelfReaction: Bool? = {
|
||||
// Find the newest 'PendingChange' entry with a matching emoji, if one exists, and
|
||||
// set the "self reaction" value based on it's action
|
||||
let maybePendingChange: OpenGroupAPI.PendingChange? = associatedPendingChanges
|
||||
.sorted(by: { lhs, rhs -> Bool in (lhs.seqNo ?? Int64.max) >= (rhs.seqNo ?? Int64.max) })
|
||||
.first { pendingChange in
|
||||
if case .reaction(_, let emoji, _) = pendingChange.metadata {
|
||||
return emoji == decodedEmoji
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// If there is no pending change for this reaction then return nil
|
||||
guard
|
||||
let pendingChange: OpenGroupAPI.PendingChange = maybePendingChange,
|
||||
case .reaction(_, _, let action) = pendingChange.metadata
|
||||
else { return nil }
|
||||
|
||||
// Otherwise add/remove accordingly
|
||||
return action == .add
|
||||
}()
|
||||
let shouldAddSelfReaction: Bool = (
|
||||
pendingChangeSelfReaction ??
|
||||
((rawReaction.you || reactors.contains(userPublicKey)) && !pendingChangeRemoveAllReaction)
|
||||
)
|
||||
|
||||
let count: Int64 = rawReaction.you ? rawReaction.count - 1 : rawReaction.count
|
||||
|
||||
let timestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000)))
|
||||
let maxLength: Int = shouldAddSelfReaction ? 4 : 5
|
||||
let desiredReactorIds: [String] = reactors
|
||||
.filter { $0 != blindedUserPublicKey }
|
||||
.filter { $0 != blindedUserPublicKey && $0 != userPublicKey } // Remove current user for now, will add back if needed
|
||||
.prefix(maxLength)
|
||||
.map{ $0 }
|
||||
|
||||
results = results
|
||||
.appending( // Add the first reaction (with the count)
|
||||
pendingChangeRemoveAllReaction ?
|
||||
nil :
|
||||
desiredReactorIds.first
|
||||
.map { reactor in
|
||||
Reaction(
|
||||
|
@ -381,14 +425,14 @@ public extension Message {
|
|||
serverHash: nil,
|
||||
timestampMs: timestampMs,
|
||||
authorId: reactor,
|
||||
emoji: emoji,
|
||||
count: rawReaction.count,
|
||||
emoji: decodedEmoji,
|
||||
count: count,
|
||||
sortId: rawReaction.index
|
||||
)
|
||||
}
|
||||
)
|
||||
.appending( // Add all other reactions
|
||||
contentsOf: desiredReactorIds.count <= 1 ?
|
||||
contentsOf: desiredReactorIds.count <= 1 || pendingChangeRemoveAllReaction ?
|
||||
[] :
|
||||
desiredReactorIds
|
||||
.suffix(from: 1)
|
||||
|
@ -398,22 +442,22 @@ public extension Message {
|
|||
serverHash: nil,
|
||||
timestampMs: timestampMs,
|
||||
authorId: reactor,
|
||||
emoji: emoji,
|
||||
emoji: decodedEmoji,
|
||||
count: 0, // Only want this on the first reaction
|
||||
sortId: rawReaction.index
|
||||
)
|
||||
}
|
||||
)
|
||||
.appending( // Add the current user reaction (if applicable and not already included)
|
||||
!rawReaction.you || reactors.contains(userPublicKey) ?
|
||||
!shouldAddSelfReaction ?
|
||||
nil :
|
||||
Reaction(
|
||||
interactionId: message.id,
|
||||
serverHash: nil,
|
||||
timestampMs: timestampMs,
|
||||
authorId: userPublicKey,
|
||||
emoji: emoji,
|
||||
count: (desiredReactorIds.isEmpty ? rawReaction.count : 0),
|
||||
emoji: decodedEmoji,
|
||||
count: 1,
|
||||
sortId: rawReaction.index
|
||||
)
|
||||
)
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension OpenGroupAPI {
|
||||
public struct PendingChange: Equatable {
|
||||
public enum ChangeType {
|
||||
case reaction
|
||||
}
|
||||
|
||||
public enum ReactAction: Equatable {
|
||||
case add
|
||||
case remove
|
||||
case removeAll
|
||||
}
|
||||
|
||||
enum Metadata {
|
||||
case reaction(messageId: Int64, emoji: String, action: ReactAction)
|
||||
}
|
||||
|
||||
let server: String
|
||||
let room: String
|
||||
let changeType: ChangeType
|
||||
var seqNo: Int64?
|
||||
let metadata: Metadata
|
||||
|
||||
public static func == (lhs: OpenGroupAPI.PendingChange, rhs: OpenGroupAPI.PendingChange) -> Bool {
|
||||
guard lhs.server == rhs.server &&
|
||||
lhs.room == rhs.room &&
|
||||
lhs.changeType == rhs.changeType &&
|
||||
lhs.seqNo == rhs.seqNo
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch lhs.changeType {
|
||||
case .reaction:
|
||||
if case .reaction(let lhsMessageId, let lhsEmoji, let lhsAction) = lhs.metadata,
|
||||
case .reaction(let rhsMessageId, let rhsEmoji, let rhsAction) = rhs.metadata {
|
||||
return lhsMessageId == rhsMessageId && lhsEmoji == rhsEmoji && lhsAction == rhsAction
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension OpenGroupAPI {
|
||||
public struct ReactionAddResponse: Codable, Equatable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case added
|
||||
case seqNo = "seqno"
|
||||
}
|
||||
|
||||
/// This field indicates whether the reaction was added (true) or already present (false).
|
||||
public let added: Bool
|
||||
|
||||
/// The seqNo after the reaction is added.
|
||||
public let seqNo: Int64?
|
||||
}
|
||||
|
||||
public struct ReactionRemoveResponse: Codable, Equatable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case removed
|
||||
case seqNo = "seqno"
|
||||
}
|
||||
|
||||
/// This field indicates whether the reaction was removed (true) or was not present to begin with (false).
|
||||
public let removed: Bool
|
||||
|
||||
/// The seqNo after the reaction is removed.
|
||||
public let seqNo: Int64?
|
||||
}
|
||||
|
||||
public struct ReactionRemoveAllResponse: Codable, Equatable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case removed
|
||||
case seqNo = "seqno"
|
||||
}
|
||||
|
||||
/// This field shows the total number of reactions that were deleted.
|
||||
public let removed: Int64
|
||||
|
||||
/// The seqNo after the reactions is all removed.
|
||||
public let seqNo: Int64?
|
||||
}
|
||||
}
|
|
@ -99,7 +99,7 @@ public enum OpenGroupAPI {
|
|||
),
|
||||
queryParameters: [
|
||||
.updateTypes: UpdateTypes.reaction.rawValue,
|
||||
.reactors: "20"
|
||||
.reactors: "5"
|
||||
]
|
||||
),
|
||||
responseType: [Failable<Message>].self
|
||||
|
@ -701,7 +701,7 @@ public enum OpenGroupAPI {
|
|||
in roomToken: String,
|
||||
on server: String,
|
||||
using dependencies: SMKDependencies = SMKDependencies()
|
||||
) -> Promise<OnionRequestResponseInfoType> {
|
||||
) -> Promise<(OnionRequestResponseInfoType, ReactionAddResponse)> {
|
||||
/// URL(String:) won't convert raw emojis, so need to do a little encoding here.
|
||||
/// The raw emoji will come back when calling url.path
|
||||
guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
|
||||
|
@ -718,7 +718,7 @@ public enum OpenGroupAPI {
|
|||
),
|
||||
using: dependencies
|
||||
)
|
||||
.map { responseInfo, _ in responseInfo }
|
||||
.decoded(as: ReactionAddResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
|
||||
}
|
||||
|
||||
public static func reactionDelete(
|
||||
|
@ -728,7 +728,7 @@ public enum OpenGroupAPI {
|
|||
in roomToken: String,
|
||||
on server: String,
|
||||
using dependencies: SMKDependencies = SMKDependencies()
|
||||
) -> Promise<OnionRequestResponseInfoType> {
|
||||
) -> Promise<(OnionRequestResponseInfoType, ReactionRemoveResponse)> {
|
||||
/// URL(String:) won't convert raw emojis, so need to do a little encoding here.
|
||||
/// The raw emoji will come back when calling url.path
|
||||
guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
|
||||
|
@ -745,7 +745,7 @@ public enum OpenGroupAPI {
|
|||
),
|
||||
using: dependencies
|
||||
)
|
||||
.map { responseInfo, _ in responseInfo }
|
||||
.decoded(as: ReactionRemoveResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
|
||||
}
|
||||
|
||||
public static func reactionDeleteAll(
|
||||
|
@ -755,7 +755,7 @@ public enum OpenGroupAPI {
|
|||
in roomToken: String,
|
||||
on server: String,
|
||||
using dependencies: SMKDependencies = SMKDependencies()
|
||||
) -> Promise<OnionRequestResponseInfoType> {
|
||||
) -> Promise<(OnionRequestResponseInfoType, ReactionRemoveAllResponse)> {
|
||||
/// URL(String:) won't convert raw emojis, so need to do a little encoding here.
|
||||
/// The raw emoji will come back when calling url.path
|
||||
guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
|
||||
|
@ -772,7 +772,7 @@ public enum OpenGroupAPI {
|
|||
),
|
||||
using: dependencies
|
||||
)
|
||||
.map { responseInfo, _ in responseInfo }
|
||||
.decoded(as: ReactionRemoveAllResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
|
||||
}
|
||||
|
||||
// MARK: - Pinning
|
||||
|
|
|
@ -19,6 +19,8 @@ public protocol OGMCacheType {
|
|||
var hasPerformedInitialPoll: [String: Bool] { get set }
|
||||
var timeSinceLastPoll: [String: TimeInterval] { get set }
|
||||
|
||||
var pendingChanges: [OpenGroupAPI.PendingChange] { get set }
|
||||
|
||||
func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval
|
||||
}
|
||||
|
||||
|
@ -53,6 +55,8 @@ public final class OpenGroupManager: NSObject {
|
|||
_timeSinceLastOpen = dependencies.date.timeIntervalSince(lastOpen)
|
||||
return dependencies.date.timeIntervalSince(lastOpen)
|
||||
}
|
||||
|
||||
public var pendingChanges: [OpenGroupAPI.PendingChange] = []
|
||||
}
|
||||
|
||||
// MARK: - Variables
|
||||
|
@ -529,11 +533,17 @@ public final class OpenGroupManager: NSObject {
|
|||
.filter { $0.deleted == true }
|
||||
.map { $0.id }
|
||||
|
||||
// Update the 'openGroupSequenceNumber' value (Note: SOGS V4 uses the 'seqNo' instead of the 'serverId')
|
||||
if let seqNo: Int64 = seqNo {
|
||||
// Update the 'openGroupSequenceNumber' value (Note: SOGS V4 uses the 'seqNo' instead of the 'serverId')
|
||||
_ = try? OpenGroup
|
||||
.filter(id: openGroup.id)
|
||||
.updateAll(db, OpenGroup.Columns.sequenceNumber.set(to: seqNo))
|
||||
|
||||
// Update pendingChange cache
|
||||
dependencies.mutableCache.mutate {
|
||||
$0.pendingChanges = $0.pendingChanges
|
||||
.filter { $0.seqNo == nil || $0.seqNo! > seqNo }
|
||||
}
|
||||
}
|
||||
|
||||
// Process the messages
|
||||
|
@ -589,11 +599,23 @@ public final class OpenGroupManager: NSObject {
|
|||
db,
|
||||
openGroupId: openGroup.id,
|
||||
message: message,
|
||||
associatedPendingChanges: dependencies.cache.pendingChanges
|
||||
.filter {
|
||||
guard $0.server == server && $0.room == roomToken && $0.changeType == .reaction else {
|
||||
return false
|
||||
}
|
||||
|
||||
if case .reaction(let messageId, _, _) = $0.metadata {
|
||||
return messageId == message.id
|
||||
}
|
||||
return false
|
||||
},
|
||||
dependencies: dependencies
|
||||
)
|
||||
|
||||
try MessageReceiver.handleOpenGroupReactions(
|
||||
db,
|
||||
threadId: openGroup.threadId,
|
||||
openGroupMessageServerId: message.id,
|
||||
openGroupReactions: reactions
|
||||
)
|
||||
|
@ -737,6 +759,55 @@ public final class OpenGroupManager: NSObject {
|
|||
|
||||
// MARK: - Convenience
|
||||
|
||||
public static func addPendingReaction(
|
||||
emoji: String,
|
||||
id: Int64,
|
||||
in roomToken: String,
|
||||
on server: String,
|
||||
type: OpenGroupAPI.PendingChange.ReactAction,
|
||||
using dependencies: OGMDependencies = OGMDependencies()
|
||||
) -> OpenGroupAPI.PendingChange {
|
||||
let pendingChange = OpenGroupAPI.PendingChange(
|
||||
server: server,
|
||||
room: roomToken,
|
||||
changeType: .reaction,
|
||||
metadata: .reaction(
|
||||
messageId: id,
|
||||
emoji: emoji,
|
||||
action: type
|
||||
)
|
||||
)
|
||||
|
||||
dependencies.mutableCache.mutate {
|
||||
$0.pendingChanges.append(pendingChange)
|
||||
}
|
||||
|
||||
return pendingChange
|
||||
}
|
||||
|
||||
public static func updatePendingChange(
|
||||
_ pendingChange: OpenGroupAPI.PendingChange,
|
||||
seqNo: Int64?,
|
||||
using dependencies: OGMDependencies = OGMDependencies()
|
||||
) {
|
||||
dependencies.mutableCache.mutate {
|
||||
if let index = $0.pendingChanges.firstIndex(of: pendingChange) {
|
||||
$0.pendingChanges[index].seqNo = seqNo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func removePendingChange(
|
||||
_ pendingChange: OpenGroupAPI.PendingChange,
|
||||
using dependencies: OGMDependencies = OGMDependencies()
|
||||
) {
|
||||
dependencies.mutableCache.mutate {
|
||||
if let index = $0.pendingChanges.firstIndex(of: pendingChange) {
|
||||
$0.pendingChanges.remove(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This method specifies if the given capability is supported on a specified Open Group
|
||||
public static func isOpenGroupSupport(
|
||||
_ capability: Capability.Variant,
|
||||
|
@ -1039,10 +1110,10 @@ public final class OpenGroupManager: NSObject {
|
|||
|
||||
extension OpenGroupManager {
|
||||
public class OGMDependencies: SMKDependencies {
|
||||
internal var _mutableCache: Atomic<OGMCacheType>?
|
||||
internal var _mutableCache: Atomic<Atomic<OGMCacheType>?>
|
||||
public var mutableCache: Atomic<OGMCacheType> {
|
||||
get { Dependencies.getValueSettingIfNull(&_mutableCache) { OpenGroupManager.shared.mutableCache } }
|
||||
set { _mutableCache = newValue }
|
||||
set { _mutableCache.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
public var cache: OGMCacheType { return mutableCache.wrappedValue }
|
||||
|
@ -1063,7 +1134,7 @@ extension OpenGroupManager {
|
|||
standardUserDefaults: UserDefaultsType? = nil,
|
||||
date: Date? = nil
|
||||
) {
|
||||
_mutableCache = cache
|
||||
_mutableCache = Atomic(cache)
|
||||
|
||||
super.init(
|
||||
onionApi: onionApi,
|
||||
|
|
|
@ -340,13 +340,14 @@ extension MessageReceiver {
|
|||
sortId: sortId
|
||||
)
|
||||
try reaction.insert(db)
|
||||
Environment.shared?.notificationsManager.wrappedValue?
|
||||
.notifyUser(
|
||||
db,
|
||||
forReaction: reaction,
|
||||
in: thread
|
||||
)
|
||||
|
||||
if sender != getUserHexEncodedPublicKey(db) {
|
||||
Environment.shared?.notificationsManager.wrappedValue?
|
||||
.notifyUser(
|
||||
db,
|
||||
forReaction: reaction,
|
||||
in: thread
|
||||
)
|
||||
}
|
||||
case .remove:
|
||||
try Reaction
|
||||
.filter(Reaction.Columns.interactionId == interactionId)
|
||||
|
|
|
@ -249,11 +249,13 @@ public enum MessageReceiver {
|
|||
|
||||
public static func handleOpenGroupReactions(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
openGroupMessageServerId: Int64,
|
||||
openGroupReactions: [Reaction]
|
||||
) throws {
|
||||
guard let interactionId: Int64 = try? Interaction
|
||||
.select(.id)
|
||||
.filter(Interaction.Columns.threadId == threadId)
|
||||
.filter(Interaction.Columns.openGroupServerMessageId == openGroupMessageServerId)
|
||||
.asRequest(of: Int64.self)
|
||||
.fetchOne(db)
|
||||
|
|
|
@ -665,6 +665,7 @@ public final class MessageSender {
|
|||
with error: MessageSenderError,
|
||||
interactionId: Int64?
|
||||
) {
|
||||
// TODO: Revert the local database change
|
||||
// If the message was a reaction then we don't want to do anything to the original
|
||||
// interaciton (which the 'interactionId' is pointing to
|
||||
guard (message as? VisibleMessage)?.reaction == nil else { return }
|
||||
|
|
|
@ -89,7 +89,7 @@ public final class Poller {
|
|||
|
||||
private func pollNextSnode(seal: Resolver<Void>) {
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
let swarm = SnodeAPI.swarmCache[userPublicKey] ?? []
|
||||
let swarm = SnodeAPI.swarmCache.wrappedValue[userPublicKey] ?? []
|
||||
let unusedSnodes = swarm.subtracting(usedSnodes)
|
||||
|
||||
guard !unusedSnodes.isEmpty else {
|
||||
|
|
|
@ -244,7 +244,7 @@ public enum Preferences {
|
|||
|
||||
// Other
|
||||
case .messageSent: return "message_sent.aiff"
|
||||
case .none: return nil
|
||||
case .none: return "silence.aiff"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,58 +6,58 @@ import SessionSnodeKit
|
|||
import SessionUtilitiesKit
|
||||
|
||||
public class SMKDependencies: Dependencies {
|
||||
internal var _onionApi: OnionRequestAPIType.Type?
|
||||
internal var _onionApi: Atomic<OnionRequestAPIType.Type?>
|
||||
public var onionApi: OnionRequestAPIType.Type {
|
||||
get { Dependencies.getValueSettingIfNull(&_onionApi) { OnionRequestAPI.self } }
|
||||
set { _onionApi = newValue }
|
||||
set { _onionApi.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
internal var _sodium: SodiumType?
|
||||
internal var _sodium: Atomic<SodiumType?>
|
||||
public var sodium: SodiumType {
|
||||
get { Dependencies.getValueSettingIfNull(&_sodium) { Sodium() } }
|
||||
set { _sodium = newValue }
|
||||
set { _sodium.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
internal var _box: BoxType?
|
||||
internal var _box: Atomic<BoxType?>
|
||||
public var box: BoxType {
|
||||
get { Dependencies.getValueSettingIfNull(&_box) { sodium.getBox() } }
|
||||
set { _box = newValue }
|
||||
set { _box.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
internal var _genericHash: GenericHashType?
|
||||
internal var _genericHash: Atomic<GenericHashType?>
|
||||
public var genericHash: GenericHashType {
|
||||
get { Dependencies.getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } }
|
||||
set { _genericHash = newValue }
|
||||
set { _genericHash.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
internal var _sign: SignType?
|
||||
internal var _sign: Atomic<SignType?>
|
||||
public var sign: SignType {
|
||||
get { Dependencies.getValueSettingIfNull(&_sign) { sodium.getSign() } }
|
||||
set { _sign = newValue }
|
||||
set { _sign.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
internal var _aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType?
|
||||
internal var _aeadXChaCha20Poly1305Ietf: Atomic<AeadXChaCha20Poly1305IetfType?>
|
||||
public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {
|
||||
get { Dependencies.getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } }
|
||||
set { _aeadXChaCha20Poly1305Ietf = newValue }
|
||||
set { _aeadXChaCha20Poly1305Ietf.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
internal var _ed25519: Ed25519Type?
|
||||
internal var _ed25519: Atomic<Ed25519Type?>
|
||||
public var ed25519: Ed25519Type {
|
||||
get { Dependencies.getValueSettingIfNull(&_ed25519) { Ed25519Wrapper() } }
|
||||
set { _ed25519 = newValue }
|
||||
set { _ed25519.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
internal var _nonceGenerator16: NonceGenerator16ByteType?
|
||||
internal var _nonceGenerator16: Atomic<NonceGenerator16ByteType?>
|
||||
public var nonceGenerator16: NonceGenerator16ByteType {
|
||||
get { Dependencies.getValueSettingIfNull(&_nonceGenerator16) { OpenGroupAPI.NonceGenerator16Byte() } }
|
||||
set { _nonceGenerator16 = newValue }
|
||||
set { _nonceGenerator16.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
internal var _nonceGenerator24: NonceGenerator24ByteType?
|
||||
internal var _nonceGenerator24: Atomic<NonceGenerator24ByteType?>
|
||||
public var nonceGenerator24: NonceGenerator24ByteType {
|
||||
get { Dependencies.getValueSettingIfNull(&_nonceGenerator24) { OpenGroupAPI.NonceGenerator24Byte() } }
|
||||
set { _nonceGenerator24 = newValue }
|
||||
set { _nonceGenerator24.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
@ -77,15 +77,15 @@ public class SMKDependencies: Dependencies {
|
|||
standardUserDefaults: UserDefaultsType? = nil,
|
||||
date: Date? = nil
|
||||
) {
|
||||
_onionApi = onionApi
|
||||
_sodium = sodium
|
||||
_box = box
|
||||
_genericHash = genericHash
|
||||
_sign = sign
|
||||
_aeadXChaCha20Poly1305Ietf = aeadXChaCha20Poly1305Ietf
|
||||
_ed25519 = ed25519
|
||||
_nonceGenerator16 = nonceGenerator16
|
||||
_nonceGenerator24 = nonceGenerator24
|
||||
_onionApi = Atomic(onionApi)
|
||||
_sodium = Atomic(sodium)
|
||||
_box = Atomic(box)
|
||||
_genericHash = Atomic(genericHash)
|
||||
_sign = Atomic(sign)
|
||||
_aeadXChaCha20Poly1305Ietf = Atomic(aeadXChaCha20Poly1305Ietf)
|
||||
_ed25519 = Atomic(ed25519)
|
||||
_nonceGenerator16 = Atomic(nonceGenerator16)
|
||||
_nonceGenerator24 = Atomic(nonceGenerator24)
|
||||
|
||||
super.init(
|
||||
generalCache: generalCache,
|
||||
|
|
|
@ -230,6 +230,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
|||
try testOpenGroup.insert(db)
|
||||
try Capability(openGroupServer: testOpenGroup.server, variant: .sogs, isMissing: false).insert(db)
|
||||
}
|
||||
mockOGMCache.when { $0.pendingChanges }.thenReturn([])
|
||||
mockGeneralCache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)")
|
||||
mockGenericHash.when { $0.hash(message: anyArray(), outputLength: any()) }.thenReturn([])
|
||||
mockSodium
|
||||
|
|
|
@ -23,19 +23,19 @@ extension SMKDependencies {
|
|||
date: Date? = nil
|
||||
) -> SMKDependencies {
|
||||
return SMKDependencies(
|
||||
onionApi: (onionApi ?? self._onionApi),
|
||||
generalCache: (generalCache ?? self._generalCache),
|
||||
storage: (storage ?? self._storage),
|
||||
sodium: (sodium ?? self._sodium),
|
||||
box: (box ?? self._box),
|
||||
genericHash: (genericHash ?? self._genericHash),
|
||||
sign: (sign ?? self._sign),
|
||||
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf),
|
||||
ed25519: (ed25519 ?? self._ed25519),
|
||||
nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16),
|
||||
nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24),
|
||||
standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults),
|
||||
date: (date ?? self._date)
|
||||
onionApi: (onionApi ?? self._onionApi.wrappedValue),
|
||||
generalCache: (generalCache ?? self._generalCache.wrappedValue),
|
||||
storage: (storage ?? self._storage.wrappedValue),
|
||||
sodium: (sodium ?? self._sodium.wrappedValue),
|
||||
box: (box ?? self._box.wrappedValue),
|
||||
genericHash: (genericHash ?? self._genericHash.wrappedValue),
|
||||
sign: (sign ?? self._sign.wrappedValue),
|
||||
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf.wrappedValue),
|
||||
ed25519: (ed25519 ?? self._ed25519.wrappedValue),
|
||||
nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16.wrappedValue),
|
||||
nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24.wrappedValue),
|
||||
standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults.wrappedValue),
|
||||
date: (date ?? self._date.wrappedValue)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,11 @@ class MockOGMCache: Mock<OGMCacheType>, OGMCacheType {
|
|||
set { accept(args: [newValue]) }
|
||||
}
|
||||
|
||||
var pendingChanges: [OpenGroupAPI.PendingChange] {
|
||||
get { return accept() as! [OpenGroupAPI.PendingChange] }
|
||||
set { accept(args: [newValue]) }
|
||||
}
|
||||
|
||||
func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval {
|
||||
return accept(args: [dependencies]) as! TimeInterval
|
||||
}
|
||||
|
|
|
@ -24,20 +24,20 @@ extension OpenGroupManager.OGMDependencies {
|
|||
date: Date? = nil
|
||||
) -> OpenGroupManager.OGMDependencies {
|
||||
return OpenGroupManager.OGMDependencies(
|
||||
cache: (cache ?? self._mutableCache),
|
||||
onionApi: (onionApi ?? self._onionApi),
|
||||
generalCache: (generalCache ?? self._generalCache),
|
||||
storage: (storage ?? self._storage),
|
||||
sodium: (sodium ?? self._sodium),
|
||||
box: (box ?? self._box),
|
||||
genericHash: (genericHash ?? self._genericHash),
|
||||
sign: (sign ?? self._sign),
|
||||
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf),
|
||||
ed25519: (ed25519 ?? self._ed25519),
|
||||
nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16),
|
||||
nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24),
|
||||
standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults),
|
||||
date: (date ?? self._date)
|
||||
cache: (cache ?? self._mutableCache.wrappedValue),
|
||||
onionApi: (onionApi ?? self._onionApi.wrappedValue),
|
||||
generalCache: (generalCache ?? self._generalCache.wrappedValue),
|
||||
storage: (storage ?? self._storage.wrappedValue),
|
||||
sodium: (sodium ?? self._sodium.wrappedValue),
|
||||
box: (box ?? self._box.wrappedValue),
|
||||
genericHash: (genericHash ?? self._genericHash.wrappedValue),
|
||||
sign: (sign ?? self._sign.wrappedValue),
|
||||
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf.wrappedValue),
|
||||
ed25519: (ed25519 ?? self._ed25519.wrappedValue),
|
||||
nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16.wrappedValue),
|
||||
nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24.wrappedValue),
|
||||
standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults.wrappedValue),
|
||||
date: (date ?? self._date.wrappedValue)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,18 +157,15 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
||||
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
||||
|
||||
notificationContent.title = interaction.previewText(db)
|
||||
notificationContent.title = "Session"
|
||||
notificationContent.body = ""
|
||||
|
||||
let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant)
|
||||
|
||||
if messageInfo.state == .permissionDenied {
|
||||
notificationContent.body = String(
|
||||
format: "modal_call_missed_tips_explanation".localized(),
|
||||
SessionThread.displayName(
|
||||
threadId: thread.id,
|
||||
variant: thread.variant,
|
||||
closedGroupName: nil, // Not supported
|
||||
openGroupName: nil // Not supported
|
||||
)
|
||||
senderName
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
self.contentHandler = contentHandler
|
||||
self.request = request
|
||||
|
||||
// Resume database
|
||||
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
|
||||
|
||||
guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
|
||||
return self.completeSilenty()
|
||||
}
|
||||
|
@ -237,6 +240,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
|
||||
private func completeSilenty() {
|
||||
SNLog("Complete silenty")
|
||||
|
||||
// Suspend the database
|
||||
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
||||
|
||||
self.contentHandler!(.init())
|
||||
}
|
||||
|
||||
|
@ -298,11 +305,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
SNLog("Add remote notification request")
|
||||
}
|
||||
|
||||
private func handleSuccess(for content: UNMutableNotificationContent) {
|
||||
contentHandler!(content)
|
||||
}
|
||||
|
||||
private func handleFailure(for content: UNMutableNotificationContent) {
|
||||
// Suspend the database
|
||||
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
||||
|
||||
content.body = "You've got a new message"
|
||||
content.title = "Session"
|
||||
let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ]
|
||||
|
|
|
@ -180,6 +180,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
shareVC?.dismiss(animated: true, completion: nil)
|
||||
|
||||
ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in
|
||||
// Resume database
|
||||
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
|
||||
Storage.shared
|
||||
.writeAsync { [weak self] db -> Promise<Void> in
|
||||
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
||||
|
@ -231,10 +233,14 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
)
|
||||
}
|
||||
.done { [weak self] _ in
|
||||
// Suspend the database
|
||||
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
||||
activityIndicator.dismiss { }
|
||||
self?.shareVC?.shareViewWasCompleted()
|
||||
}
|
||||
.catch { [weak self] error in
|
||||
// Suspend the database
|
||||
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
||||
activityIndicator.dismiss { }
|
||||
self?.shareVC?.shareViewFailed(error: error)
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ public final class SnodeAPI {
|
|||
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||
public static var clockOffset: Int64 = 0
|
||||
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||
public static var swarmCache: [String: Set<Snode>] = [:]
|
||||
public static var swarmCache: Atomic<[String: Set<Snode>]> = Atomic([:])
|
||||
|
||||
// MARK: - Namespaces
|
||||
|
||||
|
@ -96,10 +96,11 @@ public final class SnodeAPI {
|
|||
private static func loadSwarmIfNeeded(for publicKey: String) {
|
||||
guard !loadedSwarms.contains(publicKey) else { return }
|
||||
|
||||
Storage.shared.read { db in
|
||||
swarmCache[publicKey] = ((try? Snode.fetchSet(db, publicKey: publicKey)) ?? [])
|
||||
}
|
||||
let updatedCacheForKey: Set<Snode> = Storage.shared
|
||||
.read { db in try Snode.fetchSet(db, publicKey: publicKey) }
|
||||
.defaulting(to: [])
|
||||
|
||||
swarmCache.mutate { $0[publicKey] = updatedCacheForKey }
|
||||
loadedSwarms.insert(publicKey)
|
||||
}
|
||||
|
||||
|
@ -107,7 +108,8 @@ public final class SnodeAPI {
|
|||
#if DEBUG
|
||||
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
|
||||
#endif
|
||||
swarmCache[publicKey] = newValue
|
||||
swarmCache.mutate { $0[publicKey] = newValue }
|
||||
|
||||
guard persist else { return }
|
||||
|
||||
Storage.shared.write { db in
|
||||
|
@ -119,7 +121,7 @@ public final class SnodeAPI {
|
|||
#if DEBUG
|
||||
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
|
||||
#endif
|
||||
let swarmOrNil = swarmCache[publicKey]
|
||||
let swarmOrNil = swarmCache.wrappedValue[publicKey]
|
||||
guard var swarm = swarmOrNil, let index = swarm.firstIndex(of: snode) else { return }
|
||||
swarm.remove(at: index)
|
||||
setSwarm(to: swarm, for: publicKey)
|
||||
|
@ -460,7 +462,7 @@ public final class SnodeAPI {
|
|||
public static func getSwarm(for publicKey: String) -> Promise<Set<Snode>> {
|
||||
loadSwarmIfNeeded(for: publicKey)
|
||||
|
||||
if let cachedSwarm = swarmCache[publicKey], cachedSwarm.count >= minSwarmSnodeCount {
|
||||
if let cachedSwarm = swarmCache.wrappedValue[publicKey], cachedSwarm.count >= minSwarmSnodeCount {
|
||||
return Promise<Set<Snode>> { $0.fulfill(cachedSwarm) }
|
||||
}
|
||||
|
||||
|
|
|
@ -49,5 +49,6 @@ public final class Colors : NSObject {
|
|||
@objc public static var sessionMessageRequestsIcon: UIColor { UIColor(named: "session_message_requests_icon")! }
|
||||
@objc public static var sessionMessageRequestsTitle: UIColor { UIColor(named: "session_message_requests_title")! }
|
||||
@objc public static var sessionMessageRequestsInfoText: UIColor { UIColor(named: "session_message_requests_info_text")! }
|
||||
@objc public static var blockActionBackground: UIColor { UIColor(named: "session_block_action_background")! }
|
||||
@objc public static var sessionEmojiPlusButtonBackground: UIColor { UIColor(named: "session_emoji_plus_button_background")! }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x6D",
|
||||
"green" : "0x6D",
|
||||
"red" : "0x6D"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x2D",
|
||||
"green" : "0x2D",
|
||||
"red" : "0x2D"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "28",
|
||||
"green" : "28",
|
||||
"red" : "28"
|
||||
"blue" : "0x1C",
|
||||
"green" : "0x1C",
|
||||
"red" : "0x1C"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x3A",
|
||||
"green" : "0x45",
|
||||
"green" : "0x3A",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "22",
|
||||
"green" : "22",
|
||||
"red" : "22"
|
||||
"blue" : "0x16",
|
||||
"green" : "0x16",
|
||||
"red" : "0x16"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -3,28 +3,28 @@
|
|||
import Foundation
|
||||
|
||||
open class Dependencies {
|
||||
public var _generalCache: Atomic<GeneralCacheType>?
|
||||
public var _generalCache: Atomic<Atomic<GeneralCacheType>?>
|
||||
public var generalCache: Atomic<GeneralCacheType> {
|
||||
get { Dependencies.getValueSettingIfNull(&_generalCache) { General.cache } }
|
||||
set { _generalCache = newValue }
|
||||
set { _generalCache.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
public var _storage: Storage?
|
||||
public var _storage: Atomic<Storage?>
|
||||
public var storage: Storage {
|
||||
get { Dependencies.getValueSettingIfNull(&_storage) { Storage.shared } }
|
||||
set { _storage = newValue }
|
||||
set { _storage.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
public var _standardUserDefaults: UserDefaultsType?
|
||||
public var _standardUserDefaults: Atomic<UserDefaultsType?>
|
||||
public var standardUserDefaults: UserDefaultsType {
|
||||
get { Dependencies.getValueSettingIfNull(&_standardUserDefaults) { UserDefaults.standard } }
|
||||
set { _standardUserDefaults = newValue }
|
||||
set { _standardUserDefaults.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
public var _date: Date?
|
||||
public var _date: Atomic<Date?>
|
||||
public var date: Date {
|
||||
get { Dependencies.getValueSettingIfNull(&_date) { Date() } }
|
||||
set { _date = newValue }
|
||||
set { _date.mutate { $0 = newValue } }
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
@ -35,21 +35,29 @@ open class Dependencies {
|
|||
standardUserDefaults: UserDefaultsType? = nil,
|
||||
date: Date? = nil
|
||||
) {
|
||||
_generalCache = generalCache
|
||||
_storage = storage
|
||||
_standardUserDefaults = standardUserDefaults
|
||||
_date = date
|
||||
_generalCache = Atomic(generalCache)
|
||||
_storage = Atomic(storage)
|
||||
_standardUserDefaults = Atomic(standardUserDefaults)
|
||||
_date = Atomic(date)
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
public static func getValueSettingIfNull<T>(_ maybeValue: inout T?, _ valueGenerator: () -> T) -> T {
|
||||
guard let value: T = maybeValue else {
|
||||
|
||||
public static func getValueSettingIfNull<T>(_ maybeValue: inout Atomic<T?>, _ valueGenerator: () -> T) -> T {
|
||||
guard let value: T = maybeValue.wrappedValue else {
|
||||
let value: T = valueGenerator()
|
||||
maybeValue = value
|
||||
maybeValue.mutate { $0 = value }
|
||||
return value
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// 0 libswiftCore.dylib 0x00000001999fd40c _swift_release_dealloc + 32 (HeapObject.cpp:703)
|
||||
// 1 SessionMessagingKit 0x0000000106aa958c 0x106860000 + 2397580
|
||||
// 2 libswiftCore.dylib 0x00000001999fd424 _swift_release_dealloc + 56 (HeapObject.cpp:703)
|
||||
// 3 SessionUtilitiesKit 0x0000000106cbd980 static Dependencies.getValueSettingIfNull<A>(_:_:) + 264 (Dependencies.swift:49)
|
||||
// 4 SessionMessagingKit 0x0000000106aa90f4 closure #1 in SMKDependencies.sign.getter + 112 (SMKDependencies.swift:17)
|
||||
// 5 SessionUtilitiesKit 0x0000000106cbd974 static Dependencies.getValueSettingIfNull<A>(_:_:) + 252 (Dependencies.swift:48)
|
||||
// 6 SessionMessagingKit 0x000000010697aef8 specialized static OpenGroupAPI.sign(_:messageBytes:for:fallbackSigningType:using:) + 1158904 (OpenGroupAPI.swift:1190)
|
||||
}
|
||||
|
|
|
@ -58,4 +58,8 @@ public class NotificationStrings: NSObject {
|
|||
@objc public class MediaStrings: NSObject {
|
||||
@objc
|
||||
static public let allMedia = NSLocalizedString("MEDIA_DETAIL_VIEW_ALL_MEDIA_BUTTON", comment: "nav bar button item")
|
||||
@objc
|
||||
static public let media = NSLocalizedString("MEDIA_TAB_TITLE", comment: "media tab title")
|
||||
@objc
|
||||
static public let document = NSLocalizedString("DOCUMENT_TAB_TITLE", comment: "document tab title")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue