diff --git a/Images.xcassets/Settings/MenuIcons/Watch.imageset/Contents.json b/Images.xcassets/Settings/MenuIcons/Watch.imageset/Contents.json new file mode 100644 index 0000000000..a37ce181a7 --- /dev/null +++ b/Images.xcassets/Settings/MenuIcons/Watch.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SettingsWatchIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "SettingsWatchIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Settings/MenuIcons/Watch.imageset/SettingsWatchIcon@2x.png b/Images.xcassets/Settings/MenuIcons/Watch.imageset/SettingsWatchIcon@2x.png new file mode 100644 index 0000000000..ced6620bf7 Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Watch.imageset/SettingsWatchIcon@2x.png differ diff --git a/Images.xcassets/Settings/MenuIcons/Watch.imageset/SettingsWatchIcon@3x.png b/Images.xcassets/Settings/MenuIcons/Watch.imageset/SettingsWatchIcon@3x.png new file mode 100644 index 0000000000..d12522777a Binary files /dev/null and b/Images.xcassets/Settings/MenuIcons/Watch.imageset/SettingsWatchIcon@3x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 534b79fc40..c321e24a4c 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -23,6 +23,12 @@ 0941A9A4210B0E2E00EBE194 /* OpenInAppIconResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0941A9A3210B0E2E00EBE194 /* OpenInAppIconResources.swift */; }; 0941A9A6210B822D00EBE194 /* OpenInOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0941A9A5210B822D00EBE194 /* OpenInOptions.swift */; }; 0952D1752176DEB500194860 /* NotificationMuteSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0952D1742176DEB500194860 /* NotificationMuteSettingsController.swift */; }; + 0952D1772177FB5400194860 /* WatchPresetSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0952D1762177FB5400194860 /* WatchPresetSettings.swift */; }; + 096C98BA21787A5C00C211FF /* LegacyBridgeAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 096C98B921787A5C00C211FF /* LegacyBridgeAudio.swift */; }; + 096C98BF21787C6700C211FF /* TGBridgeAudioEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 096C98BB21787C6600C211FF /* TGBridgeAudioEncoder.m */; }; + 096C98C021787C6700C211FF /* TGBridgeAudioEncoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 096C98BC21787C6600C211FF /* TGBridgeAudioEncoder.h */; }; + 096C98C121787C6700C211FF /* TGBridgeAudioDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 096C98BD21787C6700C211FF /* TGBridgeAudioDecoder.h */; }; + 096C98C221787C6700C211FF /* TGBridgeAudioDecoder.mm in Sources */ = {isa = PBXBuildFile; fileRef = 096C98BE21787C6700C211FF /* TGBridgeAudioDecoder.mm */; }; 09797873210633CD0077D77F /* InstantPageSettingsButtonItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09797872210633CD0077D77F /* InstantPageSettingsButtonItemNode.swift */; }; 0979787C210642CB0077D77F /* WebEmbedPlayerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0979787B210642CB0077D77F /* WebEmbedPlayerNode.swift */; }; 0979787E210646C00077D77F /* YoutubeEmbedImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0979787D210646C00077D77F /* YoutubeEmbedImplementation.swift */; }; @@ -40,6 +46,8 @@ 09AE3823214C110900850BFD /* LegacySecureIdScanController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09AE3822214C110800850BFD /* LegacySecureIdScanController.swift */; }; 09C3466D2167D63A00B76780 /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C3466C2167D63A00B76780 /* Accessibility.swift */; }; 09C500242142BA6400EF253E /* ItemListWebsiteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */; }; + 09D304152173C0E900C00567 /* WatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D304142173C0E900C00567 /* WatchManager.swift */; }; + 09D304182173C15700C00567 /* WatchSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D304172173C15700C00567 /* WatchSettingsController.swift */; }; 09FE756D2153F5F900A3120F /* CallRouteActionSheetItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */; }; D0068FA821760FA300D1B315 /* StoreDownloadedMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0068FA721760FA300D1B315 /* StoreDownloadedMedia.swift */; }; D007019C2029E8F2006B9E34 /* LegqacyICloudFileController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D007019B2029E8F2006B9E34 /* LegqacyICloudFileController.swift */; }; @@ -111,6 +119,8 @@ D02660941F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02660931F34CE5C000E2DC5 /* LegacyLocationVenueIconDataSource.swift */; }; D02B2B9820810DA00062476B /* StickerPaneSearchStickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02B2B9720810DA00062476B /* StickerPaneSearchStickerItem.swift */; }; D02B676320800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02B676220800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift */; }; + D02C81712177729000CD1006 /* NotificationExceptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02C81702177729000CD1006 /* NotificationExceptions.swift */; }; + D02C81732177AC5900CD1006 /* NotificationSearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02C81722177AC5900CD1006 /* NotificationSearchItem.swift */; }; D02D60AE206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D60AD206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift */; }; D02D60B1206C189900FEFE1E /* SecureIdPlaintextFormController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D60B0206C189900FEFE1E /* SecureIdPlaintextFormController.swift */; }; D02D60B3206C18A600FEFE1E /* SecureIdPlaintextFormControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02D60B2206C18A600FEFE1E /* SecureIdPlaintextFormControllerNode.swift */; }; @@ -1046,6 +1056,12 @@ 0941A9A3210B0E2E00EBE194 /* OpenInAppIconResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInAppIconResources.swift; sourceTree = ""; }; 0941A9A5210B822D00EBE194 /* OpenInOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInOptions.swift; sourceTree = ""; }; 0952D1742176DEB500194860 /* NotificationMuteSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMuteSettingsController.swift; sourceTree = ""; }; + 0952D1762177FB5400194860 /* WatchPresetSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchPresetSettings.swift; sourceTree = ""; }; + 096C98B921787A5C00C211FF /* LegacyBridgeAudio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyBridgeAudio.swift; sourceTree = ""; }; + 096C98BB21787C6600C211FF /* TGBridgeAudioEncoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGBridgeAudioEncoder.m; sourceTree = ""; }; + 096C98BC21787C6600C211FF /* TGBridgeAudioEncoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBridgeAudioEncoder.h; sourceTree = ""; }; + 096C98BD21787C6700C211FF /* TGBridgeAudioDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBridgeAudioDecoder.h; sourceTree = ""; }; + 096C98BE21787C6700C211FF /* TGBridgeAudioDecoder.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TGBridgeAudioDecoder.mm; sourceTree = ""; }; 09797872210633CD0077D77F /* InstantPageSettingsButtonItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsButtonItemNode.swift; sourceTree = ""; }; 0979787B210642CB0077D77F /* WebEmbedPlayerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebEmbedPlayerNode.swift; sourceTree = ""; }; 0979787D210646C00077D77F /* YoutubeEmbedImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YoutubeEmbedImplementation.swift; sourceTree = ""; }; @@ -1067,6 +1083,8 @@ 09AE3822214C110800850BFD /* LegacySecureIdScanController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySecureIdScanController.swift; sourceTree = ""; }; 09C3466C2167D63A00B76780 /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; 09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListWebsiteItem.swift; sourceTree = ""; }; + 09D304142173C0E900C00567 /* WatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchManager.swift; sourceTree = ""; }; + 09D304172173C15700C00567 /* WatchSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsController.swift; sourceTree = ""; }; 09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRouteActionSheetItem.swift; sourceTree = ""; }; D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageContainingNode.swift; sourceTree = ""; }; D002A0D01E9B99F500A81812 /* SoftwareVideoSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SoftwareVideoSource.swift; sourceTree = ""; }; @@ -1238,6 +1256,8 @@ D02B676220800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchBarPlaceholderItem.swift; sourceTree = ""; }; D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = ""; }; D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = ""; }; + D02C81702177729000CD1006 /* NotificationExceptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationExceptions.swift; sourceTree = ""; }; + D02C81722177AC5900CD1006 /* NotificationSearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSearchItem.swift; sourceTree = ""; }; D02D60AD206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdDocumentTypeSelectionController.swift; sourceTree = ""; }; D02D60B0206C189900FEFE1E /* SecureIdPlaintextFormController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdPlaintextFormController.swift; sourceTree = ""; }; D02D60B2206C18A600FEFE1E /* SecureIdPlaintextFormControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdPlaintextFormControllerNode.swift; sourceTree = ""; }; @@ -2217,6 +2237,18 @@ name = "Open In"; sourceTree = ""; }; + 0965C7152178738A007C94D0 /* Bridge Audio */ = { + isa = PBXGroup; + children = ( + 096C98BD21787C6700C211FF /* TGBridgeAudioDecoder.h */, + 096C98BE21787C6700C211FF /* TGBridgeAudioDecoder.mm */, + 096C98BC21787C6600C211FF /* TGBridgeAudioEncoder.h */, + 096C98BB21787C6600C211FF /* TGBridgeAudioEncoder.m */, + 096C98B921787A5C00C211FF /* LegacyBridgeAudio.swift */, + ); + name = "Bridge Audio"; + sourceTree = ""; + }; 0979787F21065EAA0077D77F /* Web Embed */ = { isa = PBXGroup; children = ( @@ -2249,6 +2281,14 @@ name = "Web Embed"; sourceTree = ""; }; + 09D304162173C13500C00567 /* Watch */ = { + isa = PBXGroup; + children = ( + 09D304172173C15700C00567 /* WatchSettingsController.swift */, + ); + name = Watch; + sourceTree = ""; + }; D00C7CDA1E3776CA0080C3D5 /* Secret Preview */ = { isa = PBXGroup; children = ( @@ -2437,6 +2477,17 @@ name = "Grid Items"; sourceTree = ""; }; + D02C816F2177715A00CD1006 /* Notifications */ = { + isa = PBXGroup; + children = ( + D0579E6D2179178700495DC7 /* exceptions */, + D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */, + D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */, + D02C81722177AC5900CD1006 /* NotificationSearchItem.swift */, + ); + name = Notifications; + sourceTree = ""; + }; D02D60AF206C188000FEFE1E /* Plaintext Fields */ = { isa = PBXGroup; children = ( @@ -2785,6 +2836,14 @@ name = "Avatar Gallery"; sourceTree = ""; }; + D0579E6D2179178700495DC7 /* exceptions */ = { + isa = PBXGroup; + children = ( + D02C81702177729000CD1006 /* NotificationExceptions.swift */, + ); + name = exceptions; + sourceTree = ""; + }; D05BFB4F1EA96EC100909D38 /* Themes */ = { isa = PBXGroup; children = ( @@ -2888,6 +2947,7 @@ D07551891DDA4C7C0073E051 /* Legacy Components */ = { isa = PBXGroup; children = ( + 0965C7152178738A007C94D0 /* Bridge Audio */, D04BB2C61E48797500650E93 /* RMIntro */, D067B4AE211C916D00796039 /* Channel Intro */, D075518A1DDA4D7D0073E051 /* LegacyController.swift */, @@ -3003,6 +3063,7 @@ D048B33A203C777500038D05 /* RenderedTotalUnreadCount.swift */, D06ECFCA20B8448E00C576C2 /* ContactSynchronizationSettings.swift */, D08A10BA211DF7A80077488B /* StickerSettings.swift */, + 0952D1762177FB5400194860 /* WatchPresetSettings.swift */, ); name = Settings; sourceTree = ""; @@ -4230,16 +4291,16 @@ D0F69E791D6B8C3B0046BCD6 /* Settings */ = { isa = PBXGroup; children = ( + D02C816F2177715A00CD1006 /* Notifications */, D0FA0AC21E7742CE005BB9B7 /* Privacy and Security */, D0C9323A1E0B4AD40074F044 /* Data and Storage */, D0FA0AC31E7742EE005BB9B7 /* Stickers */, D05BFB4F1EA96EC100909D38 /* Themes */, + 09D304162173C13500C00567 /* Watch */, D0AF7C441ED84BB000CD8E0F /* Language Selection */, D0CB27D020C17A6D001ACF93 /* Terms of Service */, D01B279A1E39386C0022A4C0 /* SettingsController.swift */, D08BDF651FA8CB10009D08E1 /* EditSettingsController.swift */, - D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */, - D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */, D0CE1BD21E51BC6100404327 /* DebugController.swift */, D03E5E081E55C49C0029569A /* DebugAccountsController.swift */, D0528E671E65CB2C00E2FEF5 /* UsernameSetupController.swift */, @@ -4289,6 +4350,7 @@ D0383ED5207D19BC00C45548 /* Emoji */, D0B69C3A20EBD8B3003632C7 /* Device Access */, D01C7EFE1EF9D434008305F1 /* Device Contacts */, + 09D304142173C0E900C00567 /* WatchManager.swift */, D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */, D08775081E3E59DE00A97350 /* PeerNotificationSoundStrings.swift */, D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */, @@ -4471,6 +4533,7 @@ buildActionMask = 2147483647; files = ( D0E9BA221F05577700F079A4 /* STPCard.h in Headers */, + 096C98C021787C6700C211FF /* TGBridgeAudioEncoder.h in Headers */, D0E9BA591F055A2200F079A4 /* STPWeakStrongMacros.h in Headers */, D0E9BADE1F0574D800F079A4 /* STPBackendAPIAdapter.h in Headers */, D0E9BAD11F0573C000F079A4 /* STPToken.h in Headers */, @@ -4501,6 +4564,7 @@ D06F31E22135829B001A0F12 /* EDSunriseSet.h in Headers */, D0E9BA531F0559DA00F079A4 /* STPImageLibrary+Private.h in Headers */, D0E9BA601F055A4300F079A4 /* STPDelegateProxy.h in Headers */, + 096C98C121787C6700C211FF /* TGBridgeAudioDecoder.h in Headers */, D0E9BADF1F0574D800F079A4 /* STPDispatchFunctions.h in Headers */, D0E9BACB1F05738600F079A4 /* STPAPIPostRequest.h in Headers */, D0E9BA561F055A0B00F079A4 /* STPFormTextField.h in Headers */, @@ -4723,6 +4787,7 @@ D0208ADC1FA346A4001F0D5F /* RaiseToListen.swift in Sources */, D0EB41F91F30E5B700838FE6 /* LegacyPeerAvatarPlaceholderDataSource.swift in Sources */, D0EC6CBB1EB9F58800EBF1C3 /* texture_helper.m in Sources */, + 09D304182173C15700C00567 /* WatchSettingsController.swift in Sources */, D0EC6CBC1EB9F58800EBF1C3 /* LegacyController.swift in Sources */, D0EC6CBD1EB9F58800EBF1C3 /* LegacyControllerNode.swift in Sources */, D079FCE91F06A76C0038FADE /* Notices.swift in Sources */, @@ -4847,6 +4912,7 @@ D0ACCB1A1EC5E0C20079D8BF /* CallControllerKeyPreviewNode.swift in Sources */, D0E9BA611F055A4300F079A4 /* STPDelegateProxy.m in Sources */, D0EC6CF91EB9F58800EBF1C3 /* MediaManager.swift in Sources */, + 096C98BF21787C6700C211FF /* TGBridgeAudioEncoder.m in Sources */, D01776B81F1D6FB30044446D /* RadialProgressContentNode.swift in Sources */, D0EC6CFA1EB9F58800EBF1C3 /* ManagedAudioSession.swift in Sources */, D0EB5ADF1F798033004E89B6 /* PeerMediaCollectionEmptyNode.swift in Sources */, @@ -4856,6 +4922,7 @@ D0EC6CFD1EB9F58800EBF1C3 /* AudioWaveform.swift in Sources */, D0EC6CFF1EB9F58800EBF1C3 /* OverlayMediaController.swift in Sources */, D0EC6D001EB9F58800EBF1C3 /* OverlayMediaControllerNode.swift in Sources */, + D02C81712177729000CD1006 /* NotificationExceptions.swift in Sources */, D0EC6D021EB9F58800EBF1C3 /* diag_range.c in Sources */, D0E9BA1A1F05574500F079A4 /* STPPaymentCardTextField.m in Sources */, D0EC6D031EB9F58800EBF1C3 /* opus_header.c in Sources */, @@ -4984,6 +5051,7 @@ D0EC6D3F1EB9F58800EBF1C3 /* MediaNavigationAccessoryPanel.swift in Sources */, D0E9BA3B1F0558E800F079A4 /* NSString+Stripe.m in Sources */, D0CE8CE51F6F354400AA2DB0 /* ChatTextInputAccessoryItem.swift in Sources */, + 096C98BA21787A5C00C211FF /* LegacyBridgeAudio.swift in Sources */, D0EC6D401EB9F58800EBF1C3 /* MediaNavigationAccessoryContainerNode.swift in Sources */, D0E266FD1F66706500BFC79F /* ChatBubbleVideoDecoration.swift in Sources */, D0EC6D411EB9F58800EBF1C3 /* MediaNavigationAccessoryHeaderNode.swift in Sources */, @@ -5009,6 +5077,7 @@ D0208ADA1FA34017001F0D5F /* DeviceProximityManager.m in Sources */, D04281FC200E61BC009DDE36 /* ChatRecentActionsInteraction.swift in Sources */, D0EC6D561EB9F58800EBF1C3 /* ChatHistoryNode.swift in Sources */, + 096C98C221787C6700C211FF /* TGBridgeAudioDecoder.mm in Sources */, D0EC6D571EB9F58800EBF1C3 /* ChatHistoryListNode.swift in Sources */, D0EC6D581EB9F58800EBF1C3 /* ChatHistoryGridNode.swift in Sources */, D0B2F76E2052B59F00D3BFB9 /* InviteContactsController.swift in Sources */, @@ -5041,6 +5110,7 @@ D0EC6D671EB9F58800EBF1C3 /* ContactListNameIndexHeader.swift in Sources */, D0CE6F6E213EDF8800BCD44B /* SecureIdAuthPasswordSetupContentNode.swift in Sources */, D07E413D208A494D00FCA8F0 /* ProxyServerActionSheetController.swift in Sources */, + D02C81732177AC5900CD1006 /* NotificationSearchItem.swift in Sources */, D0EC6D681EB9F58800EBF1C3 /* AuthorizationSequenceController.swift in Sources */, D0EC6D691EB9F58800EBF1C3 /* AuthorizationSequenceSplashController.swift in Sources */, D0EC6D6A1EB9F58800EBF1C3 /* AuthorizationSequenceSplashControllerNode.swift in Sources */, @@ -5139,6 +5209,7 @@ D0147BA7206E8B4F00E40378 /* SecureIdAuthAcceptNode.swift in Sources */, D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */, D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */, + 09D304152173C0E900C00567 /* WatchManager.swift in Sources */, D039FB1921711B5D00BD1BAD /* PlatformVideoContent.swift in Sources */, D0CAD8FD20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift in Sources */, D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */, @@ -5368,6 +5439,7 @@ D0EC6E171EB9F58900EBF1C3 /* InstantPageTextStyleStack.swift in Sources */, D0EC6E181EB9F58900EBF1C3 /* InstantPageTextItem.swift in Sources */, D01C06B51FBB7720001561AB /* ChatMediaInputSettingsItem.swift in Sources */, + 0952D1772177FB5400194860 /* WatchPresetSettings.swift in Sources */, D091C7A61F8ECEA300D7DE13 /* SettingsThemeWallpaperNode.swift in Sources */, D0EC6E191EB9F58900EBF1C3 /* InstantPageAnchorItem.swift in Sources */, D05677531F4CA0D0001B723E /* InstantPagePeerReferenceNode.swift in Sources */, diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index c49b243499..198a3779dc 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -15,6 +15,9 @@ public struct ChatListNodePeersFilter: OptionSet { public static let onlyWriteable = ChatListNodePeersFilter(rawValue: 1 << 0) public static let onlyUsers = ChatListNodePeersFilter(rawValue: 1 << 1) public static let onlyGroups = ChatListNodePeersFilter(rawValue: 1 << 2) + public static let withoutSecretChats = ChatListNodePeersFilter(rawValue: 1 << 3) + + } enum ChatListNodeMode { diff --git a/TelegramUI/ChatListNodeEntries.swift b/TelegramUI/ChatListNodeEntries.swift index 089505641c..f2affd7a09 100644 --- a/TelegramUI/ChatListNodeEntries.swift +++ b/TelegramUI/ChatListNodeEntries.swift @@ -220,12 +220,25 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, if view.laterIndex == nil && savedMessagesPeer == nil { pinnedIndexOffset = UInt16(view.additionalItemEntries.count) } + + + loop: for entry in view.entries { switch entry { case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, summaryInfo): if let savedMessagesPeer = savedMessagesPeer, savedMessagesPeer.id == index.messageIndex.id.peerId { continue loop } + switch mode { + case let .peers(filter): + if filter.contains(.withoutSecretChats) { + if index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat { + continue + } + } + default: + break + } result.append(.PeerEntry(index: offsetPinnedIndex(index, offset: pinnedIndexOffset), presentationData: state.presentationData, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: false)) case let .HoleEntry(hole): result.append(.HoleEntry(hole, theme: state.presentationData.theme)) diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 2e0646a76f..43f27b17ed 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -318,8 +318,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { if let strongSelf = self { if file.isAnimated { strongSelf.fetchDisposable.set(fetchedMediaResource(postbox: account.postbox, reference: AnyMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes)).start()) - } else { - strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, message: message, file: file, userInitiated: manual).start()) + } else { + strongSelf.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, message: message, file: file, userInitiated: manual).start()) } } }, cancel: { diff --git a/TelegramUI/DeclareEncodables.swift b/TelegramUI/DeclareEncodables.swift index 08bd1770f8..5fc61daeea 100644 --- a/TelegramUI/DeclareEncodables.swift +++ b/TelegramUI/DeclareEncodables.swift @@ -26,6 +26,7 @@ private var telegramUIDeclaredEncodables: Void = { declareEncodable(CachedChannelAdminIds.self, f: { CachedChannelAdminIds(decoder: $0) }) declareEncodable(StickerSettings.self, f: { StickerSettings(decoder: $0) }) declareEncodable(InstantPagePresentationSettings.self, f: { InstantPagePresentationSettings(decoder: $0) }) + declareEncodable(WatchPresetSettings.self, f: { WatchPresetSettings(decoder: $0) }) return }() diff --git a/TelegramUI/FetchMediaUtils.swift b/TelegramUI/FetchMediaUtils.swift index 7ef252ede8..6501f1f7ee 100644 --- a/TelegramUI/FetchMediaUtils.swift +++ b/TelegramUI/FetchMediaUtils.swift @@ -3,7 +3,7 @@ import TelegramCore import Postbox import SwiftSignalKit -func freeMediaFileInteractiveFetched(account: Account, fileReference: FileMediaReference) -> Signal { +public func freeMediaFileInteractiveFetched(account: Account, fileReference: FileMediaReference) -> Signal { return fetchedMediaResource(postbox: account.postbox, reference: fileReference.resourceReference(fileReference.media.resource)) } @@ -25,7 +25,7 @@ private func fetchCategoryForFile(_ file: TelegramMediaFile) -> FetchManagerCate } } -func messageMediaFileInteractiveFetched(account: Account, message: Message, file: TelegramMediaFile, userInitiated: Bool) -> Signal { +public func messageMediaFileInteractiveFetched(account: Account, message: Message, file: TelegramMediaFile, userInitiated: Bool) -> Signal { let mediaReference = AnyMediaReference.message(message: MessageReference(message), media: file) return account.telegramApplicationContext.fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(message.id.peerId), locationKey: .messageId(message.id), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: userInitiated) } @@ -34,7 +34,7 @@ func messageMediaFileCancelInteractiveFetch(account: Account, messageId: Message account.telegramApplicationContext.fetchManager.cancelInteractiveFetches(category: fetchCategoryForFile(file), location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: file.resource) } -func messageMediaImageInteractiveFetched(account: Account, message: Message, image: TelegramMediaImage, resource: MediaResource, storeToDownloadsPeerType: AutomaticMediaDownloadPeerType?) -> Signal { +public func messageMediaImageInteractiveFetched(account: Account, message: Message, image: TelegramMediaImage, resource: MediaResource, storeToDownloadsPeerType: AutomaticMediaDownloadPeerType?) -> Signal { let mediaReference = AnyMediaReference.message(message: MessageReference(message), media: image) return account.telegramApplicationContext.fetchManager.interactivelyFetched(category: .image, location: .chat(message.id.peerId), locationKey: .messageId(message.id), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(resource), statsCategory: .image, elevatedPriority: false, userInitiated: true, storeToDownloadsPeerType: storeToDownloadsPeerType) } diff --git a/TelegramUI/ItemListController.swift b/TelegramUI/ItemListController.swift index 5e1d69e6b4..5731c7ac63 100644 --- a/TelegramUI/ItemListController.swift +++ b/TelegramUI/ItemListController.swift @@ -20,6 +20,7 @@ enum ItemListNavigationButtonStyle { enum ItemListNavigationButtonContentIcon { case search + case add } enum ItemListNavigationButtonContent: Equatable { @@ -271,6 +272,8 @@ class ItemListController: ViewController { switch icon { case .search: image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.theme) + case .add: + image = PresentationResourcesRootController.navigationAddIcon(controllerState.theme) } item = UIBarButtonItem(image: image, style: leftNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.leftNavigationButtonPressed)) } @@ -324,6 +327,8 @@ class ItemListController: ViewController { switch icon { case .search: image = PresentationResourcesRootController.navigationCompactSearchIcon(controllerState.theme) + case .add: + image = PresentationResourcesRootController.navigationAddIcon(controllerState.theme) } item = UIBarButtonItem(image: image, style: style.barButtonItemStyle, target: strongSelf, action: action) } diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index e0db4099fd..bbcf278607 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -79,8 +79,8 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let removePeer: (PeerId) -> Void let toggleUpdated: ((Bool) -> Void)? - - init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, account: Account, peer: Peer, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil) { + let hasTopStripe: Bool + init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, account: Account, peer: Peer, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, hasTopStripe: Bool = true) { self.theme = theme self.strings = strings self.dateTimeFormat = dateTimeFormat @@ -100,6 +100,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer self.toggleUpdated = toggleUpdated + self.hasTopStripe = hasTopStripe } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { @@ -507,7 +508,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: - strongSelf.topStripeNode.isHidden = false + strongSelf.topStripeNode.isHidden = !item.hasTopStripe } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat diff --git a/TelegramUI/LegacyBridgeAudio.swift b/TelegramUI/LegacyBridgeAudio.swift new file mode 100644 index 0000000000..3a0d9b2744 --- /dev/null +++ b/TelegramUI/LegacyBridgeAudio.swift @@ -0,0 +1,26 @@ +import Foundation +import SwiftSignalKit + +import TelegramUIPrivateModule + +public func legacyDecodeOpusAudio(path: String, outputPath: String) -> Signal { + return Signal { subscriber in + let decoder = TGBridgeAudioDecoder(url: URL(fileURLWithPath: path), outputUrl: URL(fileURLWithPath: outputPath)) + decoder?.start(completion: { + subscriber.putNext(outputPath) + subscriber.putCompletion() + }) + return EmptyDisposable + } +} + +public func legacyEncodeOpusAudio(path: String) -> Signal<(Data?, Int32), NoError> { + return Signal { subscriber in + let encoder = TGBridgeAudioEncoder(url: URL(fileURLWithPath: path)) + encoder?.start(completion: { (dataItem, duration) in + subscriber.putNext((dataItem?.data(), duration)) + subscriber.putCompletion() + }) + return EmptyDisposable + } +} diff --git a/TelegramUI/NotificationExceptions.swift b/TelegramUI/NotificationExceptions.swift new file mode 100644 index 0000000000..0c7a52f756 --- /dev/null +++ b/TelegramUI/NotificationExceptions.swift @@ -0,0 +1,995 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + + +private final class NotificationExceptionArguments { + let account: Account + let activateSearch:()->Void + let changeNotifications: (PeerId, TelegramPeerNotificationSettings) -> Void + let selectPeer: ()->Void + init(account: Account, activateSearch:@escaping() -> Void, changeNotifications: @escaping(PeerId, TelegramPeerNotificationSettings) -> Void, selectPeer: @escaping()->Void) { + self.account = account + self.activateSearch = activateSearch + self.changeNotifications = changeNotifications + self.selectPeer = selectPeer + } +} + +private enum NotificationExceptionEntryId: Hashable { + case search + case peerId(Int64) + + var hashValue: Int { + switch self { + case .search: + return 0 + case let .peerId(peerId): + return peerId.hashValue + } + } + + static func <(lhs: NotificationExceptionEntryId, rhs: NotificationExceptionEntryId) -> Bool { + return lhs.hashValue < rhs.hashValue + } + + static func ==(lhs: NotificationExceptionEntryId, rhs: NotificationExceptionEntryId) -> Bool { + switch lhs { + case .search: + switch rhs { + case .search: + return true + default: + return false + } + case let .peerId(lhsId): + switch rhs { + case let .peerId(rhsId): + return lhsId == rhsId + default: + return false + } + } + } +} + +private enum NotificationExceptionSectionId : ItemListSectionId { + case general = 0 +} + +private enum NotificationExceptionEntry : ItemListNodeEntry { + + + var section: ItemListSectionId { + return NotificationExceptionSectionId.general.rawValue + } + + typealias ItemGenerationArguments = NotificationExceptionArguments + + case search(PresentationTheme, PresentationStrings) + case peer(Int, Peer, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, String, TelegramPeerNotificationSettings) + + + func item(_ arguments: NotificationExceptionArguments) -> ListViewItem { + switch self { + case let .search(theme, strings): + return NotificationSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: { + arguments.activateSearch() + }) + case let .peer(_, peer, theme, strings, dateTimeFormat, value, settings): + return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, account: arguments.account, peer: peer, presence: nil, text: .text(value), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: { + arguments.changeNotifications(peer.id, settings) + }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in + + }, removePeer: { peerId in + + }, hasTopStripe: false) + } + } + + var stableId: NotificationExceptionEntryId { + switch self { + case .search: + return .search + case let .peer(_, peer, _, _, _, _, _): + return .peerId(peer.id.toInt64()) + } + } + + static func == (lhs: NotificationExceptionEntry, rhs: NotificationExceptionEntry) -> Bool { + switch lhs { + case let .search(lhsTheme, lhsStrings): + switch rhs { + case let .search(rhsTheme, rhsStrings): + return lhsTheme === rhsTheme && lhsStrings === rhsStrings + default: + return false + } + case let .peer(lhsIndex, lhsPeer, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsValue, lhsSettings): + switch rhs { + case let .peer(rhsIndex, rhsPeer, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsValue, rhsSettings): + return lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsDateTimeFormat == rhsDateTimeFormat && lhsIndex == rhsIndex && lhsPeer.isEqual(rhsPeer) && lhsValue == rhsValue && lhsSettings == rhsSettings + default: + return false + } + } + } + + static func <(lhs: NotificationExceptionEntry, rhs: NotificationExceptionEntry) -> Bool { + switch lhs { + case .search: + return true + case let .peer(lhsIndex, _, _, _, _, _, _): + switch rhs { + case .search: + return false + case let .peer(rhsIndex, _, _, _, _, _, _): + return lhsIndex < rhsIndex + } + } + } +} + + + +private final class NotificationExceptionState : Equatable { + + let mode:NotificationExceptionMode + let isSearchMode: Bool + init(mode: NotificationExceptionMode, isSearchMode: Bool = false) { + self.mode = mode + self.isSearchMode = isSearchMode + } + + func withUpdatedSearchMode(_ isSearchMode: Bool) -> NotificationExceptionState { + return NotificationExceptionState.init(mode: mode, isSearchMode: isSearchMode) + } + + func withUpdatedPeerIdSound(_ peerId: PeerId, _ sound: PeerMessageSound) -> NotificationExceptionState { + return NotificationExceptionState(mode: mode.withUpdatedPeerIdSound(peerId, sound), isSearchMode: isSearchMode) + } + func withUpdatedPeerIdMuteInterval(_ peerId: PeerId, _ muteInterval: Int32?) -> NotificationExceptionState { + return NotificationExceptionState(mode: mode.withUpdatedPeerIdMuteInterval(peerId, muteInterval), isSearchMode: isSearchMode) + } + + static func == (lhs: NotificationExceptionState, rhs: NotificationExceptionState) -> Bool { + return lhs.mode == rhs.mode && lhs.isSearchMode == rhs.isSearchMode + } +} + + +public struct NotificationExceptionWrapper : Equatable { + let settings: TelegramPeerNotificationSettings + let date: TimeInterval? + init(settings: TelegramPeerNotificationSettings, date: TimeInterval? = nil) { + self.settings = settings + self.date = date + } + + func withUpdatedSettings(_ settings: TelegramPeerNotificationSettings) -> NotificationExceptionWrapper { + return NotificationExceptionWrapper(settings: settings, date: self.date) + } + + func updateSettings(_ f: (TelegramPeerNotificationSettings) -> TelegramPeerNotificationSettings) -> NotificationExceptionWrapper { + return NotificationExceptionWrapper(settings: f(self.settings), date: self.date) + } + + + func withUpdatedDate(_ date: TimeInterval) -> NotificationExceptionWrapper { + return NotificationExceptionWrapper(settings: self.settings, date: date) + } +} + +public enum NotificationExceptionMode : Equatable { + public static func == (lhs: NotificationExceptionMode, rhs: NotificationExceptionMode) -> Bool { + switch lhs { + case let .users(lhsValue): + if case let .users(rhsValue) = rhs { + return lhsValue == rhsValue + } else { + return false + } + case let .groups(lhsValue): + if case let .groups(rhsValue) = rhs { + return lhsValue == rhsValue + } else { + return false + } + } + } + + case users([PeerId : NotificationExceptionWrapper]) + case groups([PeerId : NotificationExceptionWrapper]) + + func withUpdatedPeerIdSound(_ peerId: PeerId, _ sound: PeerMessageSound) -> NotificationExceptionMode { + let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMessageSound) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, sound in + var values = values + if let value = values[peerId] { + switch sound { + case .default: + switch value.settings.muteState { + case .default: + values.removeValue(forKey: peerId) + default: + values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)}) + } + default: + values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)}) + } + } else { + switch sound { + case .default: + break + default: + values[peerId] = NotificationExceptionWrapper(settings: TelegramPeerNotificationSettings(muteState: .default, messageSound: sound), date: Date().timeIntervalSince1970) + } + } + return values + } + + switch self { + case let .groups(values): + if peerId.namespace != Namespaces.Peer.CloudUser { + return .groups(apply(values, peerId, sound)) + } + case let .users(values): + if peerId.namespace == Namespaces.Peer.CloudUser { + return .users(apply(values, peerId, sound)) + } + } + + return self + } + + func withUpdatedPeerIdMuteInterval(_ peerId: PeerId, _ muteInterval: Int32?) -> NotificationExceptionMode { + + let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMuteState) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, muteState in + var values = values + if let value = values[peerId] { + switch muteState { + case .default: + switch value.settings.messageSound { + case .default: + values.removeValue(forKey: peerId) + default: + values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)}) + } + default: + values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)}) + } + } else { + switch muteState { + case .default: + break + default: + values[peerId] = NotificationExceptionWrapper.init(settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: .default), date: Date().timeIntervalSince1970) + } + } + return values + } + + let muteState: PeerMuteState + if let muteInterval = muteInterval { + if muteInterval == 0 { + muteState = .unmuted + } else { + let absoluteUntil: Int32 + if muteInterval == Int32.max { + absoluteUntil = Int32.max + } else { + absoluteUntil = Int32(Date().timeIntervalSince1970) + muteInterval + } + muteState = .muted(until: absoluteUntil) + } + } else { + muteState = .default + } + switch self { + case let .groups(values): + if peerId.namespace != Namespaces.Peer.CloudUser { + return .groups(apply(values, peerId, muteState)) + } + case let .users(values): + if peerId.namespace == Namespaces.Peer.CloudUser { + return .users(apply(values, peerId, muteState)) + } + } + + return self + } + + var peerIds: [PeerId] { + switch self { + case let .users(settings), let .groups(settings): + return settings.map {$0.key} + } + } + + var settings: [PeerId : NotificationExceptionWrapper] { + switch self { + case let .users(settings), let .groups(settings): + return settings + } + } +} + +private func notificationsExceptionEntries(presentationData: PresentationData, peers: [PeerId : Peer], state: NotificationExceptionState) -> [NotificationExceptionEntry] { + var entries: [NotificationExceptionEntry] = [] + + entries.append(.search(presentationData.theme, presentationData.strings)) + + + var index: Int = 0 + for (key, value) in state.mode.settings.sorted(by: { lhs, rhs in + let lhsName = peers[lhs.key]?.displayTitle ?? "" + let rhsName = peers[rhs.key]?.displayTitle ?? "" + + if let lhsDate = lhs.value.date, let rhsDate = rhs.value.date { + return lhsDate < rhsDate + } else if lhs.value.date != nil && rhs.value.date == nil { + return true + } else if lhs.value.date == nil && rhs.value.date != nil { + return false + } + + if let lhsPeer = peers[lhs.key] as? TelegramUser, let rhsPeer = peers[rhs.key] as? TelegramUser { + if lhsPeer.botInfo != nil && rhsPeer.botInfo == nil { + return false + } else if lhsPeer.botInfo == nil && rhsPeer.botInfo != nil { + return true + } + } + + return lhsName < rhsName + }) { + if let peer = peers[key], !peer.displayTitle.isEmpty { + var title: String + switch value.settings.muteState { + case .muted: + title = presentationData.strings.Notifications_ExceptionsMuted + case .unmuted: + title = presentationData.strings.Notifications_ExceptionsUnmuted + default: + title = "" + } + switch value.settings.messageSound { + case .default: + break + default: + title += (title.isEmpty ? "" : ", ") + localizedPeerNotificationSoundString(strings: presentationData.strings, sound: value.settings.messageSound) + } + entries.append(.peer(index, peer, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, title, value.settings)) + index += 1 + } + } + + return entries +} + +public func notificationExceptionsController(account: Account, mode: NotificationExceptionMode, updatedMode:@escaping(NotificationExceptionMode) -> Void) -> ViewController { + let statePromise = ValuePromise(NotificationExceptionState(mode: mode), ignoreRepeated: true) + let stateValue = Atomic(value: NotificationExceptionState(mode: mode)) + let updateState: ((NotificationExceptionState) -> NotificationExceptionState) -> Void = { f in + let result = stateValue.modify { f($0) } + statePromise.set(result) + updatedMode(result.mode) + } + + let globalValue: Atomic = Atomic(value: GlobalNotificationSettingsSet.defaultSettings) + + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + + + let presentationData = account.telegramApplicationContext.currentPresentationData.modify {$0} + + let updatePeerSound: (PeerId, PeerMessageSound) -> Void = { peerId, sound in + _ = updatePeerNotificationSoundInteractive(account: account, peerId: peerId, sound: sound).start(completed: { + updateState { value in + return value.withUpdatedPeerIdSound(peerId, sound) + } + }) + } + + let updatePeerNotificationInterval:(PeerId, Int32?) -> Void = { peerId, muteInterval in + _ = updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: muteInterval).start(completed: { + updateState { value in + return value.withUpdatedPeerIdMuteInterval(peerId, muteInterval) + } + }) + } + + var activateSearch:(()->Void)? + + + let arguments = NotificationExceptionArguments(account: account, activateSearch: { + activateSearch?() + }, changeNotifications: { peerId, settings in + + let globalSettings = globalValue.modify {$0} + + let isPrivateChat = peerId.namespace == Namespaces.Peer.CloudUser + + let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: isPrivateChat && globalSettings.privateChats.enabled || !isPrivateChat && globalSettings.groupChats.enabled ? presentationData.strings.UserInfo_NotificationsDefaultEnabled : presentationData.strings.UserInfo_NotificationsDefaultDisabled, color: .accent, action: { [weak actionSheet] in + updatePeerNotificationInterval(peerId, nil) + actionSheet?.dismissAnimated() + }), + ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + updatePeerNotificationInterval(peerId, 0) + }), + ActionSheetButtonItem(title: presentationData.strings.Notification_Mute1h, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + updatePeerNotificationInterval(peerId, 60 * 60) + }), + ActionSheetButtonItem(title: presentationData.strings.MuteFor_Days(2), color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + updatePeerNotificationInterval(peerId, 60 * 60 * 24 * 2) + }), + ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsDisable, color: .accent, action: { [weak actionSheet] in + updatePeerNotificationInterval(peerId, Int32.max) + actionSheet?.dismissAnimated() + }), + ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsChangeSound(localizedPeerNotificationSoundString(strings: presentationData.strings, sound: settings.messageSound)).0, color: .accent, action: { [weak actionSheet] in + let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.messageSound, defaultSound: isPrivateChat ? globalSettings.privateChats.sound : globalSettings.groupChats.sound, completion: { value in + updatePeerSound(peerId, value) + }) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + actionSheet?.dismissAnimated() + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, nil) + }, selectPeer: { + let filter: ChatListNodePeersFilter + switch mode { + case .groups: + filter = [.withoutSecretChats] + case .users: + filter = [.withoutSecretChats] + } + let controller = PeerSelectionController(account: account, filter: filter, title: presentationData.strings.Notifications_AddExceptionTitle) + controller.peerSelected = { [weak controller] peerId in + controller?.dismiss() + + let settingsSignal = account.postbox.transaction { transaction in + return transaction.getPeerNotificationSettings(peerId) + } |> deliverOnMainQueue + + _ = settingsSignal.start(next: { settings in + if let settings = settings as? TelegramPeerNotificationSettings { + let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + + var items: [ActionSheetButtonItem] = [] + + switch settings.muteState { + case .default, .muted: + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + updatePeerNotificationInterval(peerId, 0) + })) + default: + break + } + + items.append(ActionSheetButtonItem(title: presentationData.strings.Notification_Mute1h, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + updatePeerNotificationInterval(peerId, 60 * 60) + })) + items.append(ActionSheetButtonItem(title: presentationData.strings.MuteFor_Days(2), color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + updatePeerNotificationInterval(peerId, 60 * 60 * 24 * 2) + })) + + switch settings.muteState { + case .default, .unmuted: + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsDisable, color: .accent, action: { [weak actionSheet] in + updatePeerNotificationInterval(peerId, Int32.max) + actionSheet?.dismissAnimated() + })) + default: + break + } + + items.append(ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsChangeSound(localizedPeerNotificationSoundString(strings: presentationData.strings, sound: settings.messageSound)).0, color: .accent, action: { [weak actionSheet] in + let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.messageSound, defaultSound: nil, completion: { value in + updatePeerSound(peerId, value) + }) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + actionSheet?.dismissAnimated() + })) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, nil) + } + }) + + + + + + } + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }) + + let peersSignal:Signal<[PeerId : Peer], NoError> = statePromise.get() |> mapToSignal { state in + return account.postbox.transaction { transaction -> [PeerId : Peer] in + var peers:[PeerId : Peer] = [:] + for peerId in state.mode.peerIds { + if let peer = transaction.getPeer(peerId) { + peers[peerId] = peer + } + } + return peers + } + } + + let preferences = account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) + + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peersSignal, preferences) + |> map { presentationData, state, peers, prefs -> (ItemListControllerState, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)) in + + _ = globalValue.swap((prefs.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings)?.effective ?? GlobalNotificationSettingsSet.defaultSettings) + + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Notifications_ExceptionsTitle), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: nil) + let listState = ItemListNodeState(entries: notificationsExceptionEntries(presentationData: presentationData, peers: peers, state: state), style: .plain, searchItem: nil) + + return (controllerState, (listState, arguments)) + } + + let controller = NotificationExceptionsController(account: account, state: signal, addAction: { + arguments.selectPeer() + }) + +// let controller = ItemListController(account: account, state: signal |> afterDisposed { +// actionsDisposable.dispose() +// }) + + + activateSearch = { [weak controller] in +// updateState { state in +// return state.withUpdatedSearchMode(true) +// } + controller?.activateSearch() + } + + + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + return controller +} + + + private final class NotificationExceptionsController: ViewController { + private let account: Account + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + var peerSelected: ((PeerId) -> Void)? + + var inProgress: Bool = false { + didSet { + if self.inProgress != oldValue { + if self.isNodeLoaded { + self.controllerNode.inProgress = self.inProgress + } + + if self.inProgress { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: self.presentationData.theme)) + } else { + self.navigationItem.rightBarButtonItem = nil + } + } + } + } + + private var controllerNode: NotificationExceptionsControllerNode { + return super.displayNode as! NotificationExceptionsControllerNode + } + + + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + private let addAction:()->Void + + private let state: Signal<(ItemListControllerState, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError> + + public init(account: Account, state: Signal<(ItemListControllerState, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError>, addAction: @escaping()->Void) { + self.account = account + self.state = state + self.addAction = addAction + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.title = self.presentationData.strings.Notifications_ExceptionsTitle + + + self.scrollToTop = { [weak self] in + if let strongSelf = self { + strongSelf.controllerNode.scrollToTop() + } + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func addExceptionAction() { + self.addAction() + } + + override public func loadDisplayNode() { + let image = PresentationResourcesRootController.navigationAddIcon(presentationData.theme) + + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: image, style: UIBarButtonItem.Style.plain, target: self, action: #selector(addExceptionAction)) + + let nodeState = self.state |> deliverOnMainQueue |> map { ($0.theme, $1) } + + self.displayNode = NotificationExceptionsControllerNode(account: self.account, navigationBar: self.navigationBar!, state: nodeState) + self.displayNode.backgroundColor = .white + + self.controllerNode.navigationBar = self.navigationBar + + self.controllerNode.requestDeactivateSearch = { [weak self] in + self?.deactivateSearch() + } + + self.controllerNode.requestActivateSearch = { [weak self] in + self?.activateSearch() + } + + self.displayNodeDidLoad() + + self._ready.set(self.controllerNode.ready) + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + // self.controllerNode.animateIn() + } + + override public func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + @objc func cancelPressed() { + self.dismiss() + } + + func activateSearch() { + if self.displayNavigationBar { + if let scrollToTop = self.scrollToTop { + scrollToTop() + } + self.controllerNode.activateSearch() + self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + private func deactivateSearch() { + if !self.displayNavigationBar { + self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + self.controllerNode.deactivateSearch() + } + } +} + + + +private final class NotificationExceptionsControllerNode: ASDisplayNode { + private let account: Account + + var inProgress: Bool = false { + didSet { + + } + } + + var navigationBar: NavigationBar? + + + private let contentNode: ItemListControllerNode + + private var contactListActive = false + + private var searchDisplayController: SearchDisplayController? + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + var requestActivateSearch: (() -> Void)? + var requestDeactivateSearch: (() -> Void)? + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private var readyValue = Promise() + var ready: Signal { + return self.readyValue.get() + } + + private let state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError> + + init(account: Account, navigationBar: NavigationBar, state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError>) { + self.account = account + self.state = state + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + + self.contentNode = ItemListControllerNode(navigationBar: navigationBar, updateNavigationOffset: { _ in + + }, state: state) + + super.init() + + self.setViewBlock({ + return UITracingLayerView() + }) + + self.addSubnode(self.contentNode) + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + strongSelf.presentationData = presentationData + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() + } + } + }) + + + + self.readyValue.set(contentNode.ready) + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + let cleanInsets = layout.insets(options: []) + + + var controlSize = CGSize(width: 0, height:0) + controlSize.width = min(layout.size.width, max(200.0, controlSize.width)) + + var insets = layout.insets(options: [.input]) + insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) + insets.bottom = max(insets.bottom, cleanInsets.bottom) + insets.left += layout.safeInsets.left + insets.right += layout.safeInsets.right + + self.contentNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.contentNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + self.contentNode.containerLayoutUpdated(layout, navigationBarHeight: insets.top, transition: transition) + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + } + + func activateSearch() { + guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar else { + return + } + + if self.contentNode.supernode != nil { + var maybePlaceholderNode: SearchBarPlaceholderNode? + self.contentNode.listNode.forEachItemNode { node in + if let node = node as? NotificationSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + if let _ = self.searchDisplayController { + return + } + + if let placeholderNode = maybePlaceholderNode { + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: NotificationExceptionsSearchControllerContentNode(account: account, navigationBar: navigationBar, state: self.state), cancel: { [weak self] in + self?.requestDeactivateSearch?() + }) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { subnode in + self.insertSubnode(subnode, belowSubnode: navigationBar) + }, placeholder: placeholderNode) + } + } + } + + func deactivateSearch() { + if let searchDisplayController = self.searchDisplayController { + if self.contentNode.supernode != nil { + var maybePlaceholderNode: SearchBarPlaceholderNode? + self.contentNode.listNode.forEachItemNode { node in + if let node = node as? NotificationSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + searchDisplayController.deactivate(placeholder: maybePlaceholderNode) + self.searchDisplayController = nil + } + } + } + + func scrollToTop() { + if self.contentNode.supernode != nil { + // self.contentNode.scrollToPosition(.top) + } + } + + +} + + + + +final class NotificationExceptionsSearchControllerContentNode: SearchDisplayControllerContentNode { + private let account: Account + + private let listNode: ItemListControllerNode + private let dimNode: ASDisplayNode + private var validLayout: ContainerViewLayout? + + + private let searchQuery = Promise() + private let searchDisposable = MetaDisposable() + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let presentationDataPromise: Promise + + private let _isSearching = ValuePromise(false, ignoreRepeated: true) + override var isSearching: Signal { + return self._isSearching.get() + } + + private let state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError> + + + init(account: Account, navigationBar: NavigationBar, state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError>) { + self.account = account + self.state = state + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations)) + + self.listNode = ItemListControllerNode(navigationBar: navigationBar, updateNavigationOffset: { _ in + + }, state: searchQuery.get() |> mapToSignal { query in + return state |> map { values in + var values = values + let entries = values.1.0.entries.filter { entry in + switch entry { + case .search: + return false + case let .peer(_, peer, _, _, _, _, _): + if let query = query { + return !peer.displayTitle.components(separatedBy: " ").filter({$0.lowercased().hasPrefix(query.lowercased())}).isEmpty && !query.isEmpty + } else { + return false + } + } + } + values.1.0 = ItemListNodeState(entries: entries, style: values.1.0.style, focusItemTag: nil, emptyStateItem: nil, searchItem: nil, crossfadeState: false, animateChanges: false) + return values + } + }) + + + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) + + super.init() + + + self.addSubnode(self.dimNode) + self.addSubnode(self.listNode) + self.listNode.isHidden = true + + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme { + strongSelf.updateTheme(theme: presentationData.theme) + } + } + }) + + } + + deinit { + self.searchDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + private func updateTheme(theme: PresentationTheme) { + self.backgroundColor = theme.chatList.backgroundColor + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } + + override func searchTextUpdated(text: String) { + if text.isEmpty { + self.searchQuery.set(.single(nil)) + self.listNode.isHidden = true + } else { + self.searchQuery.set(.single(text)) + self.listNode.isHidden = false + } + + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + self.validLayout = layout + + + let topInset = navigationBarHeight + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) + + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.containerLayoutUpdated(layout, navigationBarHeight: 28, transition: transition) + } + +} diff --git a/TelegramUI/NotificationSearchItem.swift b/TelegramUI/NotificationSearchItem.swift new file mode 100644 index 0000000000..033f9f623f --- /dev/null +++ b/TelegramUI/NotificationSearchItem.swift @@ -0,0 +1,130 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import Display +import SwiftSignalKit + +private let searchBarFont = Font.regular(14.0) + +class NotificationSearchItem: ListViewItem, ItemListItem { + let selectable: Bool = false + + var sectionId: ItemListSectionId { + return 0 + } + var tag: ItemListItemTag? { + return nil + } + var requestsNoInset: Bool { + return true + } + + let theme: PresentationTheme + private let placeholder: String + private let activate: () -> Void + + init(theme: PresentationTheme, placeholder: String, activate: @escaping () -> Void) { + self.theme = theme + self.placeholder = placeholder + self.activate = activate + } + + func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = NotificationSearchItemNode() + node.placeholder = self.placeholder + + let makeLayout = node.asyncLayout() + let (layout, apply) = makeLayout(self, params) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + node.activate = self.activate + Queue.mainQueue().async { + completion(node, { + return (nil, { + apply(false) + }) + }) + } + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? NotificationSearchItemNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params) + Queue.mainQueue().async { + completion(nodeLayout, { + apply(animation.isAnimated) + }) + } + } + } + } + } +} + +class NotificationSearchItemNode: ListViewItemNode { + let searchBarNode: SearchBarPlaceholderNode + var placeholder: String? + + fileprivate var activate: (() -> Void)? { + didSet { + self.searchBarNode.activate = self.activate + } + } + + required init() { + self.searchBarNode = SearchBarPlaceholderNode() + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.searchBarNode) + } + + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + let makeLayout = self.asyncLayout() + let (layout, apply) = makeLayout(item as! NotificationSearchItem, params) + apply(false) + self.contentSize = layout.contentSize + self.insets = layout.insets + } + + func asyncLayout() -> (_ item: NotificationSearchItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { + let searchBarNodeLayout = self.searchBarNode.asyncLayout() + let placeholder = self.placeholder + + return { item, params in + let baseWidth = params.width - params.leftInset - params.rightInset + + let backgroundColor = item.theme.chatList.itemBackgroundColor + + let searchBarApply = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93)), CGSize(width: baseWidth - 16.0, height: CGFloat.greatestFiniteMagnitude), UIColor(rgb: 0x8e8e93), item.theme.chatList.regularSearchBarColor, backgroundColor) + + let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets()) + + return (layout, { [weak self] animated in + if let strongSelf = self { + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .easeInOut) + } else { + transition = .immediate + } + + strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 8.0, y: 8.0), size: CGSize(width: baseWidth - 16.0, height: 28.0)) + searchBarApply() + + strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - 16.0, height: 28.0)) + + transition.updateBackgroundColor(node: strongSelf, color: backgroundColor) + } + }) + } + } +} diff --git a/TelegramUI/NotificationsAndSounds.swift b/TelegramUI/NotificationsAndSounds.swift index 48f992497d..6afb788ad3 100644 --- a/TelegramUI/NotificationsAndSounds.swift +++ b/TelegramUI/NotificationsAndSounds.swift @@ -6,7 +6,8 @@ import TelegramCore private final class NotificationsAndSoundsArguments { let account: Account - let presentController: (ViewController, ViewControllerPresentationArguments) -> Void + let presentController: (ViewController, ViewControllerPresentationArguments?) -> Void + let pushController: (ViewController) -> Void let soundSelectionDisposable: MetaDisposable let updateMessageAlerts: (Bool) -> Void @@ -27,9 +28,12 @@ private final class NotificationsAndSoundsArguments { let resetNotifications: () -> Void - init(account: Account, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, soundSelectionDisposable: MetaDisposable, updateMessageAlerts: @escaping (Bool) -> Void, updateMessagePreviews: @escaping (Bool) -> Void, updateMessageSound: @escaping (PeerMessageSound) -> Void, updateGroupAlerts: @escaping (Bool) -> Void, updateGroupPreviews: @escaping (Bool) -> Void, updateGroupSound: @escaping (PeerMessageSound) -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateDisplayNameOnLockscreen: @escaping (Bool) -> Void, updateTotalUnreadCountStyle: @escaping (Bool) -> Void, updateTotalUnreadCountCategory: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void) { + let updatedExceptionMode: (NotificationExceptionMode) -> Void + + init(account: Account, presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, pushController: @escaping(ViewController)->Void, soundSelectionDisposable: MetaDisposable, updateMessageAlerts: @escaping (Bool) -> Void, updateMessagePreviews: @escaping (Bool) -> Void, updateMessageSound: @escaping (PeerMessageSound) -> Void, updateGroupAlerts: @escaping (Bool) -> Void, updateGroupPreviews: @escaping (Bool) -> Void, updateGroupSound: @escaping (PeerMessageSound) -> Void, updateInAppSounds: @escaping (Bool) -> Void, updateInAppVibration: @escaping (Bool) -> Void, updateInAppPreviews: @escaping (Bool) -> Void, updateDisplayNameOnLockscreen: @escaping (Bool) -> Void, updateTotalUnreadCountStyle: @escaping (Bool) -> Void, updateTotalUnreadCountCategory: @escaping (Bool) -> Void, resetNotifications: @escaping () -> Void, updatedExceptionMode: @escaping(NotificationExceptionMode) -> Void) { self.account = account self.presentController = presentController + self.pushController = pushController self.soundSelectionDisposable = soundSelectionDisposable self.updateMessageAlerts = updateMessageAlerts self.updateMessagePreviews = updateMessagePreviews @@ -44,6 +48,7 @@ private final class NotificationsAndSoundsArguments { self.updateTotalUnreadCountStyle = updateTotalUnreadCountStyle self.updateTotalUnreadCountCategory = updateTotalUnreadCountCategory self.resetNotifications = resetNotifications + self.updatedExceptionMode = updatedExceptionMode } } @@ -61,12 +66,15 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { case messageAlerts(PresentationTheme, String, Bool) case messagePreviews(PresentationTheme, String, Bool) case messageSound(PresentationTheme, String, String, PeerMessageSound) + case userExceptions(PresentationTheme, PresentationStrings, String, NotificationExceptionMode) + case messageNotice(PresentationTheme, String) case groupHeader(PresentationTheme, String) case groupAlerts(PresentationTheme, String, Bool) case groupPreviews(PresentationTheme, String, Bool) case groupSound(PresentationTheme, String, String, PeerMessageSound) + case groupExceptions(PresentationTheme, PresentationStrings, String, NotificationExceptionMode) case groupNotice(PresentationTheme, String) case inAppHeader(PresentationTheme, String) @@ -87,9 +95,9 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .messageHeader, .messageAlerts, .messagePreviews, .messageSound, .messageNotice: + case .messageHeader, .messageAlerts, .messagePreviews, .messageSound, .messageNotice, .userExceptions: return NotificationsAndSoundsSection.messages.rawValue - case .groupHeader, .groupAlerts, .groupPreviews, .groupSound, .groupNotice: + case .groupHeader, .groupAlerts, .groupPreviews, .groupSound, .groupNotice, .groupExceptions: return NotificationsAndSoundsSection.groups.rawValue case .inAppHeader, .inAppSounds, .inAppVibrate, .inAppPreviews: return NotificationsAndSoundsSection.inApp.rawValue @@ -112,42 +120,46 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { return 2 case .messageSound: return 3 - case .messageNotice: + case .userExceptions: return 4 - case .groupHeader: + case .messageNotice: return 5 - case .groupAlerts: + case .groupHeader: return 6 - case .groupPreviews: + case .groupAlerts: return 7 - case .groupSound: + case .groupPreviews: return 8 - case .groupNotice: + case .groupSound: return 9 - case .inAppHeader: + case .groupExceptions: return 10 - case .inAppSounds: + case .groupNotice: return 11 - case .inAppVibrate: + case .inAppHeader: return 12 - case .inAppPreviews: + case .inAppSounds: return 13 - case .displayNamesOnLockscreen: + case .inAppVibrate: return 14 - case .displayNamesOnLockscreenInfo: + case .inAppPreviews: return 15 - case .badgeHeader: + case .displayNamesOnLockscreen: return 16 - case .unreadCountStyle: + case .displayNamesOnLockscreenInfo: return 17 - case .unreadCountCategory: + case .badgeHeader: return 18 - case .unreadCountCategoryInfo: + case .unreadCountStyle: return 19 - case .reset: + case .unreadCountCategory: return 20 - case .resetNotice: + case .unreadCountCategoryInfo: return 21 + case .reset: + return 22 + case .resetNotice: + return 23 } } @@ -177,6 +189,12 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { } else { return false } + case let .userExceptions(lhsTheme, lhsStrings, lhsText, lhsValue): + if case let .userExceptions(rhsTheme, rhsStrings, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .messageNotice(lhsTheme, lhsText): if case let .messageNotice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -207,6 +225,12 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { } else { return false } + case let .groupExceptions(lhsTheme, lhsStrings, lhsText, lhsValue): + if case let .groupExceptions(rhsTheme, rhsStrings, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .groupNotice(lhsTheme, lhsText): if case let .groupNotice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -309,7 +333,12 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { let controller = notificationSoundSelectionController(account: arguments.account, isModal: true, currentSound: sound, defaultSound: nil, completion: { [weak arguments] value in arguments?.updateMessageSound(value) }) - arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + arguments.presentController(controller, nil) + }) + case let .userExceptions(theme, strings, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: strings.Notifications_Exceptions(Int32(value.settings.count)), sectionId: self.section, style: .blocks, action: { + let controller = notificationExceptionsController(account: arguments.account, mode: value, updatedMode: arguments.updatedExceptionMode) + arguments.pushController(controller) }) case let .messageNotice(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) @@ -330,6 +359,11 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { }) arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) + case let .groupExceptions(theme, strings, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: strings.Notifications_Exceptions(Int32(value.settings.count)), sectionId: self.section, style: .blocks, action: { + let controller = notificationExceptionsController(account: arguments.account, mode: value, updatedMode: arguments.updatedExceptionMode) + arguments.pushController(controller) + }) case let .groupNotice(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .inAppHeader(theme, text): @@ -382,19 +416,21 @@ private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound } } -private func notificationsAndSoundsEntries(globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, presentationData: PresentationData) -> [NotificationsAndSoundsEntry] { +private func notificationsAndSoundsEntries(globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, exceptions: (NotificationExceptionMode, NotificationExceptionMode), presentationData: PresentationData) -> [NotificationsAndSoundsEntry] { var entries: [NotificationsAndSoundsEntry] = [] entries.append(.messageHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotifications)) entries.append(.messageAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.privateChats.enabled)) entries.append(.messagePreviews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, globalSettings.privateChats.displayPreviews)) entries.append(.messageSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(globalSettings.privateChats.sound)), filteredGlobalSound(globalSettings.privateChats.sound))) + entries.append(.userExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.0)) entries.append(.messageNotice(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsHelp)) entries.append(.groupHeader(presentationData.theme, presentationData.strings.Notifications_GroupNotifications)) entries.append(.groupAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.groupChats.enabled)) entries.append(.groupPreviews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, globalSettings.groupChats.displayPreviews)) entries.append(.groupSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(globalSettings.groupChats.sound)), filteredGlobalSound(globalSettings.groupChats.sound))) + entries.append(.groupExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.1)) entries.append(.groupNotice(presentationData.theme, presentationData.strings.Notifications_GroupNotificationsHelp)) entries.append(.inAppHeader(presentationData.theme, presentationData.strings.Notifications_InAppNotifications)) @@ -418,9 +454,20 @@ private func notificationsAndSoundsEntries(globalSettings: GlobalNotificationSet public func notificationsAndSoundsController(account: Account) -> ViewController { var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? + + + + let notificationExceptions: Promise<(NotificationExceptionMode, NotificationExceptionMode)> = Promise() + + let updateNotificationExceptions:((NotificationExceptionMode, NotificationExceptionMode)) -> Void = { value in + notificationExceptions.set(.single(value)) + } let arguments = NotificationsAndSoundsArguments(account: account, presentController: { controller, arguments in presentControllerImpl?(controller, arguments) + }, pushController: { controller in + pushControllerImpl?(controller) }, soundSelectionDisposable: MetaDisposable(), updateMessageAlerts: { value in let _ = updateGlobalNotificationSettingsInteractively(postbox: account.postbox, { settings in return settings.withUpdatedPrivateChats { @@ -516,12 +563,57 @@ public func notificationsAndSoundsController(account: Account) -> ViewController }) ])]) presentControllerImpl?(actionSheet, nil) + }, updatedExceptionMode: { mode in + _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups) in + switch mode { + case .users: + updateNotificationExceptions((mode, groups)) + case .groups: + updateNotificationExceptions((users, mode)) + } + }) }) let preferences = account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications, ApplicationSpecificPreferencesKeys.inAppNotificationSettings]) - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, preferences) - |> map { presentationData, view -> (ItemListControllerState, (ItemListNodeState, NotificationsAndSoundsEntry.ItemGenerationArguments)) in + + + notificationExceptions.set(account.postbox.transaction{ transaction -> (NotificationExceptionMode, NotificationExceptionMode) in + let allSettings = transaction.getAllPeerNotificationSettings() ?? [:] + var users:[PeerId : NotificationExceptionWrapper] = [:] + var groups: [PeerId : NotificationExceptionWrapper] = [:] + + for (key, value) in allSettings { + if let value = value as? TelegramPeerNotificationSettings { + switch value.muteState { + case .default: + switch value.messageSound { + case .default: + break + default: + switch key.namespace { + case Namespaces.Peer.CloudUser: + users[key] = NotificationExceptionWrapper(settings: value) + default: + groups[key] = NotificationExceptionWrapper(settings: value) + } + } + default: + switch key.namespace { + case Namespaces.Peer.CloudUser: + users[key] = NotificationExceptionWrapper(settings: value) + default: + groups[key] = NotificationExceptionWrapper(settings: value) + } + } + } + + } + return (.users(users), .groups(groups)) + }) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, preferences, notificationExceptions.get()) + |> map { presentationData, view, exceptions -> (ItemListControllerState, (ItemListNodeState, NotificationsAndSoundsEntry.ItemGenerationArguments)) in let viewSettings: GlobalNotificationSettingsSet if let settings = view.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { @@ -538,7 +630,7 @@ public func notificationsAndSoundsController(account: Account) -> ViewController } let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Notifications_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(entries: notificationsAndSoundsEntries(globalSettings: viewSettings, inAppSettings: inAppSettings, presentationData: presentationData), style: .blocks) + let listState = ItemListNodeState(entries: notificationsAndSoundsEntries(globalSettings: viewSettings, inAppSettings: inAppSettings, exceptions: exceptions, presentationData: presentationData), style: .blocks) return (controllerState, (listState, arguments)) } @@ -547,5 +639,8 @@ public func notificationsAndSoundsController(account: Account) -> ViewController presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window(.root), with: a) } + pushControllerImpl = { [weak controller] c in + (controller?.navigationController as? NavigationController)?.pushViewController(c) + } return controller } diff --git a/TelegramUI/PeerSelectionController.swift b/TelegramUI/PeerSelectionController.swift index 2e2861640d..5ef8445050 100644 --- a/TelegramUI/PeerSelectionController.swift +++ b/TelegramUI/PeerSelectionController.swift @@ -40,7 +40,7 @@ public final class PeerSelectionController: ViewController { return self._ready } - public init(account: Account, filter: ChatListNodePeersFilter = [.onlyWriteable]) { + public init(account: Account, filter: ChatListNodePeersFilter = [.onlyWriteable], title: String? = nil) { self.account = account self.filter = filter @@ -49,7 +49,7 @@ public final class PeerSelectionController: ViewController { super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style - self.title = self.presentationData.strings.Conversation_ForwardTitle + self.title = title ?? self.presentationData.strings.Conversation_ForwardTitle self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 8eb0c91427..57bbc728ec 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -663,11 +663,11 @@ public func chatMessagePhotoInternal(photoData: Signal<(Data?, Data?, Bool), NoE } } -private func chatMessagePhotoThumbnailDatas(account: Account, photoReference: ImageMediaReference) -> Signal<(Data?, Data?, Bool), NoError> { +private func chatMessagePhotoThumbnailDatas(account: Account, photoReference: ImageMediaReference, onlyFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { let fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0) if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(fullRepresentationSize) { - let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: false) + let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 180.0, height: 180.0), mode: .aspectFit), complete: onlyFullSize) let signal = maybeFullSize |> take(1) @@ -710,12 +710,12 @@ private func chatMessagePhotoThumbnailDatas(account: Account, photoReference: Im } } -public func chatMessagePhotoThumbnail(account: Account, photoReference: ImageMediaReference) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoThumbnailDatas(account: account, photoReference: photoReference) +public func chatMessagePhotoThumbnail(account: Account, photoReference: ImageMediaReference, onlyFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessagePhotoThumbnailDatas(account: account, photoReference: photoReference, onlyFullSize: onlyFullSize) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in - let context = DrawingContext(size: arguments.drawingSize, clear: true) + let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) let drawingRect = arguments.drawingRect var fittedSize = arguments.imageSize @@ -803,7 +803,7 @@ public func chatMessageVideoThumbnail(account: Account, fileReference: FileMedia return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in - let context = DrawingContext(size: arguments.drawingSize, clear: true) + let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) let drawingRect = arguments.drawingRect var fittedSize = arguments.imageSize @@ -1565,7 +1565,7 @@ func chatWebpageSnippetPhoto(account: Account, photoReference: ImageMediaReferen } if let fullSizeImage = fullSizeImage { - let context = DrawingContext(size: arguments.drawingSize, clear: true) + let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true) let fittedSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height).aspectFilled(arguments.boundingSize) let drawingRect = arguments.drawingRect diff --git a/TelegramUI/PreferencesKeys.swift b/TelegramUI/PreferencesKeys.swift index b8a1997b5c..e0b37fd774 100644 --- a/TelegramUI/PreferencesKeys.swift +++ b/TelegramUI/PreferencesKeys.swift @@ -17,6 +17,7 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 { case experimentalUISettings = 11 case contactSynchronizationSettings = 12 case stickerSettings = 13 + case watchPresetSettings = 14 } public struct ApplicationSpecificPreferencesKeys { @@ -34,4 +35,5 @@ public struct ApplicationSpecificPreferencesKeys { public static let experimentalUISettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.experimentalUISettings.rawValue) public static let contactSynchronizationSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.contactSynchronizationSettings.rawValue) public static let stickerSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.stickerSettings.rawValue) + public static let watchPresetSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.watchPresetSettings.rawValue) } diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index 1558804242..4cc17f524a 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -17,7 +17,8 @@ private final class SettingsItemIcons { static let appearance = UIImage(bundleImageName: "Settings/MenuIcons/Appearance")?.precomposed() static let language = UIImage(bundleImageName: "Settings/MenuIcons/Language")?.precomposed() - static let secureId = UIImage(bundleImageName: "Settings/MenuIcons/Passport")?.precomposed() + static let passport = UIImage(bundleImageName: "Settings/MenuIcons/Passport")?.precomposed() + static let watch = UIImage(bundleImageName: "Settings/MenuIcons/Watch")?.precomposed() static let support = UIImage(bundleImageName: "Settings/MenuIcons/Support")?.precomposed() static let faq = UIImage(bundleImageName: "Settings/MenuIcons/Faq")?.precomposed() @@ -42,6 +43,7 @@ private struct SettingsItemArguments { let presentController: (ViewController) -> Void let openLanguage: () -> Void let openPassport: () -> Void + let openWatch: () -> Void let openSupport: () -> Void let openFaq: () -> Void let openEditing: () -> Void @@ -54,7 +56,7 @@ private enum SettingsSection: Int32 { case proxy case media case generalSettings - case passport + case advanced case help } @@ -75,6 +77,7 @@ private enum SettingsEntry: ItemListNodeEntry { case themes(PresentationTheme, UIImage?, String) case language(PresentationTheme, UIImage?, String, String) case passport(PresentationTheme, UIImage?, String, String) + case watch(PresentationTheme, UIImage?, String, String) case askAQuestion(PresentationTheme, UIImage?, String) case faq(PresentationTheme, UIImage?, String) @@ -89,8 +92,8 @@ private enum SettingsEntry: ItemListNodeEntry { return SettingsSection.media.rawValue case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .themes, .language: return SettingsSection.generalSettings.rawValue - case .passport: - return SettingsSection.passport.rawValue + case .passport, .watch : + return SettingsSection.advanced.rawValue case .askAQuestion, .faq: return SettingsSection.help.rawValue } @@ -124,10 +127,12 @@ private enum SettingsEntry: ItemListNodeEntry { return 11 case .passport: return 12 - case .askAQuestion: + case .watch: return 13 - case .faq: + case .askAQuestion: return 14 + case .faq: + return 15 } } @@ -240,6 +245,12 @@ private enum SettingsEntry: ItemListNodeEntry { } else { return false } + case let .watch(lhsTheme, lhsImage, lhsText, lhsValue): + if case let .watch(rhsTheme, rhsImage, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } case let .askAQuestion(lhsTheme, lhsImage, lhsText): if case let .askAQuestion(rhsTheme, rhsImage, rhsText) = rhs, lhsTheme === rhsTheme, lhsImage === rhsImage, lhsText == rhsText { return true @@ -320,6 +331,10 @@ private enum SettingsEntry: ItemListNodeEntry { return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openPassport() }) + case let .watch(theme, image, text, value): + return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.openWatch() + }) case let .askAQuestion(theme, image, text): return ItemListDisclosureItem(theme: theme, icon: image, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openSupport() @@ -351,7 +366,7 @@ private struct SettingsState: Equatable { } } -private func settingsEntries(presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings, unreadTrendingStickerPacks: Int, archivedPacks: [ArchivedStickerPackItem]?, hasPassport: Bool) -> [SettingsEntry] { +private func settingsEntries(presentationData: PresentationData, state: SettingsState, view: PeerView, proxySettings: ProxySettings, unreadTrendingStickerPacks: Int, archivedPacks: [ArchivedStickerPackItem]?, hasPassport: Bool, hasWatchApp: Bool) -> [SettingsEntry] { var entries: [SettingsEntry] = [] if let peer = peerViewMainPeer(view) as? TelegramUser { @@ -390,7 +405,10 @@ private func settingsEntries(presentationData: PresentationData, state: Settings entries.append(.language(presentationData.theme, SettingsItemIcons.language, presentationData.strings.Settings_AppLanguage, presentationData.strings.Localization_LanguageName)) if hasPassport { - entries.append(.passport(presentationData.theme, SettingsItemIcons.secureId, presentationData.strings.Settings_Passport, "")) + entries.append(.passport(presentationData.theme, SettingsItemIcons.passport, presentationData.strings.Settings_Passport, "")) + } + if hasWatchApp { + entries.append(.watch(presentationData.theme, SettingsItemIcons.watch, presentationData.strings.Settings_AppleWatch, "")) } entries.append(.askAQuestion(presentationData.theme, SettingsItemIcons.support, presentationData.strings.Settings_Support)) @@ -516,6 +534,9 @@ public func settingsController(account: Account, accountManager: AccountManager) }, openPassport: { let controller = SecureIdAuthController(account: account, mode: .list) presentControllerImpl?(controller, nil) + }, openWatch: { + let controller = watchSettingsController(account: account) + pushControllerImpl?(controller) }, openSupport: { let supportPeer = Promise() supportPeer.set(supportPeerId(account: account)) @@ -654,8 +675,13 @@ public func settingsController(account: Account, accountManager: AccountManager) } updatePassport() - let signal = combineLatest(account.telegramApplicationContext.presentationData, statePromise.get(), peerView, account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings]), combineLatest(account.viewTracker.featuredStickerPacks(), archivedPacks.get()), hasPassport.get()) - |> map { presentationData, state, view, preferences, featuredAndArchived, hasPassport -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in + let hasWatchApp = Promise(false) + if let context = account.applicationContext as? TelegramApplicationContext, let watchManager = context.watchManager { + hasWatchApp.set(watchManager.watchAppInstalled) + } + + let signal = combineLatest(account.telegramApplicationContext.presentationData, statePromise.get(), peerView, account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings]), combineLatest(account.viewTracker.featuredStickerPacks(), archivedPacks.get()), combineLatest(hasPassport.get(), hasWatchApp.get())) + |> map { presentationData, state, view, preferences, featuredAndArchived, hasPassportAndWatch -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in let proxySettings: ProxySettings if let value = preferences.values[PreferencesKeys.proxySettings] as? ProxySettings { proxySettings = value @@ -679,7 +705,9 @@ public func settingsController(account: Account, accountManager: AccountManager) } } - let listState = ItemListNodeState(entries: settingsEntries(presentationData: presentationData, state: state, view: view, proxySettings: proxySettings, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedPacks: featuredAndArchived.1, hasPassport: hasPassport), style: .blocks) + let (hasPassport, hasWatchApp) = hasPassportAndWatch + + let listState = ItemListNodeState(entries: settingsEntries(presentationData: presentationData, state: state, view: view, proxySettings: proxySettings, unreadTrendingStickerPacks: unreadTrendingStickerPacks, archivedPacks: featuredAndArchived.1, hasPassport: hasPassport, hasWatchApp: hasWatchApp), style: .blocks) return (controllerState, (listState, arguments)) } |> afterDisposed { diff --git a/TelegramUI/StickerResources.swift b/TelegramUI/StickerResources.swift index bf36f1ce0a..024d449cf7 100644 --- a/TelegramUI/StickerResources.swift +++ b/TelegramUI/StickerResources.swift @@ -178,7 +178,7 @@ public func chatMessageSticker(account: Account, file: TelegramMediaFile, small: return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in - let context = DrawingContext(size: arguments.drawingSize, clear: true) + let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil) let drawingRect = arguments.drawingRect let fittedSize = arguments.imageSize @@ -224,7 +224,12 @@ public func chatMessageSticker(account: Account, file: TelegramMediaFile, small: } context.withFlippedContext { c in + if let color = arguments.emptyColor { + c.setBlendMode(.normal) + c.fill(drawingRect) + } c.setBlendMode(.copy) + if let blurredThumbnailImage = blurredThumbnailImage { c.interpolationQuality = .low let thumbnailScaledInset = thumbnailInset * (fittedRect.width / blurredThumbnailImage.size.width) diff --git a/TelegramUI/StorageUsageController.swift b/TelegramUI/StorageUsageController.swift index c9c0174f76..5142210d44 100644 --- a/TelegramUI/StorageUsageController.swift +++ b/TelegramUI/StorageUsageController.swift @@ -523,7 +523,7 @@ func storageUsageController(account: Account) -> ViewController { if !items.isEmpty { items.append(ActionSheetButtonItem(title: presentationData.strings.Cache_Clear("\(dataSizeString(totalSize))").0, action: { if let statsPromise = statsPromise { - var clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 }) + let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 }) //var clearSize: Int64 = 0 var clearMediaIds = Set() diff --git a/TelegramUI/TGBridgeAudioDecoder.h b/TelegramUI/TGBridgeAudioDecoder.h new file mode 100644 index 0000000000..9332bf611b --- /dev/null +++ b/TelegramUI/TGBridgeAudioDecoder.h @@ -0,0 +1,8 @@ +#import + +@interface TGBridgeAudioDecoder : NSObject + +- (instancetype)initWithURL:(NSURL *)url outputUrl:(NSURL *)outputURL; +- (void)startWithCompletion:(void (^)(void))completion; + +@end diff --git a/TelegramUI/TGBridgeAudioDecoder.mm b/TelegramUI/TGBridgeAudioDecoder.mm new file mode 100644 index 0000000000..a65f3d3aa7 --- /dev/null +++ b/TelegramUI/TGBridgeAudioDecoder.mm @@ -0,0 +1,200 @@ +#import "TGBridgeAudioDecoder.h" + +#import +#import + +#import + +#import "opusfile.h" +#import "opusenc.h" + +const NSInteger TGBridgeAudioDecoderInputSampleRate = 48000; +const NSInteger TGBridgeAudioDecoderResultSampleRate = 24000; +const NSUInteger TGBridgeAudioDecoderBufferSize = 32768; + +#define checkResult(result,operation) (_checkResultLite((result),(operation),__FILE__,__LINE__)) + +struct TGAudioBuffer +{ + NSUInteger capacity; + uint8_t *data; + NSUInteger size; + int64_t pcmOffset; +}; + +inline TGAudioBuffer *TGAudioBufferWithCapacity(NSUInteger capacity) +{ + TGAudioBuffer *audioBuffer = (TGAudioBuffer *)malloc(sizeof(TGAudioBuffer)); + audioBuffer->capacity = capacity; + audioBuffer->data = (uint8_t *)malloc(capacity); + audioBuffer->size = 0; + audioBuffer->pcmOffset = 0; + return audioBuffer; +} + +inline void TGAudioBufferDispose(TGAudioBuffer *audioBuffer) +{ + if (audioBuffer != NULL) + { + free(audioBuffer->data); + free(audioBuffer); + } +} + +static inline bool _checkResultLite(OSStatus result, const char *operation, const char* file, int line) +{ + if ( result != noErr ) + { + NSLog(@"%s:%d: %s result %d %08X %4.4s\n", file, line, operation, (int)result, (int)result, (char*)&result); + return NO; + } + return YES; +} + +@interface TGBridgeAudioDecoder () +{ + NSURL *_url; + NSURL *_resultURL; + + OggOpusFile *_opusFile; + + bool _finished; + bool _cancelled; +} +@end + +@implementation TGBridgeAudioDecoder + +- (instancetype)initWithURL:(NSURL *)url outputUrl:(NSURL *)outputUrl +{ + self = [super init]; + if (self != nil) + { + _url = url; + + int64_t randomId = 0; + arc4random_buf(&randomId, 8); + _resultURL = outputUrl; + } + return self; +} + +- (void)startWithCompletion:(void (^)(void))completion +{ + [[TGBridgeAudioDecoder processingQueue] dispatch:^ + { + int error = OPUS_OK; + _opusFile = op_open_file(_url.path.UTF8String, &error); + if (_opusFile == NULL || error != OPUS_OK) + { + return; + } + + AudioStreamBasicDescription sourceFormat; + sourceFormat.mSampleRate = TGBridgeAudioDecoderInputSampleRate; + sourceFormat.mFormatID = kAudioFormatLinearPCM; + sourceFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + sourceFormat.mFramesPerPacket = 1; + sourceFormat.mChannelsPerFrame = 1; + sourceFormat.mBitsPerChannel = 16; + sourceFormat.mBytesPerPacket = 2; + sourceFormat.mBytesPerFrame = 2; + + AudioStreamBasicDescription destFormat; + memset(&destFormat, 0, sizeof(destFormat)); + destFormat.mChannelsPerFrame = sourceFormat.mChannelsPerFrame; + destFormat.mFormatID = kAudioFormatMPEG4AAC; + destFormat.mSampleRate = TGBridgeAudioDecoderResultSampleRate; + UInt32 size = sizeof(destFormat); + if (!checkResult(AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &size, &destFormat), + "AudioFormatGetProperty(kAudioFormatProperty_FormatInfo)")) + { + return; + } + + ExtAudioFileRef destinationFile; + if (!checkResult(ExtAudioFileCreateWithURL((__bridge CFURLRef)_resultURL, kAudioFileM4AType, &destFormat, NULL, kAudioFileFlags_EraseFile, &destinationFile), "ExtAudioFileCreateWithURL")) + { + return; + } + + if (!checkResult(ExtAudioFileSetProperty(destinationFile, kExtAudioFileProperty_ClientDataFormat, size, &sourceFormat), + "ExtAudioFileSetProperty(destinationFile, kExtAudioFileProperty_ClientDataFormat")) + { + return; + } + + bool canResumeAfterInterruption = false; + AudioConverterRef converter; + size = sizeof(converter); + if (checkResult(ExtAudioFileGetProperty(destinationFile, kExtAudioFileProperty_AudioConverter, &size, &converter), + "ExtAudioFileGetProperty(kExtAudioFileProperty_AudioConverter;)")) + { + UInt32 canResume = 0; + size = sizeof(canResume); + if (AudioConverterGetProperty(converter, kAudioConverterPropertyCanResumeFromInterruption, &size, &canResume) == noErr) + canResumeAfterInterruption = canResume; + } + + uint8_t srcBuffer[TGBridgeAudioDecoderBufferSize]; + while (!_cancelled) + { + AudioBufferList bufferList; + bufferList.mNumberBuffers = 1; + bufferList.mBuffers[0].mNumberChannels = sourceFormat.mChannelsPerFrame; + bufferList.mBuffers[0].mDataByteSize = TGBridgeAudioDecoderBufferSize; + bufferList.mBuffers[0].mData = srcBuffer; + + uint32_t writtenOutputBytes = 0; + while (writtenOutputBytes < TGBridgeAudioDecoderBufferSize) + { + int32_t readSamples = op_read(_opusFile, (opus_int16 *)(srcBuffer + writtenOutputBytes), (TGBridgeAudioDecoderBufferSize - writtenOutputBytes) / sourceFormat.mBytesPerFrame, NULL); + + if (readSamples > 0) + writtenOutputBytes += readSamples * sourceFormat.mBytesPerFrame; + else + break; + } + bufferList.mBuffers[0].mDataByteSize = writtenOutputBytes; + int32_t nFrames = writtenOutputBytes / sourceFormat.mBytesPerFrame; + + if (nFrames == 0) + break; + + OSStatus status = ExtAudioFileWrite(destinationFile, nFrames, &bufferList); + if (status == kExtAudioFileError_CodecUnavailableInputConsumed) + { + //TGLog(@"1"); + } + else if (status == kExtAudioFileError_CodecUnavailableInputNotConsumed) + { + //TGLog(@"2"); + } + else if (!checkResult(status, "ExtAudioFileWrite")) + { + //TGLog(@"3"); + } + } + + ExtAudioFileDispose(destinationFile); + + if (completion != nil) + completion(); + }]; +} + ++ (SQueue *)processingQueue +{ + static SQueue *queue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + static const char *queueSpecific = "org.telegram.opusAudioDecoderQueue"; + dispatch_queue_t dispatchQueue = dispatch_queue_create("org.telegram.opusAudioDecoderQueue", DISPATCH_QUEUE_SERIAL); + dispatch_queue_set_specific(dispatchQueue, queueSpecific, (void *)queueSpecific, NULL); + queue = [SQueue wrapConcurrentNativeQueue:dispatchQueue]; + }); + return queue; +} + +@end diff --git a/TelegramUI/TGBridgeAudioEncoder.h b/TelegramUI/TGBridgeAudioEncoder.h new file mode 100644 index 0000000000..ae30698b1a --- /dev/null +++ b/TelegramUI/TGBridgeAudioEncoder.h @@ -0,0 +1,11 @@ +#import + +@class TGDataItem; +@class TGLiveUploadActorData; + +@interface TGBridgeAudioEncoder : NSObject + +- (instancetype)initWithURL:(NSURL *)url; +- (void)startWithCompletion:(void (^)(TGDataItem *, int32_t))completion; + +@end diff --git a/TelegramUI/TGBridgeAudioEncoder.m b/TelegramUI/TGBridgeAudioEncoder.m new file mode 100644 index 0000000000..d5d75b8b0b --- /dev/null +++ b/TelegramUI/TGBridgeAudioEncoder.m @@ -0,0 +1,211 @@ +#import "TGBridgeAudioEncoder.h" +#import + +#import + +#import "opus.h" +#import "opusenc.h" + +#import "TGDataItem.h" + +const NSInteger TGBridgeAudioEncoderSampleRate = 16000; + +@interface TGBridgeAudioEncoder () +{ + AVAssetReader *_assetReader; + AVAssetReaderOutput *_readerOutput; + + NSMutableData *_audioBuffer; + TGDataItem *_tempFileItem; + TGOggOpusWriter *_oggWriter; +} +@end + +@implementation TGBridgeAudioEncoder + +- (instancetype)initWithURL:(NSURL *)url +{ + self = [super init]; + if (self != nil) + { + AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil]; + if (asset == nil || asset.tracks.count == 0) + { + //TGLog(@"Asset create fail"); + return nil; + } + + NSError *error; + _assetReader = [[AVAssetReader alloc] initWithAsset:asset error:&error]; + + NSDictionary *outputSettings = @ + { + AVFormatIDKey: @(kAudioFormatLinearPCM), + AVSampleRateKey: @(TGBridgeAudioEncoderSampleRate), + AVNumberOfChannelsKey: @1, + AVLinearPCMBitDepthKey: @16, + AVLinearPCMIsFloatKey: @false, + AVLinearPCMIsBigEndianKey: @false, + AVLinearPCMIsNonInterleaved: @false + }; + + _readerOutput = [AVAssetReaderAudioMixOutput assetReaderAudioMixOutputWithAudioTracks:asset.tracks audioSettings:outputSettings]; + + [_assetReader addOutput:_readerOutput]; + + _tempFileItem = [[TGDataItem alloc] init]; + } + return self; +} + +- (void)dealloc +{ + [self cleanup]; +} + +- (void)cleanup +{ + _oggWriter = nil; +} + ++ (SQueue *)processingQueue +{ + static SQueue *queue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + static const char *queueSpecific = "org.telegram.opusAudioEncoderQueue"; + dispatch_queue_t dispatchQueue = dispatch_queue_create("org.telegram.opusAudioEncoderQueue", DISPATCH_QUEUE_SERIAL); + dispatch_queue_set_specific(dispatchQueue, queueSpecific, (void *)queueSpecific, NULL); + queue = [SQueue wrapConcurrentNativeQueue:dispatchQueue]; + }); + return queue; +} + +- (void)startWithCompletion:(void (^)(TGDataItem *, int32_t))completion +{ + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); + + [[TGBridgeAudioEncoder processingQueue] dispatch:^ + { + _oggWriter = [[TGOggOpusWriter alloc] init]; + if (![_oggWriter beginWithDataItem:_tempFileItem]) + { + //TGLog(@"[TGBridgeAudioEncoder#%x error initializing ogg opus writer]", self); + [self cleanup]; + return; + } + + [_assetReader startReading]; + + while (_assetReader.status != AVAssetReaderStatusCompleted) + { + if (_assetReader.status == AVAssetReaderStatusReading) + { + CMSampleBufferRef nextBuffer = [_readerOutput copyNextSampleBuffer]; + if (nextBuffer) + { + AudioBufferList abl; + CMBlockBufferRef blockBuffer; + CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(nextBuffer, NULL, &abl, sizeof(abl), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer); + + [[TGBridgeAudioEncoder processingQueue] dispatch:^ + { + [self _processBuffer:&abl.mBuffers[0]]; + + CFRelease(nextBuffer); + CFRelease(blockBuffer); + }]; + } + else + { + break; + } + } + } + + TGDataItem *dataItemResult = nil; + NSTimeInterval durationResult = 0.0; + + NSUInteger totalBytes = 0; + + if (_assetReader.status == AVAssetReaderStatusCompleted) + { + if (_oggWriter != nil && [_oggWriter writeFrame:NULL frameByteCount:0]) + { + dataItemResult = _tempFileItem; + durationResult = [_oggWriter encodedDuration]; + totalBytes = [_oggWriter encodedBytes]; + } + + [self cleanup]; + } + + //TGLog(@"[TGBridgeAudioEncoder#%x convert time: %f ms]", self, (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0); + + if (completion != nil) + completion(dataItemResult, (int32_t)durationResult); + }]; +} + +- (void)_processBuffer:(AudioBuffer const *)buffer +{ + @autoreleasepool + { + if (_oggWriter == nil) + return; + + static const int millisecondsPerPacket = 60; + static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 1000 * millisecondsPerPacket * 2; + + unsigned char currentEncoderPacket[encoderPacketSizeInBytes]; + + int bufferOffset = 0; + + while (true) + { + int currentEncoderPacketSize = 0; + + while (currentEncoderPacketSize < encoderPacketSizeInBytes) + { + if (_audioBuffer.length != 0) + { + int takenBytes = MIN((int)_audioBuffer.length, encoderPacketSizeInBytes - currentEncoderPacketSize); + if (takenBytes != 0) + { + memcpy(currentEncoderPacket + currentEncoderPacketSize, _audioBuffer.bytes, takenBytes); + [_audioBuffer replaceBytesInRange:NSMakeRange(0, takenBytes) withBytes:NULL length:0]; + currentEncoderPacketSize += takenBytes; + } + } + else if (bufferOffset < (int)buffer->mDataByteSize) + { + int takenBytes = MIN((int)buffer->mDataByteSize - bufferOffset, encoderPacketSizeInBytes - currentEncoderPacketSize); + if (takenBytes != 0) + { + memcpy(currentEncoderPacket + currentEncoderPacketSize, ((const char *)buffer->mData) + bufferOffset, takenBytes); + bufferOffset += takenBytes; + currentEncoderPacketSize += takenBytes; + } + } + else + break; + } + + if (currentEncoderPacketSize < encoderPacketSizeInBytes) + { + if (_audioBuffer == nil) + _audioBuffer = [[NSMutableData alloc] initWithCapacity:encoderPacketSizeInBytes]; + [_audioBuffer appendBytes:currentEncoderPacket length:currentEncoderPacketSize]; + + break; + } + else + { + [_oggWriter writeFrame:currentEncoderPacket frameByteCount:(NSUInteger)currentEncoderPacketSize]; + } + } + } +} + +@end diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index c39211d775..1e92c2bf15 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -99,6 +99,8 @@ public final class TelegramApplicationContext { } private var hasOngoingCallDisposable: Disposable? + public var watchManager: WatchManager? + private var immediateExperimentalUISettingsValue = Atomic(value: ExperimentalUISettings.defaultSettings) public var immediateExperimentalUISettings: ExperimentalUISettings { return self.immediateExperimentalUISettingsValue.with { $0 } diff --git a/TelegramUI/TelegramUIPrivate/module.modulemap b/TelegramUI/TelegramUIPrivate/module.modulemap index 3d0eee950b..4f3fe4bea8 100644 --- a/TelegramUI/TelegramUIPrivate/module.modulemap +++ b/TelegramUI/TelegramUIPrivate/module.modulemap @@ -28,4 +28,6 @@ module TelegramUIPrivateModule { header "../TGEmojiSuggestions.h" header "../TGChannelIntroController.h" header "../EDSunriseSet.h" + header "../TGBridgeAudioDecoder.h" + header "../TGBridgeAudioEncoder.h" } diff --git a/TelegramUI/TransformImageArguments.swift b/TelegramUI/TransformImageArguments.swift index 08061757b6..fd45b5597e 100644 --- a/TelegramUI/TransformImageArguments.swift +++ b/TelegramUI/TransformImageArguments.swift @@ -13,15 +13,17 @@ public struct TransformImageArguments: Equatable { public let boundingSize: CGSize public let intrinsicInsets: UIEdgeInsets public let resizeMode: TransformImageResizeMode - public let emptyColor: UIColor + public let emptyColor: UIColor? + public let scale: CGFloat? - public init(corners: ImageCorners, imageSize: CGSize, boundingSize: CGSize, intrinsicInsets: UIEdgeInsets, resizeMode: TransformImageResizeMode = .fill(.black), emptyColor: UIColor = .white) { + public init(corners: ImageCorners, imageSize: CGSize, boundingSize: CGSize, intrinsicInsets: UIEdgeInsets, resizeMode: TransformImageResizeMode = .fill(.black), emptyColor: UIColor? = nil, scale: CGFloat? = nil) { self.corners = corners self.imageSize = imageSize self.boundingSize = boundingSize self.intrinsicInsets = intrinsicInsets self.resizeMode = resizeMode self.emptyColor = emptyColor + self.scale = scale } public var drawingSize: CGSize { diff --git a/TelegramUI/WatchManager.swift b/TelegramUI/WatchManager.swift new file mode 100644 index 0000000000..4267029d30 --- /dev/null +++ b/TelegramUI/WatchManager.swift @@ -0,0 +1,36 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore + +public final class WatchManagerArguments { + public let appInstalled: Signal + public let navigateToMessageRequested: Signal + public let runningRequests: Signal + + public init(appInstalled: Signal, navigateToMessageRequested: Signal, runningRequests: Signal) { + self.appInstalled = appInstalled + self.navigateToMessageRequested = navigateToMessageRequested + self.runningRequests = runningRequests + } +} + +public final class WatchManager { + private let arguments: WatchManagerArguments? + + public init(arguments: WatchManagerArguments?) { + self.arguments = arguments + } + + public var watchAppInstalled: Signal { + return self.arguments?.appInstalled ?? .single(false) + } + + public var navigateToMessageRequested: Signal { + return self.arguments?.navigateToMessageRequested ?? .never() + } + + public var runningRequests: Signal { + return self.arguments?.runningRequests ?? .single(false) + } +} diff --git a/TelegramUI/WatchPresetSettings.swift b/TelegramUI/WatchPresetSettings.swift new file mode 100644 index 0000000000..2674bbc2d4 --- /dev/null +++ b/TelegramUI/WatchPresetSettings.swift @@ -0,0 +1,68 @@ +import Foundation +import Postbox +import SwiftSignalKit + +public struct WatchPresetSettings: PreferencesEntry, Equatable { + public var customPresets: [String : String] + + public static var defaultSettings: WatchPresetSettings { + return WatchPresetSettings(presets: [:]) + } + + public init(presets: [String : String]) { + self.customPresets = presets + } + + public init(decoder: PostboxDecoder) { + let keys = decoder.decodeStringArrayForKey("presetKeys") + let values = decoder.decodeStringArrayForKey("presetValues") + if keys.count == values.count { + var presets: [String : String] = [:] + for i in 0 ..< keys.count { + presets[keys[i]] = values[i] + } + self.customPresets = presets + } else { + self.customPresets = [:] + } + } + + public func encode(_ encoder: PostboxEncoder) { + let keys = self.customPresets.keys.sorted() + let values = keys.reduce([String]()) { (values, index) -> [String] in + var values = values + if let value = self.customPresets[index] { + values.append(value) + } + return values + } + encoder.encodeStringArray(keys, forKey: "presetKeys") + encoder.encodeStringArray(values, forKey: "presetValues") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? WatchPresetSettings { + return self == to + } else { + return false + } + } + + public static func ==(lhs: WatchPresetSettings, rhs: WatchPresetSettings) -> Bool { + return lhs.customPresets == rhs.customPresets + } +} + +func updateWatchPresetSettingsInteractively(postbox: Postbox, _ f: @escaping (WatchPresetSettings) -> WatchPresetSettings) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.watchPresetSettings, { entry in + let currentSettings: WatchPresetSettings + if let entry = entry as? WatchPresetSettings { + currentSettings = entry + } else { + currentSettings = WatchPresetSettings.defaultSettings + } + return f(currentSettings) + }) + } +} diff --git a/TelegramUI/WatchSettingsController.swift b/TelegramUI/WatchSettingsController.swift new file mode 100644 index 0000000000..1321a40234 --- /dev/null +++ b/TelegramUI/WatchSettingsController.swift @@ -0,0 +1,148 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class WatchSettingsControllerArguments { + let updatePreset: (String, String) -> Void + + init(updatePreset: @escaping (String, String) -> Void) { + self.updatePreset = updatePreset + } +} + +private enum WatchSettingsSection: Int32 { + case replyPresets +} + +private enum WatchSettingsControllerEntry: ItemListNodeEntry { + case replyPresetsHeader(PresentationTheme, String) + case replyPreset(PresentationTheme, String, String, String, Int32) + case replyPresetsInfo(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .replyPresetsHeader, .replyPreset, .replyPresetsInfo: + return WatchSettingsSection.replyPresets.rawValue + } + } + + var stableId: Int32 { + switch self { + case .replyPresetsHeader: + return 0 + case let .replyPreset(_, _, _, _, index): + return 1 + index + case .replyPresetsInfo: + return 100 + } + } + + static func ==(lhs: WatchSettingsControllerEntry, rhs: WatchSettingsControllerEntry) -> Bool { + switch lhs { + case let .replyPresetsHeader(lhsTheme, lhsText): + if case let .replyPresetsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + + case let .replyPreset(lhsTheme, lhsIdentifier, lhsPlaceholder, lhsValue, lhsIndex): + if case let .replyPreset(rhsTheme, rhsIdentifier, rhsPlaceholder, rhsValue, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsIdentifier == rhsIdentifier, lhsPlaceholder == rhsPlaceholder, lhsValue == rhsValue, lhsIndex == rhsIndex { + return true + } else { + return false + } + + case let .replyPresetsInfo(lhsTheme, lhsText): + if case let .replyPresetsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + } + } + + static func <(lhs: WatchSettingsControllerEntry, rhs: WatchSettingsControllerEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(_ arguments: WatchSettingsControllerArguments) -> ListViewItem { + switch self { + case let .replyPresetsHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .replyPreset(theme, identifier, placeholder, value, _): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: ""), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: true), spacing: 0.0, sectionId: self.section, textUpdated: { updatedText in + arguments.updatePreset(identifier, updatedText.trimmingCharacters(in: .whitespacesAndNewlines)) + }, action: {}) + case let .replyPresetsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + } + } +} + +private func watchSettingsControllerEntries(presentationData: PresentationData, customPresets: [String : String]) -> [WatchSettingsControllerEntry] { + var entries: [WatchSettingsControllerEntry] = [] + + let defaultSuggestions : [(Int32, String, String)] = [ + (0, "OK", presentationData.strings.Watch_Suggestion_OK), + (1, "Thanks", presentationData.strings.Watch_Suggestion_Thanks), + (2, "WhatsUp", presentationData.strings.Watch_Suggestion_WhatsUp), + (3, "TalkLater", presentationData.strings.Watch_Suggestion_TalkLater), + (4, "CantTalk", presentationData.strings.Watch_Suggestion_CantTalk), + (5, "HoldOn", presentationData.strings.Watch_Suggestion_HoldOn), + (6, "BRB", presentationData.strings.Watch_Suggestion_BRB), + (7, "OnMyWay", presentationData.strings.Watch_Suggestion_OnMyWay) + ] + + entries.append(.replyPresetsHeader(presentationData.theme, presentationData.strings.AppleWatch_ReplyPresets)) + for (index, identifier, placeholder) in defaultSuggestions { + entries.append(.replyPreset(presentationData.theme, identifier, placeholder, customPresets[identifier] ?? "", index)) + } + entries.append(.replyPresetsInfo(presentationData.theme, presentationData.strings.AppleWatch_ReplyPresetsHelp)) + + return entries +} + +public func watchSettingsController(account: Account) -> ViewController { + var pushControllerImpl: ((ViewController) -> Void)? + var presentControllerImpl: ((ViewController) -> Void)? + + let updateDisposable = MetaDisposable() + let arguments = WatchSettingsControllerArguments(updatePreset: { identifier, text in + updateDisposable.set((.complete() |> delay(1.0, queue: Queue.mainQueue()) |> then(updateWatchPresetSettingsInteractively(postbox: account.postbox, { current in + var updatedPresets = current.customPresets + if !text.isEmpty { + updatedPresets[identifier] = text + } else { + updatedPresets.removeValue(forKey: identifier) + } + return WatchPresetSettings(presets: updatedPresets) + }))).start()) + }) + + let watchPresetSettingsKey = ApplicationSpecificPreferencesKeys.watchPresetSettings + let preferences = account.postbox.preferencesView(keys: [watchPresetSettingsKey]) + + let signal = combineLatest(account.telegramApplicationContext.presentationData, preferences) + |> deliverOnMainQueue + |> map { presentationData, preferences -> (ItemListControllerState, (ItemListNodeState, WatchSettingsControllerEntry.ItemGenerationArguments)) in + let settings = (preferences.values[watchPresetSettingsKey] as? WatchPresetSettings) ?? WatchPresetSettings.defaultSettings + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.AppleWatch_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: watchSettingsControllerEntries(presentationData: presentationData, customPresets: settings.customPresets), style: .blocks, animateChanges: false) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(account: account, state: signal) + pushControllerImpl = { [weak controller] c in + (controller?.navigationController as? NavigationController)?.pushViewController(c) + } + presentControllerImpl = { [weak controller] c in + controller?.present(c, in: .window(.root)) + } + return controller +} +