mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-09-04 20:00:53 +00:00
Merge commit 'c59bbe4a89775305b1575c1ec63203c62f8db575'
# Conflicts: # TelegramUI/FetchMediaUtils.swift
This commit is contained in:
commit
66beeb210f
22
Images.xcassets/Settings/MenuIcons/Watch.imageset/Contents.json
vendored
Normal file
22
Images.xcassets/Settings/MenuIcons/Watch.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
Images.xcassets/Settings/MenuIcons/Watch.imageset/SettingsWatchIcon@2x.png
vendored
Normal file
BIN
Images.xcassets/Settings/MenuIcons/Watch.imageset/SettingsWatchIcon@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 925 B |
BIN
Images.xcassets/Settings/MenuIcons/Watch.imageset/SettingsWatchIcon@3x.png
vendored
Normal file
BIN
Images.xcassets/Settings/MenuIcons/Watch.imageset/SettingsWatchIcon@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 760 B |
@ -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 */,
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
}()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
26
TelegramUI/LegacyBridgeAudio.swift
Normal file
26
TelegramUI/LegacyBridgeAudio.swift
Normal 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
|
||||
}
|
||||
}
|
995
TelegramUI/NotificationExceptions.swift
Normal file
995
TelegramUI/NotificationExceptions.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
130
TelegramUI/NotificationSearchItem.swift
Normal file
130
TelegramUI/NotificationSearchItem.swift
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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>()
|
||||
|
8
TelegramUI/TGBridgeAudioDecoder.h
Normal file
8
TelegramUI/TGBridgeAudioDecoder.h
Normal 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
|
200
TelegramUI/TGBridgeAudioDecoder.mm
Normal file
200
TelegramUI/TGBridgeAudioDecoder.mm
Normal 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
|
11
TelegramUI/TGBridgeAudioEncoder.h
Normal file
11
TelegramUI/TGBridgeAudioEncoder.h
Normal 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
|
211
TelegramUI/TGBridgeAudioEncoder.m
Normal file
211
TelegramUI/TGBridgeAudioEncoder.m
Normal 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
|
@ -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 }
|
||||
|
@ -28,4 +28,6 @@ module TelegramUIPrivateModule {
|
||||
header "../TGEmojiSuggestions.h"
|
||||
header "../TGChannelIntroController.h"
|
||||
header "../EDSunriseSet.h"
|
||||
header "../TGBridgeAudioDecoder.h"
|
||||
header "../TGBridgeAudioEncoder.h"
|
||||
}
|
||||
|
@ -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 {
|
||||
|
36
TelegramUI/WatchManager.swift
Normal file
36
TelegramUI/WatchManager.swift
Normal 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)
|
||||
}
|
||||
}
|
68
TelegramUI/WatchPresetSettings.swift
Normal file
68
TelegramUI/WatchPresetSettings.swift
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
148
TelegramUI/WatchSettingsController.swift
Normal file
148
TelegramUI/WatchSettingsController.swift
Normal 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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user