Merge commit 'c59bbe4a89775305b1575c1ec63203c62f8db575'

# Conflicts:
#	TelegramUI/FetchMediaUtils.swift
This commit is contained in:
Peter 2018-10-19 20:33:48 +03:00
commit 66beeb210f
31 changed files with 2147 additions and 61 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

View File

@ -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 = "<group>"; };
0941A9A5210B822D00EBE194 /* OpenInOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInOptions.swift; sourceTree = "<group>"; };
0952D1742176DEB500194860 /* NotificationMuteSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMuteSettingsController.swift; sourceTree = "<group>"; };
0952D1762177FB5400194860 /* WatchPresetSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchPresetSettings.swift; sourceTree = "<group>"; };
096C98B921787A5C00C211FF /* LegacyBridgeAudio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyBridgeAudio.swift; sourceTree = "<group>"; };
096C98BB21787C6600C211FF /* TGBridgeAudioEncoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGBridgeAudioEncoder.m; sourceTree = "<group>"; };
096C98BC21787C6600C211FF /* TGBridgeAudioEncoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBridgeAudioEncoder.h; sourceTree = "<group>"; };
096C98BD21787C6700C211FF /* TGBridgeAudioDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBridgeAudioDecoder.h; sourceTree = "<group>"; };
096C98BE21787C6700C211FF /* TGBridgeAudioDecoder.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TGBridgeAudioDecoder.mm; sourceTree = "<group>"; };
09797872210633CD0077D77F /* InstantPageSettingsButtonItemNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageSettingsButtonItemNode.swift; sourceTree = "<group>"; };
0979787B210642CB0077D77F /* WebEmbedPlayerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebEmbedPlayerNode.swift; sourceTree = "<group>"; };
0979787D210646C00077D77F /* YoutubeEmbedImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YoutubeEmbedImplementation.swift; sourceTree = "<group>"; };
@ -1067,6 +1083,8 @@
09AE3822214C110800850BFD /* LegacySecureIdScanController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySecureIdScanController.swift; sourceTree = "<group>"; };
09C3466C2167D63A00B76780 /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = "<group>"; };
09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListWebsiteItem.swift; sourceTree = "<group>"; };
09D304142173C0E900C00567 /* WatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchManager.swift; sourceTree = "<group>"; };
09D304172173C15700C00567 /* WatchSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsController.swift; sourceTree = "<group>"; };
09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRouteActionSheetItem.swift; sourceTree = "<group>"; };
D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageContainingNode.swift; sourceTree = "<group>"; };
D002A0D01E9B99F500A81812 /* SoftwareVideoSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SoftwareVideoSource.swift; sourceTree = "<group>"; };
@ -1238,6 +1256,8 @@
D02B676220800A00001A864A /* StickerPaneSearchBarPlaceholderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPaneSearchBarPlaceholderItem.swift; sourceTree = "<group>"; };
D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = "<group>"; };
D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = "<group>"; };
D02C81702177729000CD1006 /* NotificationExceptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationExceptions.swift; sourceTree = "<group>"; };
D02C81722177AC5900CD1006 /* NotificationSearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSearchItem.swift; sourceTree = "<group>"; };
D02D60AD206BD47300FEFE1E /* SecureIdDocumentTypeSelectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdDocumentTypeSelectionController.swift; sourceTree = "<group>"; };
D02D60B0206C189900FEFE1E /* SecureIdPlaintextFormController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdPlaintextFormController.swift; sourceTree = "<group>"; };
D02D60B2206C18A600FEFE1E /* SecureIdPlaintextFormControllerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureIdPlaintextFormControllerNode.swift; sourceTree = "<group>"; };
@ -2217,6 +2237,18 @@
name = "Open In";
sourceTree = "<group>";
};
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 = "<group>";
};
0979787F21065EAA0077D77F /* Web Embed */ = {
isa = PBXGroup;
children = (
@ -2249,6 +2281,14 @@
name = "Web Embed";
sourceTree = "<group>";
};
09D304162173C13500C00567 /* Watch */ = {
isa = PBXGroup;
children = (
09D304172173C15700C00567 /* WatchSettingsController.swift */,
);
name = Watch;
sourceTree = "<group>";
};
D00C7CDA1E3776CA0080C3D5 /* Secret Preview */ = {
isa = PBXGroup;
children = (
@ -2437,6 +2477,17 @@
name = "Grid Items";
sourceTree = "<group>";
};
D02C816F2177715A00CD1006 /* Notifications */ = {
isa = PBXGroup;
children = (
D0579E6D2179178700495DC7 /* exceptions */,
D01B279C1E394A500022A4C0 /* NotificationsAndSounds.swift */,
D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */,
D02C81722177AC5900CD1006 /* NotificationSearchItem.swift */,
);
name = Notifications;
sourceTree = "<group>";
};
D02D60AF206C188000FEFE1E /* Plaintext Fields */ = {
isa = PBXGroup;
children = (
@ -2785,6 +2836,14 @@
name = "Avatar Gallery";
sourceTree = "<group>";
};
D0579E6D2179178700495DC7 /* exceptions */ = {
isa = PBXGroup;
children = (
D02C81702177729000CD1006 /* NotificationExceptions.swift */,
);
name = exceptions;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 */,

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import TelegramCore
import Postbox
import SwiftSignalKit
func freeMediaFileInteractiveFetched(account: Account, fileReference: FileMediaReference) -> Signal<FetchResourceSourceType, NoError> {
public func freeMediaFileInteractiveFetched(account: Account, fileReference: FileMediaReference) -> Signal<FetchResourceSourceType, NoError> {
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<Void, NoError> {
public func messageMediaFileInteractiveFetched(account: Account, message: Message, file: TelegramMediaFile, userInitiated: Bool) -> Signal<Void, NoError> {
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<Void, NoError> {
public func messageMediaImageInteractiveFetched(account: Account, message: Message, image: TelegramMediaImage, resource: MediaResource, storeToDownloadsPeerType: AutomaticMediaDownloadPeerType?) -> Signal<Void, NoError> {
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)
}

View File

@ -20,6 +20,7 @@ enum ItemListNavigationButtonStyle {
enum ItemListNavigationButtonContentIcon {
case search
case add
}
enum ItemListNavigationButtonContent: Equatable {
@ -271,6 +272,8 @@ class ItemListController<Entry: ItemListNodeEntry>: 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<Entry: ItemListNodeEntry>: 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)
}

View File

@ -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, NoError>?, () -> 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

View File

@ -0,0 +1,26 @@
import Foundation
import SwiftSignalKit
import TelegramUIPrivateModule
public func legacyDecodeOpusAudio(path: String, outputPath: String) -> Signal<String, NoError> {
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
}
}

View File

@ -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<GlobalNotificationSettingsSet> = 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>, 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<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private let addAction:()->Void
private let state: Signal<(ItemListControllerState, (ItemListNodeState<NotificationExceptionEntry>, NotificationExceptionEntry.ItemGenerationArguments)), NoError>
public init(account: Account, state: Signal<(ItemListControllerState, (ItemListNodeState<NotificationExceptionEntry>, 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<NotificationExceptionEntry>
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<Bool>()
var ready: Signal<Bool, NoError> {
return self.readyValue.get()
}
private let state: Signal<(PresentationTheme, (ItemListNodeState<NotificationExceptionEntry>, NotificationExceptionEntry.ItemGenerationArguments)), NoError>
init(account: Account, navigationBar: NavigationBar, state: Signal<(PresentationTheme, (ItemListNodeState<NotificationExceptionEntry>, 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<NotificationExceptionEntry>
private let dimNode: ASDisplayNode
private var validLayout: ContainerViewLayout?
private let searchQuery = Promise<String?>()
private let searchDisposable = MetaDisposable()
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let presentationDataPromise: Promise<ChatListPresentationData>
private let _isSearching = ValuePromise<Bool>(false, ignoreRepeated: true)
override var isSearching: Signal<Bool, NoError> {
return self._isSearching.get()
}
private let state: Signal<(PresentationTheme, (ItemListNodeState<NotificationExceptionEntry>, NotificationExceptionEntry.ItemGenerationArguments)), NoError>
init(account: Account, navigationBar: NavigationBar, state: Signal<(PresentationTheme, (ItemListNodeState<NotificationExceptionEntry>, 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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<PeerId?>()
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>, SettingsEntry.ItemGenerationArguments)) in
let hasWatchApp = Promise<Bool>(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>, 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 {

View File

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

View File

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

View File

@ -0,0 +1,8 @@
#import <Foundation/Foundation.h>
@interface TGBridgeAudioDecoder : NSObject
- (instancetype)initWithURL:(NSURL *)url outputUrl:(NSURL *)outputURL;
- (void)startWithCompletion:(void (^)(void))completion;
@end

View File

@ -0,0 +1,200 @@
#import "TGBridgeAudioDecoder.h"
#import <AudioToolbox/AudioToolbox.h>
#import <AVFoundation/AVFoundation.h>
#import <SSignalKit/SSignalKit.h>
#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

View File

@ -0,0 +1,11 @@
#import <Foundation/Foundation.h>
@class TGDataItem;
@class TGLiveUploadActorData;
@interface TGBridgeAudioEncoder : NSObject
- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletion:(void (^)(TGDataItem *, int32_t))completion;
@end

View File

@ -0,0 +1,211 @@
#import "TGBridgeAudioEncoder.h"
#import <AVFoundation/AVFoundation.h>
#import <SSignalKit/SSignalKit.h>
#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

View File

@ -99,6 +99,8 @@ public final class TelegramApplicationContext {
}
private var hasOngoingCallDisposable: Disposable?
public var watchManager: WatchManager?
private var immediateExperimentalUISettingsValue = Atomic<ExperimentalUISettings>(value: ExperimentalUISettings.defaultSettings)
public var immediateExperimentalUISettings: ExperimentalUISettings {
return self.immediateExperimentalUISettingsValue.with { $0 }

View File

@ -28,4 +28,6 @@ module TelegramUIPrivateModule {
header "../TGEmojiSuggestions.h"
header "../TGChannelIntroController.h"
header "../EDSunriseSet.h"
header "../TGBridgeAudioDecoder.h"
header "../TGBridgeAudioEncoder.h"
}

View File

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

View File

@ -0,0 +1,36 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
public final class WatchManagerArguments {
public let appInstalled: Signal<Bool, NoError>
public let navigateToMessageRequested: Signal<MessageId, NoError>
public let runningRequests: Signal<Bool, NoError>
public init(appInstalled: Signal<Bool, NoError>, navigateToMessageRequested: Signal<MessageId, NoError>, runningRequests: Signal<Bool, NoError>) {
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<Bool, NoError> {
return self.arguments?.appInstalled ?? .single(false)
}
public var navigateToMessageRequested: Signal<MessageId, NoError> {
return self.arguments?.navigateToMessageRequested ?? .never()
}
public var runningRequests: Signal<Bool, NoError> {
return self.arguments?.runningRequests ?? .single(false)
}
}

View File

@ -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<Void, NoError> {
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)
})
}
}

View File

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