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:
Morgan Pretty 2022-09-15 14:22:11 +10:00
commit 1bc6b0bdba
75 changed files with 3982 additions and 2422 deletions

View File

@ -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("}")
}

View File

@ -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)",

View File

@ -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

View File

@ -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()
}
}
}
}

View File

@ -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

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -60,6 +60,8 @@ public class MediaView: UIView {
themeBackgroundColor = .backgroundSecondary
clipsToBounds = true
layer.masksToBounds = true
layer.cornerRadius = VisibleMessageCell.largeCornerRadius
createContents()
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 }

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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),

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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
)
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -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
)
)

View File

@ -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
}
}
}
}
}

View File

@ -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?
}
}

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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 }

View File

@ -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 {

View File

@ -244,7 +244,7 @@ public enum Preferences {
// Other
case .messageSent: return "message_sent.aiff"
case .none: return nil
case .none: return "silence.aiff"
}
}

View File

@ -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,

View File

@ -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

View File

@ -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)
)
}
}

View File

@ -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
}

View File

@ -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)
)
}
}

View File

@ -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
)
}

View File

@ -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 ]

View File

@ -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)
}

View File

@ -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) }
}

View File

@ -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")! }
}

View File

@ -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
}
}

View File

@ -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"

View File

@ -6,7 +6,7 @@
"components" : {
"alpha" : "1.000",
"blue" : "0x3A",
"green" : "0x45",
"green" : "0x3A",
"red" : "0xFF"
}
},

View File

@ -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"

View File

@ -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)
}

View File

@ -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")
}