From b8230a4fdbd173deaae7954c543c2ca233e7f2fc Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 6 Jun 2017 13:13:26 +0300 Subject: [PATCH] no message --- .gitmodules | 3 + Images.xcassets/Call List/Contents.json | 9 + .../InfoButton.imageset/CallInfoIcon@2x.png | Bin 0 -> 598 bytes .../InfoButton.imageset/CallInfoIcon@3x.png | Bin 0 -> 932 bytes .../InfoButton.imageset/Contents.json | 22 + .../OutgoingIcon.imageset/CallOutgoing@2x.png | Bin 0 -> 1502 bytes .../OutgoingIcon.imageset/CallOutgoing@3x.png | Bin 0 -> 556 bytes .../OutgoingIcon.imageset/Contents.json | 22 + .../CallBluetoothIcon@2x.png | Bin 0 -> 618 bytes .../CallBluetoothIcon@3x.png | Bin 0 -> 1022 bytes .../Contents.json | 22 + .../CallCancelIcon@2x.png | Bin 0 -> 503 bytes .../CallCancelIcon@3x.png | Bin 0 -> 782 bytes .../CallCancelButton.imageset/Contents.json | 22 + .../CallKitLogo.imageset/CallKitLogo@2x.png | Bin 0 -> 1104 bytes .../CallKitLogo.imageset/CallKitLogo@3x.png | Bin 0 -> 1641 bytes .../Call/CallKitLogo.imageset/Contents.json | 22 + .../CallQuickMessageIcon@2x.png | Bin 0 -> 535 bytes .../CallQuickMessageIcon@3x.png | Bin 0 -> 805 bytes .../CallMessageButton.imageset/Contents.json | 22 + .../CallMuteIcon@2x.png | Bin 0 -> 1138 bytes .../CallMuteIcon@3x.png | Bin 0 -> 1784 bytes .../CallMuteButton.imageset/Contents.json | 22 + .../CallPhoneIcon@2x.png | Bin 0 -> 545 bytes .../CallPhoneIcon@3x.png | Bin 0 -> 844 bytes .../CallPhoneButton.imageset/Contents.json | 22 + .../CallSpeakerIcon@2x.png | Bin 0 -> 1197 bytes .../CallSpeakerIcon@3x.png | Bin 0 -> 1918 bytes .../CallSpeakerButton.imageset/Contents.json | 22 + Images.xcassets/Call/Contents.json | 9 + .../Tabs/IconCalls.imageset/Contents.json | 22 + .../IconCalls.imageset/TabIconCalls@2x.png | Bin 0 -> 1019 bytes .../IconCalls.imageset/TabIconCalls@3x.png | Bin 0 -> 1562 bytes .../IconCallsSelected.imageset/Contents.json | 22 + .../TabIconCalls_Highlighted@2x.png | Bin 0 -> 747 bytes .../TabIconCalls_Highlighted@3x.png | Bin 0 -> 1100 bytes .../Info/CallButton.imageset/Contents.json | 22 + .../CallButton.imageset/TabIconCalls@2x.png | Bin 0 -> 1019 bytes .../CallButton.imageset/TabIconCalls@3x.png | Bin 0 -> 1562 bytes Images.xcassets/Chat/Info/Contents.json | 9 + .../Search/Calendar.imageset/Contents.json | 22 + .../ConversationSearchCalendar@2x.png | Bin 0 -> 510 bytes .../ConversationSearchCalendar@3x.png | Bin 0 -> 1027 bytes .../Chat/Input/Search/Contents.json | 9 + .../Search/DownButton.imageset/Contents.json | 22 + .../InlineSearchDown@2x.png | Bin 0 -> 251 bytes .../InlineSearchDown@3x.png | Bin 0 -> 320 bytes .../Search/UpButton.imageset/Contents.json | 22 + .../UpButton.imageset/InlineSearchUp@2x.png | Bin 0 -> 230 bytes .../UpButton.imageset/InlineSearchUp@3x.png | Bin 0 -> 319 bytes .../CallIncomingArrow.imageset/Contents.json | 22 + .../MessageCallIncomingIcon@2x.png | Bin 0 -> 162 bytes .../MessageCallIncomingIcon@3x.png | Bin 0 -> 174 bytes .../CallOutgoingArrow.imageset/Contents.json | 22 + .../MessageCallOutgoingIcon@2x.png | Bin 0 -> 162 bytes .../MessageCallOutgoingIcon@3x.png | Bin 0 -> 153 bytes .../InstantVideoMute.imageset/Contents.json | 22 + .../VideoMessageMutedIcon@2x.png | Bin 0 -> 593 bytes .../VideoMessageMutedIcon@3x.png | Bin 0 -> 874 bytes TelegramUI.xcodeproj/project.pbxproj | 4086 +++++++--- .../xcschemes/TelegramUI.xcscheme | 8 +- .../xcschemes/xcschememanagement.plist | 5 + TelegramUI/AccessoryPanelNode.swift | 3 + TelegramUI/ActivityIndicator.swift | 54 + TelegramUI/AddFormatToStringWithRanges.swift | 28 + TelegramUI/AlertController.swift | 7 - .../ArhivedStickerPacksController.swift | 49 +- ...orizationSequenceCodeEntryController.swift | 8 +- ...ationSequenceCodeEntryControllerNode.swift | 10 +- .../AuthorizationSequenceController.swift | 2 + ...onSequenceCountrySelectionController.swift | 6 +- ...ationSequencePasswordEntryController.swift | 8 +- ...nSequencePasswordEntryControllerNode.swift | 10 +- ...rizationSequencePhoneEntryController.swift | 8 +- ...tionSequencePhoneEntryControllerNode.swift | 16 +- ...uthorizationSequenceSignUpController.swift | 8 +- ...rizationSequenceSignUpControllerNode.swift | 22 +- ...uthorizationSequenceSplashController.swift | 5 +- .../AutomaticMediaDownloadSettings.swift | 6 +- TelegramUI/AvatarGalleryController.swift | 30 +- TelegramUI/AvatarNode.swift | 14 +- TelegramUI/BlockedPeersController.swift | 36 +- TelegramUI/CallController.swift | 160 + TelegramUI/CallControllerButton.swift | 195 + TelegramUI/CallControllerButtonsNode.swift | 196 + TelegramUI/CallControllerKeyPreviewNode.swift | 107 + TelegramUI/CallControllerNode.swift | 369 + TelegramUI/CallControllerStatusNode.swift | 130 + TelegramUI/CallKitIntergation.swift | 229 + TelegramUI/CallListCallItem.swift | 536 ++ TelegramUI/CallListController.swift | 196 + TelegramUI/CallListControllerNode.swift | 417 + TelegramUI/CallListNodeEntries.swift | 128 + TelegramUI/CallListNodeLocation.swift | 83 + TelegramUI/CallListViewTransition.swift | 170 + .../ChangePhoneNumberCodeController.swift | 40 +- TelegramUI/ChangePhoneNumberController.swift | 16 +- .../ChangePhoneNumberControllerNode.swift | 22 +- .../ChangePhoneNumberIntroController.swift | 29 +- TelegramUI/ChannelAdminController.swift | 478 ++ TelegramUI/ChannelAdminsController.swift | 313 +- TelegramUI/ChannelBlacklistController.swift | 20 +- TelegramUI/ChannelInfoController.swift | 72 +- TelegramUI/ChannelMembersController.swift | 39 +- TelegramUI/ChannelVisibilityController.swift | 242 +- TelegramUI/ChatBotInfoItem.swift | 54 +- TelegramUI/ChatBotStartInputPanelNode.swift | 27 +- TelegramUI/ChatButtonKeyboardInputNode.swift | 47 +- .../ChatChannelSubscriberInputPanelNode.swift | 28 +- TelegramUI/ChatController.swift | 499 +- TelegramUI/ChatControllerInteraction.swift | 13 +- TelegramUI/ChatControllerNode.swift | 79 +- TelegramUI/ChatDateSelectionSheet.swift | 114 + TelegramUI/ChatDocumentGalleryItem.swift | 12 +- TelegramUI/ChatEmptyItem.swift | 38 +- TelegramUI/ChatHistoryEntriesForView.swift | 22 +- TelegramUI/ChatHistoryEntry.swift | 62 +- TelegramUI/ChatHistoryGridNode.swift | 37 +- TelegramUI/ChatHistoryListNode.swift | 85 +- .../ChatHistoryNavigationButtonNode.swift | 46 +- TelegramUI/ChatHoleItem.swift | 31 +- TelegramUI/ChatImageGalleryItem.swift | 14 +- TelegramUI/ChatInfoTitlePanelNode.swift | 32 +- TelegramUI/ChatInterfaceInputContexts.swift | 2 +- TelegramUI/ChatInterfaceInputNodes.swift | 2 +- TelegramUI/ChatInterfaceState.swift | 26 +- .../ChatInterfaceStateAccessoryPanels.swift | 12 +- .../ChatInterfaceStateContextMenus.swift | 23 +- .../ChatInterfaceStateInputPanels.swift | 40 +- .../ChatItemGalleryFooterContentNode.swift | 15 +- TelegramUI/ChatListController.swift | 78 +- TelegramUI/ChatListControllerNode.swift | 16 +- TelegramUI/ChatListEmptyItem.swift | 62 - TelegramUI/ChatListHoleItem.swift | 13 +- TelegramUI/ChatListItem.swift | 253 +- TelegramUI/ChatListNode.swift | 75 +- TelegramUI/ChatListNodeEntries.swift | 72 +- TelegramUI/ChatListRecentPeersListItem.swift | 23 +- TelegramUI/ChatListSearchContainerNode.swift | 168 +- TelegramUI/ChatListSearchItem.swift | 21 +- TelegramUI/ChatListSearchItemHeader.swift | 24 +- .../ChatListSearchRecentPeersNode.swift | 25 +- TelegramUI/ChatListTitleLockView.swift | 15 +- TelegramUI/ChatMediaActionSheetRollItem.swift | 2 +- TelegramUI/ChatMediaInputGifPane.swift | 2 +- TelegramUI/ChatMediaInputGridEntries.swift | 5 +- TelegramUI/ChatMediaInputNode.swift | 59 +- TelegramUI/ChatMediaInputPanelEntries.swift | 41 +- TelegramUI/ChatMediaInputRecentGifsItem.swift | 45 +- ...ChatMediaInputRecentStickerPacksItem.swift | 40 +- .../ChatMediaInputStickerGridItem.swift | 16 +- .../ChatMediaInputStickerPackItem.swift | 21 +- TelegramUI/ChatMediaInputStickerPane.swift | 4 +- TelegramUI/ChatMediaInputTrendingItem.swift | 6 +- TelegramUI/ChatMessageActionButtonsNode.swift | 28 +- TelegramUI/ChatMessageActionItemNode.swift | 400 +- TelegramUI/ChatMessageBackground.swift | 37 +- TelegramUI/ChatMessageBubbleContentNode.swift | 7 +- TelegramUI/ChatMessageBubbleImages.swift | 33 +- TelegramUI/ChatMessageBubbleItemNode.swift | 163 +- .../ChatMessageCallBubbleContentNode.swift | 222 + TelegramUI/ChatMessageDateAndStatusNode.swift | 146 +- TelegramUI/ChatMessageDateHeader.swift | 116 +- .../ChatMessageFileBubbleContentNode.swift | 2 +- TelegramUI/ChatMessageForwardInfoNode.swift | 8 +- .../ChatMessageInstantVideoItemNode.swift | 177 +- .../ChatMessageInteractiveFileNode.swift | 85 +- .../ChatMessageInteractiveMediaNode.swift | 40 +- TelegramUI/ChatMessageItem.swift | 22 +- .../ChatMessageMediaBubbleContentNode.swift | 17 +- TelegramUI/ChatMessageNotificationItem.swift | 12 +- TelegramUI/ChatMessageReplyInfoNode.swift | 18 +- .../ChatMessageSelectionInputPanelNode.swift | 41 +- TelegramUI/ChatMessageStickerItemNode.swift | 32 +- .../ChatMessageTextBubbleContentNode.swift | 93 +- .../ChatMessageWebpageBubbleContentNode.swift | 52 +- .../ChatPanelInterfaceInteraction.swift | 21 +- .../ChatPinnedMessageTitlePanelNode.swift | 39 +- .../ChatPresentationInterfaceState.swift | 105 +- TelegramUI/ChatReportPeerTitlePanelNode.swift | 8 +- .../ChatRequestInProgressTitlePanelNode.swift | 2 +- TelegramUI/ChatSearchInputPanelNode.swift | 130 + .../ChatSearchNavigationContentNode.swift | 47 + ...ChatSecretAutoremoveTimerActionSheet.swift | 84 +- .../ChatTextInputAudioRecordingButton.swift | 12 +- ...xtInputAudioRecordingCancelIndicator.swift | 16 +- ...TextInputAudioRecordingOverlayButton.swift | 4 +- .../ChatTextInputAudioRecordingTimeNode.swift | 25 +- TelegramUI/ChatTextInputPanelNode.swift | 229 +- TelegramUI/ChatTitleView.swift | 35 +- TelegramUI/ChatToastAlertPanelNode.swift | 2 +- TelegramUI/ChatUnblockInputPanelNode.swift | 21 +- TelegramUI/ChatUnreadItem.swift | 46 +- TelegramUI/ChatVideoGalleryItem.swift | 16 +- TelegramUI/CommandChatInputPanelItem.swift | 10 +- TelegramUI/CompomentsThemes.swift | 16 + TelegramUI/ComposeController.swift | 37 +- TelegramUI/ComposeControllerNode.swift | 44 +- TelegramUI/ContactListActionItem.swift | 27 +- TelegramUI/ContactListNameIndexHeader.swift | 12 +- TelegramUI/ContactListNode.swift | 183 +- .../ContactMultiselectionController.swift | 52 +- .../ContactMultiselectionControllerNode.swift | 35 +- TelegramUI/ContactSelectionController.swift | 40 +- .../ContactSelectionControllerNode.swift | 32 +- TelegramUI/ContactsController.swift | 41 +- TelegramUI/ContactsControllerNode.swift | 36 +- TelegramUI/ContactsPeerItem.swift | 46 +- TelegramUI/ContactsSearchContainerNode.swift | 22 +- .../ContactsSectionHeaderAccessoryItem.swift | 14 +- TelegramUI/ContactsVCardItem.swift | 56 +- .../ConvertToSupergroupController.swift | 8 +- TelegramUI/CounterContollerTitleView.swift | 11 +- TelegramUI/CreateChannelController.swift | 67 +- TelegramUI/CreateGroupController.swift | 64 +- .../DataAndStorageSettingsController.swift | 209 +- TelegramUI/DebugAccountsController.swift | 8 +- TelegramUI/DebugController.swift | 42 +- TelegramUI/DeclareEncodables.swift | 2 + TelegramUI/DefaultDarkPresentationTheme.swift | 210 + TelegramUI/DefaultPresentationStrings.swift | 3 + TelegramUI/DefaultPresentationTheme.swift | 210 + TelegramUI/DeleteChatInputPanelNode.swift | 4 +- TelegramUI/EditAccessoryPanelNode.swift | 47 +- TelegramUI/EditableTokenListNode.swift | 48 +- .../FeaturedStickerPacksController.swift | 45 +- TelegramUI/ForwardAccessoryPanelNode.swift | 47 +- TelegramUI/GalleryController.swift | 39 +- TelegramUI/GalleryControllerNode.swift | 21 +- TelegramUI/GalleryPagerNode.swift | 8 +- TelegramUI/GeneratedMediaStoreSettings.swift | 2 +- TelegramUI/GridMessageItem.swift | 31 +- TelegramUI/GroupAdminsController.swift | 8 +- TelegramUI/GroupInfoController.swift | 360 +- TelegramUI/GroupsInCommonController.swift | 37 +- TelegramUI/HashtagChatInputPanelItem.swift | 6 +- TelegramUI/HashtagSearchController.swift | 8 +- ...textResultsChatInputContextPanelNode.swift | 2 +- ...ListContextResultsChatInputPanelItem.swift | 4 +- TelegramUI/HorizontalPeerItem.swift | 14 +- ...rizontalStickersChatContextPanelNode.swift | 6 +- TelegramUI/InAppNotificationSettings.swift | 6 +- .../InstalledStickerPacksController.swift | 115 +- TelegramUI/InstantPageController.swift | 3 +- TelegramUI/InstantPageLayout.swift | 4 +- TelegramUI/InstantPageTextItem.swift | 4 +- TelegramUI/ItemListActionItem.swift | 34 +- TelegramUI/ItemListAvatarAndNameItem.swift | 131 +- TelegramUI/ItemListCheckboxItem.swift | 54 +- TelegramUI/ItemListController.swift | 102 +- TelegramUI/ItemListControllerNode.swift | 43 +- ...ItemListControllerSegmentedTitleView.swift | 25 +- TelegramUI/ItemListDisclosureItem.swift | 40 +- .../ItemListEditableDeleteControlNode.swift | 2 +- TelegramUI/ItemListEditableItem.swift | 4 +- TelegramUI/ItemListMultilineInputItem.swift | 37 +- TelegramUI/ItemListMultilineTextItem.swift | 31 +- TelegramUI/ItemListPeerActionItem.swift | 29 +- TelegramUI/ItemListPeerItem.swift | 77 +- TelegramUI/ItemListRecentSessionItem.swift | 41 +- TelegramUI/ItemListSectionHeaderItem.swift | 12 +- TelegramUI/ItemListSingleLineInputItem.swift | 31 +- TelegramUI/ItemListStickerPackItem.swift | 70 +- TelegramUI/ItemListSwitchItem.swift | 70 +- TelegramUI/ItemListTextItem.swift | 19 +- TelegramUI/ItemListTextWithLabelItem.swift | 26 +- TelegramUI/LanguageSelectionController.swift | 440 + .../LanguageSelectionControllerNode.swift | 31 + TelegramUI/LegacyAttachmentMenu.swift | 12 +- TelegramUI/LegacyController.swift | 4 +- TelegramUI/LegacyControllerNode.swift | 2 +- TelegramUI/LinkHighlightingNode.swift | 187 + TelegramUI/ListController.swift | 40 - TelegramUI/ListControllerButtonItem.swift | 60 - .../ListControllerDisclosureActionItem.swift | 80 - TelegramUI/ListControllerGroupableItem.swift | 145 - TelegramUI/ListControllerItem.swift | 4 - TelegramUI/ListControllerNode.swift | 49 - TelegramUI/ListControllerSpacerItem.swift | 43 - TelegramUI/ListMessageFileItemNode.swift | 52 +- TelegramUI/ListMessageItem.swift | 4 +- TelegramUI/ListMessageSnippetItemNode.swift | 29 +- TelegramUI/ListSectionHeaderNode.swift | 20 +- TelegramUI/Localizable.swift | 73 - TelegramUI/ManagedAudioPlaylistPlayer.swift | 196 +- TelegramUI/ManagedAudioSession.swift | 268 +- TelegramUI/ManagedVideoNode.swift | 62 +- TelegramUI/MapInputController.swift | 2 +- TelegramUI/Markdown.swift | 33 +- TelegramUI/MediaManager.swift | 203 +- ...ediaNavigationAccessoryContainerNode.swift | 8 +- .../MediaNavigationAccessoryHeaderNode.swift | 82 +- ...MediaNavigationAccessoryItemListNode.swift | 23 +- TelegramUI/MediaPlayer.swift | 70 +- TelegramUI/MediaPlayerAudioRenderer.swift | 24 +- TelegramUI/MediaPlayerNode.swift | 4 +- TelegramUI/MediaResources.swift | 8 +- TelegramUI/MentionChatInputPanelItem.swift | 6 +- TelegramUI/NetworkStatusTitleView.swift | 24 +- TelegramUI/NetworkUsageStatsController.swift | 294 +- .../NotificationContainerController.swift | 4 +- TelegramUI/NotificationSoundSelection.swift | 90 +- TelegramUI/NotificationsAndSounds.swift | 208 +- TelegramUI/NumberPluralizationForm.h | 12 + TelegramUI/NumberPluralizationForm.m | 346 + TelegramUI/NumericFormat.swift | 14 + TelegramUI/OngoingCallContext.swift | 74 + TelegramUI/OngoingCallThreadLocalContext.h | 36 + TelegramUI/OngoingCallThreadLocalContext.mm | 185 + TelegramUI/OverlayMediaController.swift | 40 + TelegramUI/OverlayMediaControllerNode.swift | 178 + TelegramUI/OverlayMediaItem.swift | 10 + TelegramUI/OverlayMediaManager.swift | 35 + TelegramUI/PasscodeOptionsController.swift | 78 +- TelegramUI/PeerInfoEntries.swift | 0 TelegramUI/PeerMediaAudioPlaylist.swift | 40 +- .../PeerMediaCollectionController.swift | 26 +- .../PeerMediaCollectionControllerNode.swift | 21 +- .../PeerMediaCollectionInterfaceState.swift | 42 +- ...PeerMediaCollectionModeSelectionNode.swift | 28 +- TelegramUI/PeerMediaCollectionTitleView.swift | 13 +- TelegramUI/PeerNotificationSoundStrings.swift | 4 +- TelegramUI/PeerSelectionController.swift | 2 +- TelegramUI/PeerSelectionControllerNode.swift | 31 +- TelegramUI/PhotoResources.swift | 50 +- TelegramUI/PreferencesKeys.swift | 2 + TelegramUI/PresenceStrings.swift | 102 +- TelegramUI/PresentationCall.swift | 287 + TelegramUI/PresentationCallManager.swift | 202 + TelegramUI/PresentationData.swift | 124 +- TelegramUI/PresentationPasscodeSettings.swift | 4 +- TelegramUI/PresentationResourceKey.swift | 127 + .../PresentationResourcesCallList.swift | 16 + TelegramUI/PresentationResourcesChat.swift | 480 ++ .../PresentationResourcesChatList.swift | 85 + .../PresentationResourcesItemList.swift | 48 + .../PresentationResourcesRootController.swift | 145 + TelegramUI/PresentationStrings.swift | 7211 +++++++++++++++++ TelegramUI/PresentationTheme.swift | 971 +++ .../PresentationThemeEssentialGraphics.swift | 140 + TelegramUI/PresentationThemeSettings.swift | 91 + TelegramUI/PresentationsResourceCache.swift | 51 + TelegramUI/PrivacyAndSecurityController.swift | 183 +- TelegramUI/ProgressNavigationButtonNode.swift | 25 +- TelegramUI/RadialProgressNode.swift | 27 +- TelegramUI/RadialTimeoutNode.swift | 11 +- TelegramUI/RecentSessionsController.swift | 104 +- TelegramUI/ReplyAccessoryPanelNode.swift | 48 +- TelegramUI/SearchBarNode.swift | 58 +- TelegramUI/SearchBarPlaceholderNode.swift | 11 +- TelegramUI/SearchDisplayController.swift | 12 +- ...retChatHandshakeStatusInputPanelNode.swift | 4 +- TelegramUI/SecretChatKeyVisualization.h | 1 + TelegramUI/SecretChatKeyVisualization.m | 28 + TelegramUI/SecretMediaPreviewController.swift | 8 +- .../SelectivePrivacySettingsController.swift | 149 +- ...ectivePrivacySettingsPeersController.swift | 54 +- TelegramUI/SettingsAccountInfoItem.swift | 134 - TelegramUI/SettingsController.swift | 238 +- TelegramUI/SettingsThemeWallpaperNode.swift | 69 + TelegramUI/SettingsThemesItem.swift | 321 + TelegramUI/ShareActionButtonNode.swift | 2 +- TelegramUI/ShareController.swift | 11 +- TelegramUI/ShareControllerNode.swift | 25 +- TelegramUI/ShareControllerPeerGridItem.swift | 6 +- .../SoftwareVideoLayerFrameManager.swift | 6 +- TelegramUI/StickerPackPreviewController.swift | 4 +- .../StickerPackPreviewControllerNode.swift | 27 +- TelegramUI/StickerPreviewController.swift | 4 +- TelegramUI/StorageUsageController.swift | 82 +- TelegramUI/StringPluralization.swift | 44 + TelegramUI/StringWithAppliedEntities.swift | 23 +- ...pLongTapOrDoubleTapGestureRecognizer.swift | 35 +- TelegramUI/TelegramApplicationContext.swift | 54 +- TelegramUI/TelegramController.swift | 12 +- TelegramUI/TelegramRootController.swift | 53 + TelegramUI/TelegramUI.h | 8 +- TelegramUI/TextNode.swift | 103 +- TelegramUI/ThemeGalleryController.swift | 308 + TelegramUI/ThemeGalleryItem.swift | 218 + TelegramUI/ThemeGalleryToolbarNode.swift | 71 + TelegramUI/ThemeGridController.swift | 80 + TelegramUI/ThemeGridControllerItem.swift | 84 + TelegramUI/ThemeGridControllerNode.swift | 177 + ...pVerificationPasswordEntryController.swift | 94 +- .../TwoStepVerificationResetController.swift | 42 +- .../TwoStepVerificationUnlockController.swift | 165 +- TelegramUI/UserInfoController.swift | 343 +- TelegramUI/UsernameSetupController.swift | 69 +- ...ntextResultsChatInputPanelButtonItem.swift | 6 +- ...ListContextResultsChatInputPanelItem.swift | 10 +- TelegramUI/VideoOverlayMediaItem.swift | 27 + .../VoiceCallDataSavingController.swift | 62 +- TelegramUI/VoiceCallSettings.swift | 2 +- TelegramUI/Wallpapers.swift | 105 + .../WebpagePreviewAccessoryPanelNode.swift | 56 +- submodules/libtgvoip | 1 + 397 files changed, 28757 insertions(+), 6756 deletions(-) create mode 100644 .gitmodules create mode 100644 Images.xcassets/Call List/Contents.json create mode 100644 Images.xcassets/Call List/InfoButton.imageset/CallInfoIcon@2x.png create mode 100644 Images.xcassets/Call List/InfoButton.imageset/CallInfoIcon@3x.png create mode 100644 Images.xcassets/Call List/InfoButton.imageset/Contents.json create mode 100644 Images.xcassets/Call List/OutgoingIcon.imageset/CallOutgoing@2x.png create mode 100644 Images.xcassets/Call List/OutgoingIcon.imageset/CallOutgoing@3x.png create mode 100644 Images.xcassets/Call List/OutgoingIcon.imageset/Contents.json create mode 100644 Images.xcassets/Call/CallBluetoothButton.imageset/CallBluetoothIcon@2x.png create mode 100644 Images.xcassets/Call/CallBluetoothButton.imageset/CallBluetoothIcon@3x.png create mode 100644 Images.xcassets/Call/CallBluetoothButton.imageset/Contents.json create mode 100644 Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@2x.png create mode 100644 Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@3x.png create mode 100644 Images.xcassets/Call/CallCancelButton.imageset/Contents.json create mode 100644 Images.xcassets/Call/CallKitLogo.imageset/CallKitLogo@2x.png create mode 100644 Images.xcassets/Call/CallKitLogo.imageset/CallKitLogo@3x.png create mode 100644 Images.xcassets/Call/CallKitLogo.imageset/Contents.json create mode 100644 Images.xcassets/Call/CallMessageButton.imageset/CallQuickMessageIcon@2x.png create mode 100644 Images.xcassets/Call/CallMessageButton.imageset/CallQuickMessageIcon@3x.png create mode 100644 Images.xcassets/Call/CallMessageButton.imageset/Contents.json create mode 100644 Images.xcassets/Call/CallMuteButton.imageset/CallMuteIcon@2x.png create mode 100644 Images.xcassets/Call/CallMuteButton.imageset/CallMuteIcon@3x.png create mode 100644 Images.xcassets/Call/CallMuteButton.imageset/Contents.json create mode 100644 Images.xcassets/Call/CallPhoneButton.imageset/CallPhoneIcon@2x.png create mode 100644 Images.xcassets/Call/CallPhoneButton.imageset/CallPhoneIcon@3x.png create mode 100644 Images.xcassets/Call/CallPhoneButton.imageset/Contents.json create mode 100644 Images.xcassets/Call/CallSpeakerButton.imageset/CallSpeakerIcon@2x.png create mode 100644 Images.xcassets/Call/CallSpeakerButton.imageset/CallSpeakerIcon@3x.png create mode 100644 Images.xcassets/Call/CallSpeakerButton.imageset/Contents.json create mode 100644 Images.xcassets/Call/Contents.json create mode 100644 Images.xcassets/Chat List/Tabs/IconCalls.imageset/Contents.json create mode 100644 Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@2x.png create mode 100644 Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@3x.png create mode 100644 Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/Contents.json create mode 100644 Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@2x.png create mode 100644 Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@3x.png create mode 100644 Images.xcassets/Chat/Info/CallButton.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Info/CallButton.imageset/TabIconCalls@2x.png create mode 100644 Images.xcassets/Chat/Info/CallButton.imageset/TabIconCalls@3x.png create mode 100644 Images.xcassets/Chat/Info/Contents.json create mode 100644 Images.xcassets/Chat/Input/Search/Calendar.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@2x.png create mode 100644 Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@3x.png create mode 100644 Images.xcassets/Chat/Input/Search/Contents.json create mode 100644 Images.xcassets/Chat/Input/Search/DownButton.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Input/Search/DownButton.imageset/InlineSearchDown@2x.png create mode 100644 Images.xcassets/Chat/Input/Search/DownButton.imageset/InlineSearchDown@3x.png create mode 100644 Images.xcassets/Chat/Input/Search/UpButton.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Input/Search/UpButton.imageset/InlineSearchUp@2x.png create mode 100644 Images.xcassets/Chat/Input/Search/UpButton.imageset/InlineSearchUp@3x.png create mode 100644 Images.xcassets/Chat/Message/CallIncomingArrow.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Message/CallIncomingArrow.imageset/MessageCallIncomingIcon@2x.png create mode 100644 Images.xcassets/Chat/Message/CallIncomingArrow.imageset/MessageCallIncomingIcon@3x.png create mode 100644 Images.xcassets/Chat/Message/CallOutgoingArrow.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Message/CallOutgoingArrow.imageset/MessageCallOutgoingIcon@2x.png create mode 100644 Images.xcassets/Chat/Message/CallOutgoingArrow.imageset/MessageCallOutgoingIcon@3x.png create mode 100644 Images.xcassets/Chat/Message/InstantVideoMute.imageset/Contents.json create mode 100644 Images.xcassets/Chat/Message/InstantVideoMute.imageset/VideoMessageMutedIcon@2x.png create mode 100644 Images.xcassets/Chat/Message/InstantVideoMute.imageset/VideoMessageMutedIcon@3x.png create mode 100644 TelegramUI/ActivityIndicator.swift create mode 100644 TelegramUI/AddFormatToStringWithRanges.swift delete mode 100644 TelegramUI/AlertController.swift create mode 100644 TelegramUI/CallController.swift create mode 100644 TelegramUI/CallControllerButton.swift create mode 100644 TelegramUI/CallControllerButtonsNode.swift create mode 100644 TelegramUI/CallControllerKeyPreviewNode.swift create mode 100644 TelegramUI/CallControllerNode.swift create mode 100644 TelegramUI/CallControllerStatusNode.swift create mode 100644 TelegramUI/CallKitIntergation.swift create mode 100644 TelegramUI/CallListCallItem.swift create mode 100644 TelegramUI/CallListController.swift create mode 100644 TelegramUI/CallListControllerNode.swift create mode 100644 TelegramUI/CallListNodeEntries.swift create mode 100644 TelegramUI/CallListNodeLocation.swift create mode 100644 TelegramUI/CallListViewTransition.swift create mode 100644 TelegramUI/ChannelAdminController.swift create mode 100644 TelegramUI/ChatDateSelectionSheet.swift delete mode 100644 TelegramUI/ChatListEmptyItem.swift create mode 100644 TelegramUI/ChatMessageCallBubbleContentNode.swift create mode 100644 TelegramUI/ChatSearchInputPanelNode.swift create mode 100644 TelegramUI/ChatSearchNavigationContentNode.swift create mode 100644 TelegramUI/CompomentsThemes.swift create mode 100644 TelegramUI/DefaultDarkPresentationTheme.swift create mode 100644 TelegramUI/DefaultPresentationStrings.swift create mode 100644 TelegramUI/DefaultPresentationTheme.swift create mode 100644 TelegramUI/LanguageSelectionController.swift create mode 100644 TelegramUI/LanguageSelectionControllerNode.swift create mode 100644 TelegramUI/LinkHighlightingNode.swift delete mode 100644 TelegramUI/ListController.swift delete mode 100644 TelegramUI/ListControllerButtonItem.swift delete mode 100644 TelegramUI/ListControllerDisclosureActionItem.swift delete mode 100644 TelegramUI/ListControllerGroupableItem.swift delete mode 100644 TelegramUI/ListControllerItem.swift delete mode 100644 TelegramUI/ListControllerNode.swift delete mode 100644 TelegramUI/ListControllerSpacerItem.swift delete mode 100644 TelegramUI/Localizable.swift create mode 100644 TelegramUI/NumberPluralizationForm.h create mode 100644 TelegramUI/NumberPluralizationForm.m create mode 100644 TelegramUI/OngoingCallContext.swift create mode 100644 TelegramUI/OngoingCallThreadLocalContext.h create mode 100644 TelegramUI/OngoingCallThreadLocalContext.mm create mode 100644 TelegramUI/OverlayMediaController.swift create mode 100644 TelegramUI/OverlayMediaControllerNode.swift create mode 100644 TelegramUI/OverlayMediaItem.swift create mode 100644 TelegramUI/OverlayMediaManager.swift delete mode 100644 TelegramUI/PeerInfoEntries.swift create mode 100644 TelegramUI/PresentationCall.swift create mode 100644 TelegramUI/PresentationCallManager.swift create mode 100644 TelegramUI/PresentationResourceKey.swift create mode 100644 TelegramUI/PresentationResourcesCallList.swift create mode 100644 TelegramUI/PresentationResourcesChat.swift create mode 100644 TelegramUI/PresentationResourcesChatList.swift create mode 100644 TelegramUI/PresentationResourcesItemList.swift create mode 100644 TelegramUI/PresentationResourcesRootController.swift create mode 100644 TelegramUI/PresentationStrings.swift create mode 100644 TelegramUI/PresentationTheme.swift create mode 100644 TelegramUI/PresentationThemeEssentialGraphics.swift create mode 100644 TelegramUI/PresentationThemeSettings.swift create mode 100644 TelegramUI/PresentationsResourceCache.swift delete mode 100644 TelegramUI/SettingsAccountInfoItem.swift create mode 100644 TelegramUI/SettingsThemeWallpaperNode.swift create mode 100644 TelegramUI/SettingsThemesItem.swift create mode 100644 TelegramUI/StringPluralization.swift create mode 100644 TelegramUI/TelegramRootController.swift create mode 100644 TelegramUI/ThemeGalleryController.swift create mode 100644 TelegramUI/ThemeGalleryItem.swift create mode 100644 TelegramUI/ThemeGalleryToolbarNode.swift create mode 100644 TelegramUI/ThemeGridController.swift create mode 100644 TelegramUI/ThemeGridControllerItem.swift create mode 100644 TelegramUI/ThemeGridControllerNode.swift create mode 100644 TelegramUI/VideoOverlayMediaItem.swift create mode 100644 TelegramUI/Wallpapers.swift create mode 160000 submodules/libtgvoip diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..f86d21f5c9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodules/libtgvoip"] + path = submodules/libtgvoip + url = https://bitbucket.org/grishka/libtgvoip.git diff --git a/Images.xcassets/Call List/Contents.json b/Images.xcassets/Call List/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Call List/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/Images.xcassets/Call List/InfoButton.imageset/CallInfoIcon@2x.png b/Images.xcassets/Call List/InfoButton.imageset/CallInfoIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2ee78f74833515103a654404061cc6242ce096a9 GIT binary patch literal 598 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!3?wz9Rv81Sx&WULS0K&s|35>+3x@h^j!xx> zvWn$*ylTyQvG`+jMceds|+_U*~>0#e(Qx8Y(dHkuxI_LBc>GF%4 z4#!$;vp3a@eD|oft}t%i_rmEqA5Ut}t(A@Q*w-=3;>ySQ0c;N^l%y!|*l$k0`gmgM z7g1Y={F>wHN@HkJC_?z$M{+-uh!{t=9JT%n{!IteD)&aAmP3h-Ht+siY7eR945H#W%onz zLrFiLMtLUg47>2sXM5|>L#Kidk$ma%b6>DK7yf5KmV>mvv4FO#nB@4>SM( literal 0 HcmV?d00001 diff --git a/Images.xcassets/Call List/InfoButton.imageset/CallInfoIcon@3x.png b/Images.xcassets/Call List/InfoButton.imageset/CallInfoIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9c3391484f8740044b3c18b21c66fb0530058296 GIT binary patch literal 932 zcmeAS@N?(olHy`uVBq!ia0vp^P9V&|3?#2~eYgdr&IkB}xB_X0hPMn2FBs~dGBiA8 zsDB1RbwCD?`=6ol1w#Xv3lwPqDtitVe+3oSt0Vvz{38bq5tN>&VSWOd{ zgqj0X4^;UdWXmL=O0bO}DX=X-6M&44cTkIfmIAp=FBxY32U!7h5)gqzUV*eXyacg< zdV%alsN+wzGRy$_$h9QMFPK3lqH)F9`)__SInTKDU$*11%A8+ccece_C$`^U<+AuqR?+CV*M+<1@Xb{h;A#D- zD#OKAvdY7xEl@}5qUNJc75$)>KHpybdiL$xvtL@XJ(pKZ^f^*`=I&qS1CK9%UR$@L zw(b<8&7J!%7t}B=h|oH{AZ8VV$fr%KuQ9yhjWm75uyvJ=lH7uummJrsJ?K2KLYCPo zR`%jshK<{Js%jtDEm+L--C=+FjfMQpYy2k3I_zH?uVg6ESF-4$K+?qc$chAmODEl! zKZJaZz5otNt^itb@)m`9cBHPfEp|NeOFjiPMGO& zjFx7PufsbArE)u!0wZVk#imAc{#$Ns@oeoPm yMOG*5{jBc-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxSU1_g&``n5OwZ87 z)XdCKN5ROz&`93^h|F{iO{`4Ktc=VRpg;*|TTx1yRgjAt)Gi>;Rw<*Tq`*pFzr4I$ zuiRKKzbIYb(9+TpWQLKEE>MMTab;dfVufyAu`f(~1RD^r68eAMwS&*t9 zlvO-#6unGuQL7KPqQUqtGDiP7g#y* zY})POop-EeU2d5$;qb!br z{xXfJUZvB$mU3_T{o0D-r#{D_hP!$DuUbc1&3&gS*8Tj`P8CfVby%KelOdnqW%KD#FMvpmb4l11T2Z{m?FRuc#Xf+kyFfxdBXXssU|6G zF&AuoY)%JmeNkdnZXub-IKf%BV@bz}ZHqStXv8e~IZ-m2F^6lz;xr>CW)B}%y_ld` zsu9-qN!98bdY9)j2_?oIsPNNaGRqVROJ}Q0SR}A2^qg^;O-WB{O_H@xr;GmCf4j~c zU^}z1w{w5tt`{?FkIwx0=;M!or9n3j1gI?lDq^sv6QQ%%4(S96b&$0m1PJdcRi z{pS=ae!bMIaVl%#dqI=;yc%35~|@U6(fFMjWaS)dZv M)78&qol`;+00n?1$^ZZW literal 0 HcmV?d00001 diff --git a/Images.xcassets/Call List/OutgoingIcon.imageset/CallOutgoing@3x.png b/Images.xcassets/Call List/OutgoingIcon.imageset/CallOutgoing@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..24e55285f60ceddcec53770e2b3d372d7b7440d5 GIT binary patch literal 556 zcmV+{0@MA8P)!5)BC7a0{z7FA215xzJnsjn9aOpCO6HrfARfhGQxP z1Y}HcDI3h^^{B%j4NW}Cia4>179+T#5QnmlB#+&BV1&aQO2U+rlyc)x5@z&!X12?r zB2M`i54E^8nEEX>rkFSp(L^>A*^alDcFly zFVDGaFjEXl2qCZ&a|(A0l`3boGFv0Axt4lH#R2;)RHEtP_PwuEfnhentLs!Z0)GXJ zF37sFTM3*UOlk^vhtn$c2eG8#j;u1r3rmf5|8D0fPu$aWka$=KAm2(}sf2eVubPdQ~g z0BIKX!77zRL~T!6?3#NkYVo1~mUVwG{0ezO)7pRWGoj|!LFXP!&b#qJ@4Hq0nWa(9hA8pp;T5 zrT7U{)I{=_r1`r^t}~M{GdFW@dZ=@RAZkpfHE)avsSqVO&C;pX%qE;Ppw26uyX)hUfE(j z=EvF4b}=p=n9r0+oaW5F^d}DBmU5WIgX~91tD>7^569)fwiY!P_LPF^(Y)j8RU2iy$os^U>aS%X-Zf#Duld z1!y6#oPxc(JM}lZkKm=uh0&U|GShZcgS}4Y1aq)(njfZ)hh_Yo%aQVPBKg)SFD7g(VssjZ0DL=^FmZI2te z?OBh49SYqjIu89EK5r-`-dej|ZLK3Ob(UZM^4Vciepx8m`q?1M(Y5WIq1Mt7t&D{C=H2?qr07*qoM6N<$ Ef;4{{QUCw| literal 0 HcmV?d00001 diff --git a/Images.xcassets/Call/CallBluetoothButton.imageset/CallBluetoothIcon@3x.png b/Images.xcassets/Call/CallBluetoothButton.imageset/CallBluetoothIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..846ad33529ec3e7ad8713ecd1a1a461db3b0cb74 GIT binary patch literal 1022 zcmVMIvh%r}+nwh*j*X3tjg5_sjg5_s zO_hfZHsl}K%MkVXN5VTA@{fdJHs>D+qioGT5>O{;#I0kQbRRKL5o* z{lv9A3Vi-akt^sDn3EjROWenYkV=*q={O$dAKLU)(;j9Z9i)#r#W5YkH9SNXB}gB0 zn)a-TO}vM+kv`@OC$l78VH&bP`k1qv&IY)e2b78w$>K_pKIR-}(kJfaEyP9A$}4Is zMFyDX(j{IhQB5O}v@!_{$e41Zk2y~vE#NBdQ*L^sMC8mUq>l*~(je|;2ofPXg+hih zOCVj(xjbK9u^YI`^)erQOhGcFub3gs$m;4Pc9wf?21qZm3r{37;=<#`yuL#A$Y`eY z7UFsu1g9$bmT5soF(r)w*VD8+QQ4(L6H?9$ZPtnJ{abM(T^~}$3~8Xdp6=F>%5Mr9 zk>SihS4(`)Z(8Eq`LrU#n7&T4_u_$au93}weB39ipvB`MzLvCI#};(k)kn-@ zFT_ub>`(q!kp0OZF?S;aVZ@w`@q`o88LgYvgOm}|8F#(IoQ4r}DKTOMy%2i@-EmmX z$}%5yJW(-Sw^)DudXc#)(p6rtQimv*v8kG7oL`wt3SMxuxFht!g`)% zxQ@rocpZ;WZErS|x_DirQGu&22ScBfat?+gs) s{}&^w;kQ3*Y;0_7Y;0_7Y;1C!fA);>?+NB@o&W#<07*qoM6N<$f*aT4q5uE@ literal 0 HcmV?d00001 diff --git a/Images.xcassets/Call/CallBluetoothButton.imageset/Contents.json b/Images.xcassets/Call/CallBluetoothButton.imageset/Contents.json new file mode 100644 index 0000000000..f97cc97939 --- /dev/null +++ b/Images.xcassets/Call/CallBluetoothButton.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "CallBluetoothIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "CallBluetoothIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@2x.png b/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1d51eb1725412a7a57a141697c6b83fddf1684fb GIT binary patch literal 503 zcmVPx$u}MThR9FesncZ>2AP|H@I#DyJ!*8BE+hsTnrJ0nGM|n>c9z-HI?iNO7Miv;@ z{W#FipzyY)d7dX^Ki1-R2;nEzjXW1QP|5NG?3&%ak2y=c{1nVY3igu z20Qu3)XPZFKo+qk(O_YwffT0gMcvTR4eR!D3se`{GJD!cmIKlgmaa_~d|U^dNanM~ zgk@^woJ*Nuz|i;?F8|YGYOCJ?Xtx=0{OWT^SslEXKO{=MVpEMCBonWuM z&6G&{LPlOE>=;NV>{v)6tP4`};vBhN^K4wFuBbLg$_N?n0%KsEkYiv;kbPkrAmxFe zeJF63fyLO;r}<(@J9rE|eN%>No?*n$D1tH#m|7d?9>R1@x?mClnl?=+fy=XQ54S*0 zR4ALh(DC@2WPp5)ZAXCCcAU|Kzc(6JhG>gHC8>6tulPdp6@}U7Rr^}i#Yjq1&wXhq t(12qvvYQ>}!+*XBagY*O<(m?AYy=^A5#DT002ovPDHLkV1oIe-5US^ literal 0 HcmV?d00001 diff --git a/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@3x.png b/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..95e99b4db2eb99897ac533f45a385cae0a48b33b GIT binary patch literal 782 zcmV+p1M&QcP)j{00001b5ch_0Itp) z=>Px%$Vo&&RA>d|Tgy?yFc6J%pam*mG6P*u8x5qP6aW`4aG?UuTsRTlbEL#LqgcP) zmB`L0vMp)%y(h`CGuE)0*?zxY61CPy)8TQc zVY31NbS!c5Bi}XW$A)JK)|>#qowA3mkD>ytF)^IB4o407!CsoFvHe zwA8U53mvKnAcj-S@iL2guYw4VPM=NFmhms0R!PVTQvjuoJ>MzS$~ZPTY?~ zZ6Mn5JkMaR1xPDSkZ7nt8kn^I5U=R#QUIm=MiYyzSQ}_(v5W%TG3u+=dl^ zKfniHigHZtWh$r-P?x-3e&?`88++njkwO7d`L{z{S%CJ4D+$mpabW@4CoUvFKg25w z&>!)M09?m&mov6|@V>grD=mNh^?W7ba5Gvz0swBw6QARImbEW{5OJ?Wy8;LkZ&v^% z;_V4gNIZ8Lw4VZn#7Usf_xTFM$1gi-`B8fastvfdKQSSsfJ*tTCWJIVY5&U7ehXZvS>H%kj=cj6T)B?>JE}VCBWhqBBp`PnTXzR?pcf88G?qS-}$kI9!n3>kouP;oR4=@@@2(vff%K@8Z2ickq?i~S*9P+#T8 zE=Y)mp(s=Wy5kTeB*o#ow;&>fC9;+b+ zeE_ZH#v^EsZ4ejB@I90dRj~li2(@8HoaG%)Z;H-vSi;YTQkH}Gg2Ux$3C@H(vQE+~gr5*Xk;I00QD zAvhCOb`+Gu3vq^bKMLaCzS8_ZoTad~G<*jY@}Dk>@}$M*-= WI;Ah6Ozuzs0000uDA8|VAs!qS>JjV#Xc17qYyo_oXhSBJR53xTC%_w31K3Rg8_M+fRE4@ zen^yhK-0XA!w2XCKO|09L6f|W!dvKpT@V|54hQo(1TUfkc0g=a3a=EmKORLBY=$I} zf>#Ri;Z{_`YDkKFj3nlDG4e1E;(!O>WpgLu3yg+1>&_Q?sUEqgkU=w`sN}$_N z8yg^+840gAIupNPHbi4};1y+u;5GC?0AheI5h1VJQ3o3#hPek`K{^FLVkX33$HD7A z?T@F>34Vy-mVmr2K`B;1QY?TeXoLcrPg{0D+IAS;!7zve0+@)3Q2)^kVxwH7T{oa6 zHbES+5Y?e}^B1;2Z1gPBqNDK{CO{mt5pALNQiN3yo1Ft!cPAQS8^l37&=2`gdnv$D zh|Si+2iJ8xzQqiP24>| zP>3PQ(H%KZQOL&}h(o#}<(-ETtcDn5E~-IAsTfNk4$4AG%ES}sjsV0U<>-tYs3`r7 zH4ukAillZTe!)VBf##qJRP?mkmpC3t=^WI-PKbfZ(GEFKF{qF25DmRGT_#uXxj|xz6=m*izEwH8Yun}US zxc{}lD2Rr3;vm>kMTmhGqYNqzIhYF3)F{~2aEM{rBL^xD1(*lXSY_B&6Nn*Jq8KVp zC0GX0+^evyM1p1s~Q< z{Ea`L5;PEEAU_U+Bb|vKF%e?3$@mc}L0K3BG1Pcy(PhZPY)AqEsDa<15|o4K5Q9~P zc3q2N%!kBaK7NF-#(4EGwe&4-)uHHxA zvPmVZhq%BB7+EInMLvFnvPn&Bfw)9F7@ZG)K-r)Xc0gR@I~d<1Pzh=SKg4Bjf$^OP zm4NODKwM@!4utV#Byh|-7~)cc;hG+RioqWk0dcV+xTfQwV(<-0u@&M{7F?4r#$(~n zVc|o}gSbop$G}zH2Nk6cVg1K5=mBw&DR5oKLPhCaSbHhMHi!#UhwJj;C#WdA2`h*X zF&|>MSK-R;f{MbcutInSJs}1=6RzxNs3^PyD+JR=_CmNeAHIW%;5k@P_{hMK-59R! zHmC@mh83k}&h- z{WuthpdLg+Es+)-1{K0juwwKO4#)1jjx7+4e1f#;Q>Y-mf)#^Da5RE-2_{1{aVgTK z>!E`97*>=X!wCqPMk9MAd`O!PfePY1SW!9+(e{SJBfAySt`DI0@&>FZoP$`q#KFDn zkCAp=1GSfzVMTB;;_VRB_l|#4(?M~Zro0k%^kxc2_EMy6Z$s_oDOe#~43g~xH!KiY!> n0RR91073gxFH!{n0002MDm+INc8$IT00000NkvXXu0mjf<=@MQ literal 0 HcmV?d00001 diff --git a/Images.xcassets/Call/CallKitLogo.imageset/Contents.json b/Images.xcassets/Call/CallKitLogo.imageset/Contents.json new file mode 100644 index 0000000000..d9ac180f96 --- /dev/null +++ b/Images.xcassets/Call/CallKitLogo.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "CallKitLogo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "CallKitLogo@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Call/CallMessageButton.imageset/CallQuickMessageIcon@2x.png b/Images.xcassets/Call/CallMessageButton.imageset/CallQuickMessageIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b374e37c32b98c345281edad3586dcdc3b26f12a GIT binary patch literal 535 zcmV+y0_gpTP)a&GVY`~l~4F7JEJ?>Qm{LLC8SSS7|Gm!wH^$ssXT znIS-(*bi;Ylb}=*EYN0^@GwdoGl?^X#~e~ghzqRbf)JHPnJxo2tN4xVDvea|ok#5E zky$E=F+R3%D%FnTr%edG?MK*EeLX)V<*VjxF_sXDqnqHva zs7xfwbfY@m8E9QOw4T|dDdkWKpU{tv^b27vP*{j+fuh2x7U)PwYk`u&ofar19BYBj zg*`3Mp72Wx^h5Zj1qulrTA+HNT*GpRXWrh^%Auj`^mD6}ImVs+^vx-S23|hp)$A){ zwkUf&p7<4wC+F{mNXAvXO6klut2+Ip?)Xw@`gU@m-|)FupBUtyBTRzEQVpraWTDL0 zj+Uu5()ip=*|@I1w3}(MpGaCj{?MDVYYb5lQ^W~NG+QbEUaoT%>REwid%zkKv{G*M zFT>*Vp{^LlEkYFy_~|1^kRUyD@_{P5|4Rb@&;NlHYD87=lLXTIppTFSyVeYy2{2t>t7K_DVu~;k? z%OYfuON4T&sG*ixYN(=|2)SfPj-5prQ97CAZ%oojlrpm91P@b0j6vKoNQ@%FX2a*u zzyw~Hpn)6{(Nid;8(nl$N=k6yL+qg+-SksND2Vv^^xzjgwjYi`5Nt{N~{X4RK z1VNuK$C`u85EL^U(0y#91Wi=iktWPlOmI!hXO2`_F-04x-UXU5Nwe4ZM={MYFYybQ zGcGVkL9+2zGKvI7ldL0XK?*I&#Lr_vjKBhU?mt}*kVKEm&l(Y=5pgHbB~GBr-Mup* zP9Wjpj>;J%a#menK%~HcYW!@ZlC36CAy%M5O`u7vK$Dt4hgg9QHGyYh1%}iF#>5J| zQxo_oR^W@8fMC=F-iyuuNloC5Sb=dhfhS@G9;*r56f4lGCQvU{;FOv`iCBRmHGx%v z%K4>yCQbE1{Ya$1ZS{iYxJZHh>H_PS7Z-n8`2lhq$KjSZfvCH-N};g$v#fF3o4YGY z;I#YR=w?CZ;AK*yR~p3#lqA<)PiII({7cDCXtpwIJpNNxug~9%x6SgF4PM85=S)^$ zY|}9*zF;i=4qb}|Cf-L=6zbfTI1%s_dzekSw^fRn4lMo+R`{`SlTB|RWlNQxE0%RW z&+hNB-tV2cjkNpr-0u!*Z?RhY5O9P;fiA()sV#b(I}``FHTqrPx`yZpdZ}ZrnZ4Z? zo}xdejl*P`+nL_?OQZ?TvzJQhY2zUyOz~l{tIrg#>7$*?oMb1fP4BB`{?xyoC&uzW z{#GGJ7A)ibj~!#SIR6fWxUwYt7}bHlKeSZ`!yNHcD;bIZtv-9jMYfB3wv_~SQEBA= j78Z-eVzF2(mj9Elg2Hi1xowt600000NkvXXu0mjfrG9-* literal 0 HcmV?d00001 diff --git a/Images.xcassets/Call/CallMessageButton.imageset/Contents.json b/Images.xcassets/Call/CallMessageButton.imageset/Contents.json new file mode 100644 index 0000000000..9b35d685e3 --- /dev/null +++ b/Images.xcassets/Call/CallMessageButton.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "CallQuickMessageIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "CallQuickMessageIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Call/CallMuteButton.imageset/CallMuteIcon@2x.png b/Images.xcassets/Call/CallMuteButton.imageset/CallMuteIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..da7ee35a3f2203ab67c04fc261349aa548e49354 GIT binary patch literal 1138 zcmV-&1daQNP)o$a~noZq|WoacPbbI&>V zc|PAqvWphVDdP{5U8Gr{PU3UpvG2p=(rS2GXnlQ?P! zkC*aydL5V~NPsIw@MJ0Q5Z8ed5_`>tUfN7^5V%8F;z!rv62rh(61Du}KD>cPq}VCZ z@AhM9koXvgN^Ib!yYOL&jm!YgOB`|+4y_U|12@?q5p@@SjSzk=0q;rdTzUE}6%HpQ zb^{NomH5(K_&n7T-vU2Lgt_A;9L`GAa39z!(djNc%~KL@0#^tSWYSGI{DzNp#DHTG z`_>50?~-#}hs4vs9GfN1uLfWCR*vu~DkUxeUr5xk;3gbClGwq&EYc|PzPs=w+ayi{ zqY~xB-GsxC#3pV7&q*A17Y_R+UIlKjUSiB$_#c$v=Q8k)L{s5R^Z($`Be4f~z!r%! z?!xaTqrj&U+pJ$`izv;& z#8S&|)Blw8gxwl-m^^EAO413Xbd(*wzjV5w_v4bT7LU9e7|2PUP789@-7i={CrADn z0J@9ePXf~f?RLa*O*?P9`!f<9d~aV9WEyB(Zn^k9V=H#JO*rD0+&#uRd{o=lv;vdM zuSLBXeanf8PAN3&?x*qyQOAf+1#`fGoFko%_)g2+?^9#>?IDwTMpq>QqZqb&=8~nQ5mI(d8CHw@r zMuPg)6r+vBV!UJTjJv-g(Mp@i1uwD)3_8A={N9Iv88({xRvQT*miy&6y(Q}d{G=3} zt7ja5D6QFlZC;NcZA9s}-2D*^rQic}S@C?D0Xk?VOo$L+n(1JG831nanC0%nr6#%x zdY#RdNqREx#d7!7wd~t9(#<(0m<3>#3C_`-KRwY2T+gm#Yq^c1EL-k=(i6Bu%Tmzs z0B*VaS+C%AERbfGCveN%FXHnCUe2vd(D49nx%(8A-oSl~WrB_eaLe73Z1)6ix%-e; z@G99LK-&rOn*LnTZpfEF_r-W}@IU6cQv|tr|62SY~DK5pG77xkalr(z0A8Eh@6o z71c5`6&DJ|6u~f;mMEgg5H;~-zD%c|{y5LQJLkThd(S+_e)q4Pd+)Q)ea`co-}m`_ ze^NwnldgxL4^^CFvg^<1P{eF1SV#r4DI%x)P?iDk2R)L0GLK4H=)@V9X`zz5?m!vJ zIRKtY_^Dwu&_Vp@pn+lDVO|5^82#hCpFsufB#(9~$?%4f#X$f*i1}`R*5f|b$NNEU zVeT_`y)ef5_>hRn!4pc0Hlzx)Ld!FW{$Q#Oy z90p*CdEOrLfHK=_rhWi`ON^8xo(SkcXJEDfuuhWb!OpZ%7I{Y*!xaFEC5g#tqf~c? zwp$LsK5mdCz77&)ASVHM*&#ZEK#_PJfHMrXiOzsfdeI8N7Y@98Rb0rlEg~@oMMnoboz_}-$@d^*$cp@rf7C32o#ti zNlXXe3gc{|(|43z^pqri0${5{bap3-^__$$@T?>;!c^lPHlnjLRg_r&D^-*enS%piOl8i~_49 zi5u7lQKHjl6fEb6873pyMW^p5J1yskbpUKJ-`E-i3e1%xMsmp}I&U|k(`OXp9Pusy zhaIBRXB1c}N%Z5WO?2LDM5oUv#yR3KBRYrMM5hN7>oM*yO<0sP{ckWCX-5pv8K)LU zhEFhX`_e424p%TC=V+rtbhZE;R+%+CVy4kGiM%uZ24H5y)drX6;yIGU zD6_gO(K(*0k=`N2Ry88=q0CxnjS*M#T{@~E=jeTh=v)!$sG3qpZDN#B<`xZ1YrVi? z;~X8giO#-Jg?j7u6ec^YZU>+yszbwFW5gwHl_Z|Di_VC#Y5>@t{M=k>swcToJqf#A z!8o0xRZ-*Tnyg)#93_K204$Huso@T0rX+EPU35lSI(uAoDo|py>i9JKSN<{1(KmL{ zX+{`t6kdtj;>KEIM&-nqvq~4cX}-fbnrllU*6Coa`#MX3IcTkp`EGvFUZ;AKn-@CU zl~@hHY3so67N*P$`*xi7U3Gwkouj4ZucOiF)(y-@$-4>2VUn#cZc>aJdcQ#&6&9WK z>GWzeC7zko+m*;*iFNBz9VCm*24;2o9R*?M$P%4B(j6u%JTv>Ex3=q1mPNKJrQ1?d zZ(y#lJn{*gyr)5u7+{>E1>P&SWt=uRr>KcclnLDF8jc(1=o!1{^afL49(q>RZlpNT zWRpoTjVAl9aX3dSyT@xTF}W!O)J!#Hj3tk3dXh~ZV=1GWW=G{|k1-`2?MmKZGAK0? z!d+~qv@7ZHtzC3>4^SbsQHRjT*h_7sFbcOT%~AITd6ZK}6RqLnMH{U&QAc?^@oWwp zMs!}2NKzB9ZEe%8v^a1Si3jcNN`ay{+m(Vw3AHQD3>-z`V|%+&uqdH+rTKxQNW5Tg zR|*y-R{GfOU* zr987zw$;3{eYDn+$MXOE|NlJ>|Ia6)Xi`vOx(Zb)RjN{9x{{bEO4M3qr)Fnd*WtBZ zgZ?zA*DH@))2`VLtJK6;GQ|qJoOVy2T=luD%}#ZuMB!Lwzsq_RQjbfTEQ$Q6cTA_T z^h~R|NGD^T$Hq&Cy~-n7=4sVqynWK5I{rtwqduD`{f?LsqqD_(6YZ@H@h>vYnJn7P zjNe(PQ||hG(B+By?zrQDC%SylFL%$>#xLf3C(R2d?Y2gpYGpABWvZ>vXpd96q+Rr$dgVf>ces=PDP%akz#SkNU=HcWV`GQnh_CR%GOtz$dfsK$ky{1 zqFuJyq5zzit+p6qy=;7QR(TYF*)AK9jkPhvG#`y_K!+37$wUFEwBB(K{V=+2r7=va zzkPOFi$=3z44G-QX1DbFTXT$1>8%lOoY!Qf(ilfdHE43dJ0o716~9>EphGsQi3z6G jCWjp`KZ=TqipIlF%r|exLzJJq00000NkvXXu0mjfK5__K literal 0 HcmV?d00001 diff --git a/Images.xcassets/Call/CallPhoneButton.imageset/CallPhoneIcon@3x.png b/Images.xcassets/Call/CallPhoneButton.imageset/CallPhoneIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8237f34688c33f8d6e31e8ca8bc6db055bb6aabc GIT binary patch literal 844 zcmV-S1GD^zP)bEGeX-P4GrY%EHu(utHs8TPo6u z3^kH2N>L)z3KX=%MRZV!E;^}Y>eIy#tES@ae6t6c=X&PlefOXJ=Kub0yu5UDbaci5 zKSGHk2?Ob5l0!a4lwih8DW#Y(Q%n*0QrW;VLh$vRfIci>6Gthef+ji{z!3)M zppknNaD(hD z%3EB?Yl=xBQ0u6{9H5e3+=U+Qv!9vT)Y}v)_=7vr$6dAx)^_Gnz-Qc@F7gOdA2ya7 zZo00cueFuMj)#q-22Z4#rS^vn!NMS($^d5O*i)dg>BiIfLDqx{8PPN|S$M|6@xywP zq0#oxRdka+-VUmQ$w)Z?qs5)hQ}KUM8&4?X5@#@xhJj=fSxGE0L=#O6v8*DIWDKNX z;w+abrM?&JisU(Bd%u>0DUBfXR&?n@oYDAOCX%X2$E)p!drJYTY z(Caa4sBDzbFS$>Xmc=OI?HBWGl2#GP>zZ|G77>3Xwi64PSJ5KP0!NBG*5C10ML|CBDG`skir7ODvE`Zuju-J*p@=mKNPX4zFclxZ3An6R#9j(W zRcd~8SR+!o(xO}WgNwf^{ng?Dl;PRFtm8V+hi}vFX`~`LkIf~RC)k0RM z3;?QC``oDrMW_}^a!O$J3-j-si#O_uJ>E_UJcU=Pe74%8)aGxTPI4Mk3v6 z7$3%NuFx`p_Azbvje&HIabYiYoM)T5Gb+u4&tMC=)QvgHeb3a9x1@}i&W%0&^xyGRf)8cQf`57)@&!(OM_o;AC(ce``8Yd@Xy@7e3iWM<7; zYmCW~@f>i1J|u4hYM%<)_>OYci7t+Bz6>0u(4r0zu;U#Cww&+Dadg~ z^s4Y!>mfLpF>I1CGc5XH02;U}4Ag!62qD4xN;XkPk*ZIZZ@v^9Ydp80EigLM$uhoN zM$K2%d$IHPyJdov$V$0Lf$xGCtg7DDs{Qwm z1i2(zKgN*9HV6V{3@?iIuIhTIsPj~{vc)Q67PqOVF5~%=s}K~-81_g+x?6SrL#tW& zuQo%zJIwppKv>hkMv7XlI$r|7HHy{M>{(ePV8(C|fTOB5S;mg@iDe6-0!v5nf$Hhu zh3U5yEr$yLY#MK*09L8m9(x7O#{it>fvCV% z+Y1b;0r*KfQ>g;r0z;z$dkcVHv=T)2WgSUHTLHioMnnZxCeFJn+B^UnwWFNHqK%FU z>;(z3Jrr#&04XgcWT9wdqn@#3s2OYGiKxJ418_!NV-)~g%FY^FI6v)0J}otW!sNiRJtlICEiC?Mq8D551Fq`ZS?hDQ`Kpr zP6*b!SmQKfWx!|JDQJu_oCKhrUOrJj6}S=AT*~?{{)DY%+}ydSuyp*rDt2p_{#c1r zF=6^+y`*o^U`ybgS$eM{OJ#>TK+z$V4(zVa!-s)-j?K4%4NM7+S#%YI$F zu+sVeMg3UM7S^EV=JU9C%~$%eqB34sc~YGnHE}aafiv_s|2O*wPpc1$szc8Z00000 LNkvXXu0mjfX_+vg literal 0 HcmV?d00001 diff --git a/Images.xcassets/Call/CallSpeakerButton.imageset/CallSpeakerIcon@3x.png b/Images.xcassets/Call/CallSpeakerButton.imageset/CallSpeakerIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..531fff536d0e931a27b58785028533d7705044c0 GIT binary patch literal 1918 zcmV-^2Z8vBP)F>e-h21;z212GU-P>&_q%gv=FBS?&c#eIGPA6%SS7CLUK)sfIRn52QbTN^r?}-pGA*unkbm?QDly16U?Q_A*<2t=p#wE7 zdZ)dP5KBKkQxH~k& zaa@6LA(bLioEo;9o;Yo_(+?93+D;T`?PI|Mrm&b`%ypk+zED1HJp1pkJrq+}WW1)! zcYY=tSZDZKYyFzqLDt}?R(@|f|MQ~O=5d!n`}G~1JC+j}Gk83V7TP|B8*jtkT06vg z1|K2m);}*s2{K9SfGdsX%Y#}w&!8QChoy(h(ScNor?mDejSSj_TD!-6p`w#s)CzUH zhuo+^Ru@~z1cP>zR_3|~Dyc)B){c%kq*5%fb*O2lXJw&#JdQ*Wp_m#AkeOP2o@#~- z%o+7@h7D(nlbj_X`jFK)tMxq${<5=IzG-N6xu^}P8Fk1ayPCVj&$-K2IX@ib)IOcJWj`0fb6c5H9u0pRXIY$RV!Qi zQWpX>K2e5j#a{va2`afN`{|Z1x!3K8kV$S}yNNR7-8v|3as&H_>Hyaum`KZB=HXa? zY@ko5m#AWy?Z~69t8#!)X8b>rr3Beh&&gAUIr*sdoE#c!bF!NbS7aGhSLX{`G}1rBNDYtfJKq>3!Z^VTuAJn9?L>bb>Iu~CfH4Hr3%X4aX<1A&l6Ny?5Q zTK7B#QgCh7m^}!D_J_6Z$9f^)1zbFdDjG=)v9foe6@p=m5?KheQy=sj%(R2$s7-^ z!tzdO^n6e9lj-{G0ejsYbERm`4cnq6I*82Y;7~Ko8DdioHPhTcr0tv)(8j;LX{vw} z#b-BY8oAOIQ_Vm*5kZAT7PUN1b3)j6|43J<-HNl+Q>)KXLq*DHDzf$AeHT)$$ONz`o2Q!? z_xUQ4Mn&HC1agW%JZCywDW#NnDEYKgkv4_}YP>@KR;!CRNm!jFXfIZT^{O)b6tv?Z4i1`0?+V~MMj*Vg*=lXLT zXOBuj*2Pc+BSKGs+&~7=c(&iof?!iik=4Dj;Y1 z^Hr@Ritabt`~*tb#C%K7#)itTMiQ^EjSqeMt5~7fzrn6U(0dhH6aWAK07*qoM6N<$ Ef&pWqK>z>% literal 0 HcmV?d00001 diff --git a/Images.xcassets/Call/CallSpeakerButton.imageset/Contents.json b/Images.xcassets/Call/CallSpeakerButton.imageset/Contents.json new file mode 100644 index 0000000000..6ae257da6a --- /dev/null +++ b/Images.xcassets/Call/CallSpeakerButton.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "CallSpeakerIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "CallSpeakerIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Call/Contents.json b/Images.xcassets/Call/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Call/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/Tabs/IconCalls.imageset/Contents.json b/Images.xcassets/Chat List/Tabs/IconCalls.imageset/Contents.json new file mode 100644 index 0000000000..14ee7b04c8 --- /dev/null +++ b/Images.xcassets/Chat List/Tabs/IconCalls.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "TabIconCalls@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "TabIconCalls@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@2x.png b/Images.xcassets/Chat List/Tabs/IconCalls.imageset/TabIconCalls@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fc1df7e1b36e71be6c00b182691922f13957e516 GIT binary patch literal 1019 zcmVdHT?k%Ne+tIzlMJL|B` zVeD$;E=0G&i{)UHYluTEu(f4vFq;*UtZkj0Zx_u!XXl(@o!?J_e13QD%Q@fYdEV#! zKHuj}B<|V&#KIRCU{>Gi#k&O9u|&u4-=8w!uDbx)R82qD@4S{;8Z=;ywJnP~t?L-V z3x`un9Hpdk%KaYyB6o)>nY_NRv3Mx z!#K6Jpi2*HMw#%js3Yyh!JY-2YQ#Dp^&LHgr3J{bjNfo-b+lZx4QtpX1)W-M=D6Nfollu|oi+ETm#OlmLk!zV& zMq6V6ZLZ-uPHxX}rW4a1Tc=SYrZ2mOliRT}L2YcHtrZ%>!T#IVTB;bw!GSm%`CiPm z?6|^R$|xN^5hJWRuzI~HizJU=T@Ba82HH}kQ5-bPqmE9j?`{d1=wp47S!T1@DOFBC zW}v+?HdyL06WJ%*RLv!xhS z*kF@un8+0=!6tsjI-M<4qwKLen8eiQz>Z^`iLIoxTWQSe^K!63qnLrF>U_;U0>acj ztW5CgyuYsH5@zV*hq6}p(1$S>l8+ZljBjD3^-}ig(Um%j`GuWx&TUmM#!Ty1tPIH_ zb+Qg({+th+Dpw~~n)=jgK2ZiUt~XjMI?6br8O-JI>Efboz)JH<&TW2I8LSLuO7Gu_ zExCnr7^@|iA?qpS@LargMci8^F(>kklm%ZgHZy>g;pKl1`zRBb!GAJf^ppngVTOw1 z-aqsR`VlbBh(bEl`vn^<2J-EXO97OVG8m^QhENv!815j7gcTnqLW zp7rL&mEj0qqgtxefiZoBZt6{K)gP(I3HE9XV|C^my}B0b6lN%L71>&$My_L}b)22I zr@Fn8&#|t=q?em(NNNUS4NKacT+_8U+@WtUBf|}MU1nnL?62OL-F|!a zJil||;$kbdVk@@(!wSrW_u)4df^VTHm}E87U?SfNJuDQt;6!z@wP4*eI64580GC+` zmS@0af_RuO3QXg#%fFnbYy_Ld+7MnZ6o%sEZ}16%|FtH?GO(}`x)5v|EF1W>;!gG# z0tv&}z8b{~zJ&(2*G!n0 z$gYP|MmNo~`e1~E1E#e17=GMWJWIC{rCVysH>^Wx9ru`Fq)KPTo-_n^_jH+-m(Ta~ zElLsWMU#Ozi1Kwk80LNQ)nJ85xQ`-)4pnMC?4+#RPMI!!zCd%^IRaVoIAy(y48tYw z+u_WN644T%(V)j2WD#Fk30#0!c{mOiSPf6Up{Zh7we{E*!$$ zKg#a01aR3xidu!r>@AJ`B15n>1RsAB;da48n%6=7hC_y`2}LkCfqIm>UTU=T_0&0bM z%Kao_bV9J!4wrU^Y^vy(Ns&XGmRSa0V@HfzAPFg`cPFU!Yf@N)Re<@^l2wOVwT}o) zW9KAbOH@MUVdC1MtBWOIajmyQCEg>G(lEePQ>99XYl{t0E*|y}4V|k#A$0h34Hh@i1fmcqdrlc44=|B_)u+@s5qIdRCGA%oK^ZW!l2!d9 z$YI|NYKmWE5BuvQQsDyrqfTA#Qlv%>0sh{dGvwaIvTPmF!|&nm25<8>Y+RDXy}p`Y z1(a+29eF8{E`8h&@lUpczsR;wi6wd*zBXlGhHMP5c71o)d7kp&KDLdg@?z*9Xzft6 zYchH3*^=`_e+@p4cDS=xi&P1n4hz5!P{k1%m^gut+*!lC&06Y|hTv!C2_FGV2@9jk z(d)VFcMzN}`C`qJBHpBr1$^s;3YEA-vdh)`YEd-{PF@Tj+-cKqTPcaQ%jbk@EL2F?c@-Y#G1^ttK`_#U+LtpMc zFHu6-SQp!@qFX&FRx!SG_e~*W1yx1vo}E+^+TN%xzV&7m`stFr3^VYMukLZ^R^Waz z*`uSoZ*0Cefo-P2&k!`~x=5WXSjwqIPM(;JdQ>X>mWpHGXr&FH}A%m+MKM=#?jWpuC(lA3II-!GVU^osh57$gV z8@3SGJS#{mCtG&&>FQ+-Py{Q==hviECyTaCJN4>T$Jw*QyhNQGUQ}LJ*z!2r*6!(S zy(ri_Zy-p%G2#t8W?U0tnHaR&&wda=p&Od`MxXhz*ov*#`k$_U0lEj+FV*9N82|tP M07*qoM6N<$f=a0Eg8%>k literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/Contents.json b/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/Contents.json new file mode 100644 index 0000000000..16a7f1ac7f --- /dev/null +++ b/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "TabIconCalls_Highlighted@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "TabIconCalls_Highlighted@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@2x.png b/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4270a1212d0a81f9d920bea2a22e4101fcf9280f GIT binary patch literal 747 zcmVgJr6-}1USL(SItQUYJP9==bru7m^wg8cTqDs7fkx6k??Ror z9`ugRON&rbidrDBERJ?go6zN->LReDQWYTSNg4*y84jjlu4kc<=tu?@Fy0V?e%TA9 zVE~S=Whw-dZYnNS}1C~T+yvwfsK_uEAED@CB zva4^L7=|6{V;uFGIbp!B8wMv{=MXIJqz@j1LyH+rsxA zf=X2aE6h=pbjQLm?Pn-9-UgDAcAcdv+{E^P(BEvnS$+db#l{$eU*- zhOHEOhUJ#?7)uiEQ5+FH)>+Km82O%$J#S?V0gcUH`*WB2c zdCSiJG550*q)FOl#FQx{S2P5^p`yY|7HqNmZp6xWH92+GzNCJINOZVgR&kbB0TzNc zneO^Djp(l*lP?D16EcUUV-uO;A9^vo4Zao$&o@LQ_|Myu% z3tc1?cBN=sU}70}8I+5Xv@L*v8|9=|C|wM0Y-4g`sdYLgZSYe^vyC{vKj@3)agFoJ zk>=M6g`KGuF4%e dAt52$`46Pva3?+kt?d8+002ovPDHLkV1k4&SuOwo literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@3x.png b/Images.xcassets/Chat List/Tabs/IconCallsSelected.imageset/TabIconCalls_Highlighted@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9e1be91dd9548f2a1f87656d5c19b0bb2a5237fc GIT binary patch literal 1100 zcmV-S1he~zP);pnsi3I>MdMj&o1k}bbv6^aiC}1M zX!}Mkvfa>hJGy~Tb}K>CUo5XHVQ5=)0ik5Kfu`~A(A(z!d!P`Sr|rFSh5~gHUoi3p7`l)}gbHnMMY3r6BdA{YI@S3ziowoiJ?gk3gtE(p!2(qhmw z!qa|Rg3t=%&h{ghCZLpUg*Hgr=og*@_k(eZLuf|1?;75&#hSj8E}YgVy1?Tt?d9f` z2*K1%$(`f=k~a^cu5Jnl%}x)B2Ky6iuZGZ!lHH9PJp64D7eNSG%sd4`i{sf_o^!m% zqY1%e^K$p1&t5V^N23X$oUOubP{9Pt3f`Howmt>R7SRktX9lRVor)#|LpKBvd@%rl zZ(G_v9-s_=E&t!C!&B?Zz8*mE!#uOmO*qA=>3VLR()2VG9mM}nv|QP57AX4MzhL_s zt{6BQx`J~1K+_NHXXV_wh!rmBDEc-)&%=+X^jQk+QSP%r*9xoLfPD?FtD7O-ONOSO z{0p`Nx%A&Up=9h6J(JxTQJBt0H1~375oX&bSbnQ)vNzYyPM;Dgu~FH(DYZR z`3jH!*Pkf4Ef5{m0gHqmXz_MCyp5+l-si6WY-a)?|+=*DeRH+?+Go;;ftJ`$~1D|eRPgnP)K z-(#(442xjpj^R&Sq&2y}C-as2XVg_?k7axRVd#z;=!_@CS3_?5aMQD1uXU`|nP<}sH==@0; z?vzyYCMR1um;1HPQ$bqKdX?M`;48I#wjqAz44kHMO;zdh!uOB~f*=TjAk+gvho6u- S;pmwF0000^_ literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Info/CallButton.imageset/Contents.json b/Images.xcassets/Chat/Info/CallButton.imageset/Contents.json new file mode 100644 index 0000000000..14ee7b04c8 --- /dev/null +++ b/Images.xcassets/Chat/Info/CallButton.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "TabIconCalls@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "TabIconCalls@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Info/CallButton.imageset/TabIconCalls@2x.png b/Images.xcassets/Chat/Info/CallButton.imageset/TabIconCalls@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fc1df7e1b36e71be6c00b182691922f13957e516 GIT binary patch literal 1019 zcmVdHT?k%Ne+tIzlMJL|B` zVeD$;E=0G&i{)UHYluTEu(f4vFq;*UtZkj0Zx_u!XXl(@o!?J_e13QD%Q@fYdEV#! zKHuj}B<|V&#KIRCU{>Gi#k&O9u|&u4-=8w!uDbx)R82qD@4S{;8Z=;ywJnP~t?L-V z3x`un9Hpdk%KaYyB6o)>nY_NRv3Mx z!#K6Jpi2*HMw#%js3Yyh!JY-2YQ#Dp^&LHgr3J{bjNfo-b+lZx4QtpX1)W-M=D6Nfollu|oi+ETm#OlmLk!zV& zMq6V6ZLZ-uPHxX}rW4a1Tc=SYrZ2mOliRT}L2YcHtrZ%>!T#IVTB;bw!GSm%`CiPm z?6|^R$|xN^5hJWRuzI~HizJU=T@Ba82HH}kQ5-bPqmE9j?`{d1=wp47S!T1@DOFBC zW}v+?HdyL06WJ%*RLv!xhS z*kF@un8+0=!6tsjI-M<4qwKLen8eiQz>Z^`iLIoxTWQSe^K!63qnLrF>U_;U0>acj ztW5CgyuYsH5@zV*hq6}p(1$S>l8+ZljBjD3^-}ig(Um%j`GuWx&TUmM#!Ty1tPIH_ zb+Qg({+th+Dpw~~n)=jgK2ZiUt~XjMI?6br8O-JI>Efboz)JH<&TW2I8LSLuO7Gu_ zExCnr7^@|iA?qpS@LargMci8^F(>kklm%ZgHZy>g;pKl1`zRBb!GAJf^ppngVTOw1 z-aqsR`VlbBh(bEl`vn^<2J-EXO97OVG8m^QhENv!815j7gcTnqLW zp7rL&mEj0qqgtxefiZoBZt6{K)gP(I3HE9XV|C^my}B0b6lN%L71>&$My_L}b)22I zr@Fn8&#|t=q?em(NNNUS4NKacT+_8U+@WtUBf|}MU1nnL?62OL-F|!a zJil||;$kbdVk@@(!wSrW_u)4df^VTHm}E87U?SfNJuDQt;6!z@wP4*eI64580GC+` zmS@0af_RuO3QXg#%fFnbYy_Ld+7MnZ6o%sEZ}16%|FtH?GO(}`x)5v|EF1W>;!gG# z0tv&}z8b{~zJ&(2*G!n0 z$gYP|MmNo~`e1~E1E#e17=GMWJWIC{rCVysH>^Wx9ru`Fq)KPTo-_n^_jH+-m(Ta~ zElLsWMU#Ozi1Kwk80LNQ)nJ85xQ`-)4pnMC?4+#RPMI!!zCd%^IRaVoIAy(y48tYw z+u_WN644T%(V)j2WD#Fk30#0!c{mOiSPf6Up{Zh7we{E*!$$ zKg#a01aR3xidu!r>@AJ`B15n>1RsAB;da48n%6=7hC_y`2}LkCfqIm>UTU=T_0&0bM z%Kao_bV9J!4wrU^Y^vy(Ns&XGmRSa0V@HfzAPFg`cPFU!Yf@N)Re<@^l2wOVwT}o) zW9KAbOH@MUVdC1MtBWOIajmyQCEg>G(lEePQ>99XYl{t0E*|y}4V|k#A$0h34Hh@i1fmcqdrlc44=|B_)u+@s5qIdRCGA%oK^ZW!l2!d9 z$YI|NYKmWE5BuvQQsDyrqfTA#Qlv%>0sh{dGvwaIvTPmF!|&nm25<8>Y+RDXy}p`Y z1(a+29eF8{E`8h&@lUpczsR;wi6wd*zBXlGhHMP5c71o)d7kp&KDLdg@?z*9Xzft6 zYchH3*^=`_e+@p4cDS=xi&P1n4hz5!P{k1%m^gut+*!lC&06Y|hTv!C2_FGV2@9jk z(d)VFcMzN}`C`qJBHpBr1$^s;3YEA-vdh)`YEd-{PF@Tj+-cKqTPcaQ%jbk@EL2F?c@-Y#G1^ttK`_#U+LtpMc zFHu6-SQp!@qFX&FRx!SG_e~*W1yx1vo}E+^+TN%xzV&7m`stFr3^VYMukLZ^R^Waz z*`uSoZ*0Cefo-P2&k!`~x=5WXSjwqIPM(;JdQ>X>mWpHGXr&FH}A%m+MKM=#?jWpuC(lA3II-!GVU^osh57$gV z8@3SGJS#{mCtG&&>FQ+-Py{Q==hviECyTaCJN4>T$Jw*QyhNQGUQ}LJ*z!2r*6!(S zy(ri_Zy-p%G2#t8W?U0tnHaR&&wda=p&Od`MxXhz*ov*#`k$_U0lEj+FV*9N82|tP M07*qoM6N<$f=a0Eg8%>k literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Info/Contents.json b/Images.xcassets/Chat/Info/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Chat/Info/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Search/Calendar.imageset/Contents.json b/Images.xcassets/Chat/Input/Search/Calendar.imageset/Contents.json new file mode 100644 index 0000000000..622e8d2050 --- /dev/null +++ b/Images.xcassets/Chat/Input/Search/Calendar.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ConversationSearchCalendar@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ConversationSearchCalendar@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@2x.png b/Images.xcassets/Chat/Input/Search/Calendar.imageset/ConversationSearchCalendar@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a9e4237a050ce76feb31fd997af07fd4c0cfcf81 GIT binary patch literal 510 zcmVPx$xJg7oRA>e5TG4TXFbtJ*OhKpVZ}ZbVx?dYzc2c_QN+J14Svwe2poLC?>37mk0&=z~_z+n=Z zl=ys7fG-?IotR|Inj3IeuE^6K_TmoH&-Fz70gI z`lsTdGGh9Lnip!{f`fAg4h8iHCu>-TsGOZKw8b7faCnN~m!BOt9-}Sx*ny+wEXS_* z*d(d-rYhknLM@V!y%Nbtj4_l)kK`6vU_8Z?Rx=Z?yVjfv(Su@2s}d2dqXqCsv%4Q& z`k&$V?6YeYJy+@*KHBqA44%@;j08rn^~(|ow9$%~8NO}VT-G1yW8!k&J&HJEm|45M|w^j~T>53FnT9pV_=YNa002t}1^@s6I8J)%00001b5ch_0Itp) z=>Px&y-7qtRCodHoIh_9F%-a`(~Bwr8$*DK4Hm?}Hz+&oj7WR{HUCD2$#Dr4lzy?FwDCYV87+=n=-@Dvho7}T>I_LS>e*T?(_SudV)O80xcVVvIV)T0e zdYC1nPVxQ~67>wm*AMF^y>h&kY*V!id;R++oXoxIn$4;-Zksv=1@G~Co@Ai((`GH+ zq|~qiX+S;-23d{Tq{6jS7HoM*sp*+sjp8QL37BSfmNPxkWM^`gN-JSa+cuu+%uYR$ z7d+#M2`mOIY=(85`i0Fmi|F=xrJl&>oQG@Awt6)y+5U*V!`~nZ^KF>w1U1?VXwkNM zu*?T?qVW4reZkAjG~~|sQMRQXp36tHv7$xW6b>Bo_JkQxd&3+}FK!N>dNr#Tl&Xak zSXCxLiNFe?sRjtFDwCi@U$xrp+Gn!#}E1H2l2 zkC{g+{ORly?u@w`OKd&Eaqe?Ie}cB(j@eIpXKgH~9FK-uF;FI)R~=4@p+gJnQbgVqfF zaeF5fSd2$5uml!^07F%3mcU{VV5pWCEJ8D-&TJtg?e$V6lwF!3B#v>P49w;G^zzUJ^$oWGD zGbEt6Y^F5RG=6eCCvkF|bXIn?yqs)9UOp^$+BcMHD#wKY>u`E`0r?cO6~;YX<`uUF z{}m>VOn_grx?Du)GA{x$KCCVm5xUHa085%70aBqNunH1{yTBqqDpUkkL4t4>SOiFg ziohyJ5bm;IQ9ugK$~rT*w3?;hq)2lk@5J)aV1asv2WD(0u(VuQK7yl-;P-F5z_iTO zYe%eEy=G(!FR(0%xJO{c8Cea1Wl_XE0xQnQYT%&s$eb%fJCV&s>!U{<57Hyr`dD^u xneiyQ#GGY^=f{{si|A+maWh(L{r_kF{R6f1GE(rC#$o^f002ovPDHLkV1haF-Jt*g literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Search/Contents.json b/Images.xcassets/Chat/Input/Search/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Chat/Input/Search/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Search/DownButton.imageset/Contents.json b/Images.xcassets/Chat/Input/Search/DownButton.imageset/Contents.json new file mode 100644 index 0000000000..86eb5eb72b --- /dev/null +++ b/Images.xcassets/Chat/Input/Search/DownButton.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "InlineSearchDown@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "InlineSearchDown@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Search/DownButton.imageset/InlineSearchDown@2x.png b/Images.xcassets/Chat/Input/Search/DownButton.imageset/InlineSearchDown@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d1a1a9038eee7f02efc6ea915d47422786ca453a GIT binary patch literal 251 zcmeAS@N?(olHy`uVBq!ia0vp^sz5Bl!3HF`c6{LmQinWU978G?k6t?J$Z9CT@?hsB zhIcWARu4S<1@Bk}3a36WeA8n0bZ3IS-R6DLKPTxV{MG4+KOQuPL0db#c0(Pfna%>C z$NdXpHN7LLgGylc>><I?-rEU5}>KM~W10j#Li6A+l1SJot1M-JC6gZDDavD6A yVN^NrLgAOaO6t=phHLK+?5frIY5B)#9%JaY?fSkmPi_SIhQZU-&t;ucLK6T=U|szH literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Search/DownButton.imageset/InlineSearchDown@3x.png b/Images.xcassets/Chat/Input/Search/DownButton.imageset/InlineSearchDown@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ab4719206a3a8b8c71958aa6855a65d523d03630 GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^7CVBymPm7=`1$MXM5{~|L1P1@Sd-|K>th9lPyfE zURG;4&Chn9|NcmdhJWb}%NcW*e=p*z-516G{8U2uwEZ&Ia`!ZN-?M&u&X=j?bq{9{ zOZuItq#rBn!hf*jsmgMBFny0=-l+KVf6wl#jzT-^%qL6B%rc$8a_*O_%EO7bEaWtM z9#1@kgId{zTkhnel8MLG3R}dgG+uVzWRa=T80`KoCas`0kMF;hGyBvRpYIy%W3SyP cXg?#}V&Wv0%^Pg^fFZ-+>FVdQ&MBb@01KFe+5i9m literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Search/UpButton.imageset/Contents.json b/Images.xcassets/Chat/Input/Search/UpButton.imageset/Contents.json new file mode 100644 index 0000000000..eb53b93d1e --- /dev/null +++ b/Images.xcassets/Chat/Input/Search/UpButton.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "InlineSearchUp@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "InlineSearchUp@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Search/UpButton.imageset/InlineSearchUp@2x.png b/Images.xcassets/Chat/Input/Search/UpButton.imageset/InlineSearchUp@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d70227406382c51797b40d5b81db825697444bf9 GIT binary patch literal 230 zcmV@@3-MVKxQ91y&M#Z}W}^jtl1m#KImcG)un}Xp zJQkn>HsbRAav2%u^%J=Y1KP^tA7m>|JdB^vKj0tA!)Va!c!pUtbOR$mJd6kNd9Fc3 zzI%`}7*mL^=N`n@Wk3YXEr@`*1`#m#AOf}(sS6MR>jH_~TY%id5;Xuy(|JrQ)xEF_ gqnIKQ6C{`V6=J8l(=hpOQ%nApigX literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Input/Search/UpButton.imageset/InlineSearchUp@3x.png b/Images.xcassets/Chat/Input/Search/UpButton.imageset/InlineSearchUp@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8b9d50e849ed681493143f8525385d80e7fe1481 GIT binary patch literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^7CSE#`uOpetGW)*9>-r&r-11{)zW{eT3(F@FF-_BbEIb~tOw5zYH$Nj_;kab} zQSZb1xQ@-;KX*_2$Grh(cJQ(Mdv^2Vrmg$L{#ZrCS%IFWbC=D?$~7t7?^GHqj> WwnDpJ_&P9P7(8A5T-G@yGywogL5Onz literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Message/CallIncomingArrow.imageset/Contents.json b/Images.xcassets/Chat/Message/CallIncomingArrow.imageset/Contents.json new file mode 100644 index 0000000000..c559ed6952 --- /dev/null +++ b/Images.xcassets/Chat/Message/CallIncomingArrow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "MessageCallIncomingIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "MessageCallIncomingIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/CallIncomingArrow.imageset/MessageCallIncomingIcon@2x.png b/Images.xcassets/Chat/Message/CallIncomingArrow.imageset/MessageCallIncomingIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..08e0bd3090e469061103b8c88f9025ffcb914292 GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|mGCf@!Lp07OCmi5Uk&uz((As(U z!`y?t6DIw4*fD#`#CD8Rj zqfztFYL-PA(sil=uJb=?H)=2)^yZ1<@$bP0l+XkKTYoo% literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Message/CallIncomingArrow.imageset/MessageCallIncomingIcon@3x.png b/Images.xcassets/Chat/Message/CallIncomingArrow.imageset/MessageCallIncomingIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c512787ff3f48a5465b1855cebe120a68863faea GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i1|)m0dbP0l+XkK$-Fqn literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Message/CallOutgoingArrow.imageset/MessageCallOutgoingIcon@3x.png b/Images.xcassets/Chat/Message/CallOutgoingArrow.imageset/MessageCallOutgoingIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0883be8cf266636abc93c4858f52e2724c9f3c93 GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i1|)m0d+_tn82<0P$()v_DON;gb0pKY=rBCTIO5P iC;B9W)+qW*l`$V+{9?K4Tc;h+E(T9mKbLh*2~7Z!%{W>B literal 0 HcmV?d00001 diff --git a/Images.xcassets/Chat/Message/InstantVideoMute.imageset/Contents.json b/Images.xcassets/Chat/Message/InstantVideoMute.imageset/Contents.json new file mode 100644 index 0000000000..d2a8671fea --- /dev/null +++ b/Images.xcassets/Chat/Message/InstantVideoMute.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "VideoMessageMutedIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "VideoMessageMutedIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/InstantVideoMute.imageset/VideoMessageMutedIcon@2x.png b/Images.xcassets/Chat/Message/InstantVideoMute.imageset/VideoMessageMutedIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6e933e1e40e941210cffbe605392b96e0c2d604f GIT binary patch literal 593 zcmV-X0Px%3rR#lRA>e5n9nOjQ547LiLmiwuwYg!NV2rBP_tE5wk(u|wSR&|V=58cRdx5oCQ1wM4@Wym~78#RR!WFaOuF!~mN{ zpwl+O9m$vMb+7sJHa}5e9aXqi*Chm|`M?e2nP=>WnYY0n>ubg(n-4Nl@D1Gf_Y>gBhUK5J-H&f)&a<16LriYV$!x3I;PEfU{>*TaSW+o zd^_u4mWZQ3+h#+xN+$UO;TJZ~b7l2!V%LAAUGYKT)xn1Lh6OJi3tR&PSPm}WsQPFq zvd3-yfe9UJ>Q8=q;-CBV+?RDTXUyEc@WrZE@9J0WpE)meb7Vw-Fr%OVZuCN_@nXgn zuHAgUd3N79YI|;vI)f$qZ#M7T;cDxk8^I@%T%z9B#2~Nf+1Y zF!0vTVL6efr;*Yh?6N|e-{q~{hM%_{Ux{|O@xxQ1-pWk-+|FukITiI?eyE}9L zu~OxKPLEynUdv3epYi!Ley$}=pQ)@)sG7k^6*>Fi?~~GRrsqVDi=RE4IVpYaiT1X=J@KqjWi0PrzBntac;}-X9e3uaRbKmOckO<$ zRN0k#yYBezUw8F}XmD$c%hWjs6J@_CmR>)(*KH3=blcwIWb=yL)%&!UaP92SvTQyK z^v;<~^{;Fn1Et~}{~SDPIq{hH@54VP*}2WW(@-y{|0G_M{r*0O_=Z|9%P)J{E`5C$ zBi~v!eUbNq6JF-#>QT>&*GxXPAU%G=`ku$>Gd52?w&VDo&?DiA(Hj(>wQl`1>%@99 zA>M0G16-c}zhn4bFq++EU-Pr*zT4q^Ppp|LPW}3=9)4U&`qpoTGU;i5d3lnBb+vC? zWfZt@-p)5q&*5eJVXJ_Kkm@(DOsWj+-ro4i @@ -45,7 +45,7 @@ @@ -63,7 +63,7 @@ diff --git a/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist b/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist index b4b60aceba..4c50bc14ee 100644 --- a/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,6 +12,11 @@ SuppressBuildableAutocreation + D0EC6CA41EB9F4CC00EBF1C3 + + primary + + D0FC407E1D5B8E7400261D9D primary diff --git a/TelegramUI/AccessoryPanelNode.swift b/TelegramUI/AccessoryPanelNode.swift index a472cc91d6..1a41aa8d9d 100644 --- a/TelegramUI/AccessoryPanelNode.swift +++ b/TelegramUI/AccessoryPanelNode.swift @@ -4,4 +4,7 @@ import AsyncDisplayKit class AccessoryPanelNode: ASDisplayNode { var dismiss: (() -> Void)? var interfaceInteraction: ChatPanelInterfaceInteraction? + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + } } diff --git a/TelegramUI/ActivityIndicator.swift b/TelegramUI/ActivityIndicator.swift new file mode 100644 index 0000000000..8a5bfa2b69 --- /dev/null +++ b/TelegramUI/ActivityIndicator.swift @@ -0,0 +1,54 @@ +import Foundation +import AsyncDisplayKit + +final class ActivityIndicator: ASDisplayNode { + private let indicatorNode: ASImageNode + + init(theme: PresentationTheme) { + self.indicatorNode = ASImageNode() + self.indicatorNode.isLayerBacked = true + self.indicatorNode.displayWithoutProcessing = true + self.indicatorNode.displaysAsynchronously = false + + self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme) + + super.init() + + self.isLayerBacked = true + + self.addSubnode(self.indicatorNode) + } + + override func willEnterHierarchy() { + super.willEnterHierarchy() + + let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + basicAnimation.duration = 0.5 + basicAnimation.fromValue = NSNumber(value: Float(0.0)) + basicAnimation.toValue = NSNumber(value: Float.pi * 2.0) + basicAnimation.repeatCount = Float.infinity + basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) + + self.indicatorNode.layer.add(basicAnimation, forKey: "progressRotation") + } + + override func didExitHierarchy() { + super.didExitHierarchy() + + self.indicatorNode.layer.removeAnimation(forKey: "progressRotation") + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: 22.0, height: 22.0) + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + let indicatorSize = CGSize(width: 22.0, height: 22.0) + self.indicatorNode.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: floor((size.height - indicatorSize.height) / 2.0)), size: indicatorSize) + } +} diff --git a/TelegramUI/AddFormatToStringWithRanges.swift b/TelegramUI/AddFormatToStringWithRanges.swift new file mode 100644 index 0000000000..fcd2c8a3db --- /dev/null +++ b/TelegramUI/AddFormatToStringWithRanges.swift @@ -0,0 +1,28 @@ +import Foundation + +func addAttributesToStringWithRanges(_ stringWithRanges: (String, [(Int, NSRange)]), body: MarkdownAttributeSet, argumentAttributes: [Int: MarkdownAttributeSet], textAlignment: NSTextAlignment = .natural) -> NSAttributedString { + let result = NSMutableAttributedString() + + var bodyAttributes: [String: Any] = [NSFontAttributeName: body.font, NSForegroundColorAttributeName: body.textColor, NSParagraphStyleAttributeName: paragraphStyleWithAlignment(textAlignment)] + if !body.additionalAttributes.isEmpty { + for (key, value) in body.additionalAttributes { + bodyAttributes[key] = value + } + } + + result.append(NSAttributedString(string: stringWithRanges.0, attributes: bodyAttributes)) + + for (index, range) in stringWithRanges.1 { + if let attributes = argumentAttributes[index] { + var argumentAttributes: [String: Any] = [NSFontAttributeName: attributes.font, NSForegroundColorAttributeName: attributes.textColor, NSParagraphStyleAttributeName: paragraphStyleWithAlignment(textAlignment)] + if !attributes.additionalAttributes.isEmpty { + for (key, value) in attributes.additionalAttributes { + argumentAttributes[key] = value + } + } + result.addAttributes(argumentAttributes, range: range) + } + } + + return result +} diff --git a/TelegramUI/AlertController.swift b/TelegramUI/AlertController.swift deleted file mode 100644 index 5656c9b853..0000000000 --- a/TelegramUI/AlertController.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation -import Display -import AsyncDisplayKit - -class AlertController { - -} diff --git a/TelegramUI/ArhivedStickerPacksController.swift b/TelegramUI/ArhivedStickerPacksController.swift index 058af3efdc..68c7a70fb8 100644 --- a/TelegramUI/ArhivedStickerPacksController.swift +++ b/TelegramUI/ArhivedStickerPacksController.swift @@ -56,7 +56,7 @@ private enum ArchivedStickerPacksEntryId: Hashable { private enum ArchivedStickerPacksEntry: ItemListNodeEntry { case info(String) - case pack(Int32, StickerPackCollectionInfo, StickerPackItem?, Int32, Bool, ItemListStickerPackItemEditing) + case pack(Int32, PresentationTheme, StickerPackCollectionInfo, StickerPackItem?, String, Bool, ItemListStickerPackItemEditing) var section: ItemListSectionId { switch self { @@ -69,7 +69,7 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry { switch self { case .info: return .index(0) - case let .pack(_, info, _, _, _, _): + case let .pack(_, _, info, _, _, _, _): return .pack(info.id) } } @@ -82,11 +82,14 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry { } else { return false } - case let .pack(lhsIndex, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing): - if case let .pack(rhsIndex, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs { + case let .pack(lhsIndex, lhsTheme, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing): + if case let .pack(rhsIndex, rhsTheme, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs { if lhsIndex != rhsIndex { return false } + if lhsTheme !== rhsTheme { + return false + } if lhsInfo != rhsInfo { return false } @@ -118,9 +121,9 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry { default: return true } - case let .pack(lhsIndex, _, _, _, _, _): + case let .pack(lhsIndex, _, _, _, _, _, _): switch rhs { - case let .pack(rhsIndex, _, _, _, _, _): + case let .pack(rhsIndex, _, _, _, _, _, _): return lhsIndex < rhsIndex default: return false @@ -132,8 +135,8 @@ private enum ArchivedStickerPacksEntry: ItemListNodeEntry { switch self { case let .info(text): return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .pack(_, info, topItem, count, enabled, editing): - return ItemListStickerPackItem(account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, sectionId: self.section, action: { _ in + case let .pack(_, theme, info, topItem, count, enabled, editing): + return ItemListStickerPackItem(theme: theme, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, sectionId: self.section, action: { _ in arguments.openStickerPack(info) }, setPackIdWithRevealedOptions: { current, previous in arguments.setPackIdWithRevealedOptions(current, previous) @@ -189,11 +192,19 @@ private struct ArchivedStickerPacksControllerState: Equatable { } } -private func archivedStickerPacksControllerEntries(state: ArchivedStickerPacksControllerState, packs: [ArchivedStickerPackItem]?, installedView: CombinedView) -> [ArchivedStickerPacksEntry] { +private func stringForStickerCount(_ count: Int32) -> String { + if count == 1 { + return "1 sticker" + } else { + return "\(count) stickers" + } +} + +private func archivedStickerPacksControllerEntries(presentationData: PresentationData, state: ArchivedStickerPacksControllerState, packs: [ArchivedStickerPackItem]?, installedView: CombinedView) -> [ArchivedStickerPacksEntry] { var entries: [ArchivedStickerPacksEntry] = [] if let packs = packs { - entries.append(.info("You can have up to 200 sticker sets installed.\nUnused stickers are archived when you add more.\n\n")) + entries.append(.info(presentationData.strings.StickerPacksSettings_ArchivedPacks_Info + "\n\n")) var installedIds = Set() if let view = installedView.views[.itemCollectionIds(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionIdsView, let ids = view.idsByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { @@ -203,7 +214,7 @@ private func archivedStickerPacksControllerEntries(state: ArchivedStickerPacksCo var index: Int32 = 0 for item in packs { if !installedIds.contains(item.info.id) { - entries.append(.pack(index, item.info, item.topItems.first, item.info.count, !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == item.info.id))) + entries.append(.pack(index, presentationData.theme, item.info, item.topItems.first, stringForStickerCount(item.info.count), !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == item.info.id))) index += 1 } } @@ -286,18 +297,19 @@ public func archivedStickerPacksController(account: Account) -> ViewController { var previousPackCount: Int? - let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, installedStickerPacks.get() |> deliverOnMainQueue) - |> map { state, packs, installedView -> (ItemListControllerState, (ItemListNodeState, ArchivedStickerPacksEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, installedStickerPacks.get() |> deliverOnMainQueue) + |> deliverOnMainQueue + |> map { presentationData, state, packs, installedView -> (ItemListControllerState, (ItemListNodeState, ArchivedStickerPacksEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? if let packs = packs, packs.count != 0 { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { updateState { $0.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { updateState { $0.withUpdatedEditing(true) } @@ -313,16 +325,15 @@ public func archivedStickerPacksController(account: Account) -> ViewController { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() } - let controllerState = ItemListControllerState(title: .text("Archived Stickers"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.StickerPacksSettings_ArchivedPacks), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: archivedStickerPacksControllerEntries(state: state, packs: packs, installedView: installedView), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && packs != nil && (previous! != 0 && previous! >= packs!.count - 10)) + let listState = ItemListNodeState(entries: archivedStickerPacksControllerEntries(presentationData: presentationData, state: state, packs: packs, installedView: installedView), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && packs != nil && (previous! != 0 && previous! >= packs!.count - 10)) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/AuthorizationSequenceCodeEntryController.swift b/TelegramUI/AuthorizationSequenceCodeEntryController.swift index bb45666d03..041f80d31c 100644 --- a/TelegramUI/AuthorizationSequenceCodeEntryController.swift +++ b/TelegramUI/AuthorizationSequenceCodeEntryController.swift @@ -27,12 +27,8 @@ final class AuthorizationSequenceCodeEntryController: ViewController { } } - override init(navigationBar: NavigationBar = NavigationBar()) { - super.init(navigationBar: navigationBar) - - self.navigationBar.backgroundColor = nil - self.navigationBar.isOpaque = false - self.navigationBar.stripeColor = UIColor.clear + init() { + super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme) self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) } diff --git a/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift b/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift index 8bdd841bc0..5cffd5d1b3 100644 --- a/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceCodeEntryControllerNode.swift @@ -41,7 +41,7 @@ func authorizationNextOptionText(_ type: AuthorizationCodeNextType?, timeout: In } } } else { - return NSAttributedString(string: "Haven't received the code?", font: Font.regular(16.0), textColor: UIColor(0x007ee5), paragraphAlignment: .center) + return NSAttributedString(string: "Haven't received the code?", font: Font.regular(16.0), textColor: UIColor(rgb: 0x007ee5), paragraphAlignment: .center) } } @@ -84,11 +84,11 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF override init() { self.navigationBackgroundNode = ASDisplayNode() self.navigationBackgroundNode.isLayerBacked = true - self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef) + self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef) self.stripeNode = ASDisplayNode() self.stripeNode.isLayerBacked = true - self.stripeNode.backgroundColor = UIColor(0xbcbbc1) + self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1) self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true @@ -105,7 +105,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.codeSeparatorNode = ASDisplayNode() self.codeSeparatorNode.isLayerBacked = true - self.codeSeparatorNode.backgroundColor = UIColor(0xbcbbc1) + self.codeSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1) self.codeField = TextFieldNode() self.codeField.textField.font = Font.regular(24.0) @@ -129,7 +129,7 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF self.codeField.textField.addTarget(self, action: #selector(self.codeFieldTextChanged(_:)), for: .editingChanged) - self.codeField.textField.attributedPlaceholder = NSAttributedString(string: "Code", font: Font.regular(24.0), textColor: UIColor(0xbcbcc3)) + self.codeField.textField.attributedPlaceholder = NSAttributedString(string: "Code", font: Font.regular(24.0), textColor: UIColor(rgb: 0xbcbcc3)) } deinit { diff --git a/TelegramUI/AuthorizationSequenceController.swift b/TelegramUI/AuthorizationSequenceController.swift index 96e19dccfa..3778808560 100644 --- a/TelegramUI/AuthorizationSequenceController.swift +++ b/TelegramUI/AuthorizationSequenceController.swift @@ -7,6 +7,8 @@ import SwiftSignalKit import MtProtoKitDynamic public final class AuthorizationSequenceController: NavigationController { + static let navigationBarTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007ee5), primaryTextColor: .black, backgroundColor: .clear, separatorColor: .clear) + private var account: UnauthorizedAccount private var stateDisposable: Disposable? diff --git a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift index 80ccfebfd2..1682e0fa80 100644 --- a/TelegramUI/AuthorizationSequenceCountrySelectionController.swift +++ b/TelegramUI/AuthorizationSequenceCountrySelectionController.swift @@ -440,13 +440,11 @@ final class AuthorizationSequenceCountrySelectionController: ViewController { var completeWithCountryCode: ((Int) -> Void)? - override init(navigationBar: NavigationBar = NavigationBar()) { + init() { self.innerController = InnerCountrySelectionController() self.innerNavigationController = UINavigationController(rootViewController: self.innerController) - super.init(navigationBar: navigationBar) - - self.navigationBar.isHidden = true + super.init(navigationBarTheme: nil) self.innerController.dismiss = { [weak self] in self?.cancelPressed() diff --git a/TelegramUI/AuthorizationSequencePasswordEntryController.swift b/TelegramUI/AuthorizationSequencePasswordEntryController.swift index 9eeb25a646..303ee407f5 100644 --- a/TelegramUI/AuthorizationSequencePasswordEntryController.swift +++ b/TelegramUI/AuthorizationSequencePasswordEntryController.swift @@ -24,12 +24,8 @@ final class AuthorizationSequencePasswordEntryController: ViewController { } } - override init(navigationBar: NavigationBar = NavigationBar()) { - super.init(navigationBar: navigationBar) - - self.navigationBar.backgroundColor = nil - self.navigationBar.isOpaque = false - self.navigationBar.stripeColor = UIColor.clear + init() { + super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme) self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) } diff --git a/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift b/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift index e987c389fb..b46e8d3464 100644 --- a/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequencePasswordEntryControllerNode.swift @@ -30,11 +30,11 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT override init() { self.navigationBackgroundNode = ASDisplayNode() self.navigationBackgroundNode.isLayerBacked = true - self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef) + self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef) self.stripeNode = ASDisplayNode() self.stripeNode.isLayerBacked = true - self.stripeNode.backgroundColor = UIColor(0xbcbbc1) + self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1) self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true @@ -49,11 +49,11 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT self.nextOptionNode = ASTextNode() self.nextOptionNode.isLayerBacked = true self.nextOptionNode.displaysAsynchronously = false - self.nextOptionNode.attributedText = NSAttributedString(string: "Forgot password?", font: Font.regular(16.0), textColor: UIColor(0x007ee5), paragraphAlignment: .center) + self.nextOptionNode.attributedText = NSAttributedString(string: "Forgot password?", font: Font.regular(16.0), textColor: UIColor(rgb: 0x007ee5), paragraphAlignment: .center) self.codeSeparatorNode = ASDisplayNode() self.codeSeparatorNode.isLayerBacked = true - self.codeSeparatorNode.backgroundColor = UIColor(0xbcbbc1) + self.codeSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1) self.codeField = TextFieldNode() self.codeField.textField.font = Font.regular(20.0) @@ -79,7 +79,7 @@ final class AuthorizationSequencePasswordEntryControllerNode: ASDisplayNode, UIT } func updateData(hint: String) { - self.codeField.textField.attributedPlaceholder = NSAttributedString(string: hint, font: Font.regular(20.0), textColor: UIColor(0xbcbcc3)) + self.codeField.textField.attributedPlaceholder = NSAttributedString(string: hint, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3)) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { diff --git a/TelegramUI/AuthorizationSequencePhoneEntryController.swift b/TelegramUI/AuthorizationSequencePhoneEntryController.swift index 7a3d6bbc59..e328a696f8 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryController.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryController.swift @@ -24,12 +24,8 @@ final class AuthorizationSequencePhoneEntryController: ViewController { private let hapticFeedback = HapticFeedback() - override init(navigationBar: NavigationBar = NavigationBar()) { - super.init(navigationBar: navigationBar) - - self.navigationBar.backgroundColor = nil - self.navigationBar.isOpaque = false - self.navigationBar.stripeColor = UIColor.clear + init() { + super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme) self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) } diff --git a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift index fd02c4be8a..ca3edf8fc9 100644 --- a/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/TelegramUI/AuthorizationSequencePhoneEntryControllerNode.swift @@ -241,7 +241,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 61.0, height: let arrowSize: CGFloat = 10.0 let lineWidth = UIScreenPixel context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0xbcbbc1).cgColor) + context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor) context.setLineWidth(lineWidth) context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0)) context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize - lineWidth / 2.0)) @@ -254,7 +254,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 61.0, height: private let countryButtonHighlightedBackground = generateImage(CGSize(width: 60.0, height: 67.0), rotatedContext: { size, context in let arrowSize: CGFloat = 10.0 context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0xbcbbc1).cgColor) + context.setFillColor(UIColor(rgb: 0xbcbbc1).cgColor) context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize))) context.move(to: CGPoint(x: size.width, y: size.height - arrowSize)) context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize)) @@ -268,7 +268,7 @@ private let phoneInputBackground = generateImage(CGSize(width: 85.0, height: 57. let arrowSize: CGFloat = 10.0 let lineWidth = UIScreenPixel context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0xbcbbc1).cgColor) + context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor) context.setLineWidth(lineWidth) context.move(to: CGPoint(x: 15.0, y: size.height - lineWidth / 2.0)) context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0)) @@ -313,11 +313,11 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { override init() { self.navigationBackgroundNode = ASDisplayNode() self.navigationBackgroundNode.isLayerBacked = true - self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef) + self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef) self.stripeNode = ASDisplayNode() self.stripeNode.isLayerBacked = true - self.stripeNode.backgroundColor = UIColor(0xbcbbc1) + self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1) self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true @@ -327,14 +327,14 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.noticeNode = ASTextNode() self.noticeNode.isLayerBacked = true self.noticeNode.displaysAsynchronously = false - self.noticeNode.attributedText = NSAttributedString(string: "Please confirm your country code and enter your phone number.", font: Font.regular(16.0), textColor: UIColor(0x878787), paragraphAlignment: .center) + self.noticeNode.attributedText = NSAttributedString(string: "Please confirm your country code and enter your phone number.", font: Font.regular(16.0), textColor: UIColor(rgb: 0x878787), paragraphAlignment: .center) self.termsOfServiceNode = ASTextNode() self.termsOfServiceNode.isLayerBacked = true self.termsOfServiceNode.displaysAsynchronously = false let termsString = NSMutableAttributedString() termsString.append(NSAttributedString(string: "By signing up,\nyou agree to the ", font: Font.regular(16.0), textColor: UIColor.black)) - termsString.append(NSAttributedString(string: "Terms of Service", font: Font.regular(16.0), textColor: UIColor(0x007ee5))) + termsString.append(NSAttributedString(string: "Terms of Service", font: Font.regular(16.0), textColor: UIColor(rgb: 0x007ee5))) termsString.append(NSAttributedString(string: ".", font: Font.regular(16.0), textColor: UIColor.black)) let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .center @@ -371,7 +371,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 10.0, right: 0.0) self.countryButton.contentHorizontalAlignment = .left - self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: "Your phone number", font: Font.regular(20.0), textColor: UIColor(0xbcbcc3)) + self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: "Your phone number", font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3)) self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside) diff --git a/TelegramUI/AuthorizationSequenceSignUpController.swift b/TelegramUI/AuthorizationSequenceSignUpController.swift index c707f9875b..2cf3a48c69 100644 --- a/TelegramUI/AuthorizationSequenceSignUpController.swift +++ b/TelegramUI/AuthorizationSequenceSignUpController.swift @@ -24,12 +24,8 @@ final class AuthorizationSequenceSignUpController: ViewController { } } - override init(navigationBar: NavigationBar = NavigationBar()) { - super.init(navigationBar: navigationBar) - - self.navigationBar.backgroundColor = nil - self.navigationBar.isOpaque = false - self.navigationBar.stripeColor = UIColor.clear + init() { + super.init(navigationBarTheme: AuthorizationSequenceController.navigationBarTheme) self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) } diff --git a/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift b/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift index 43614bf133..1faf50ddc9 100644 --- a/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceSignUpControllerNode.swift @@ -33,11 +33,11 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel override init() { self.navigationBackgroundNode = ASDisplayNode() self.navigationBackgroundNode.isLayerBacked = true - self.navigationBackgroundNode.backgroundColor = UIColor(0xefefef) + self.navigationBackgroundNode.backgroundColor = UIColor(rgb: 0xefefef) self.stripeNode = ASDisplayNode() self.stripeNode.isLayerBacked = true - self.stripeNode.backgroundColor = UIColor(0xbcbbc1) + self.stripeNode.backgroundColor = UIColor(rgb: 0xbcbbc1) self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true @@ -47,31 +47,31 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel self.currentOptionNode = ASTextNode() self.currentOptionNode.isLayerBacked = true self.currentOptionNode.displaysAsynchronously = false - self.currentOptionNode.attributedText = NSAttributedString(string: "Enter your name and add a profile picture", font: Font.regular(16.0), textColor: UIColor(0x878787), paragraphAlignment: .center) + self.currentOptionNode.attributedText = NSAttributedString(string: "Enter your name and add a profile picture", font: Font.regular(16.0), textColor: UIColor(rgb: 0x878787), paragraphAlignment: .center) self.firstSeparatorNode = ASDisplayNode() self.firstSeparatorNode.isLayerBacked = true - self.firstSeparatorNode.backgroundColor = UIColor(0xbcbbc1) + self.firstSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1) self.lastSeparatorNode = ASDisplayNode() self.lastSeparatorNode.isLayerBacked = true - self.lastSeparatorNode.backgroundColor = UIColor(0xbcbbc1) + self.lastSeparatorNode.backgroundColor = UIColor(rgb: 0xbcbbc1) self.firstNameField = TextFieldNode() self.firstNameField.textField.font = Font.regular(20.0) self.firstNameField.textField.textAlignment = .natural self.firstNameField.textField.returnKeyType = .next - self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: "First name", font: self.firstNameField.textField.font, textColor: UIColor(0xbcbcc3)) + self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: "First name", font: self.firstNameField.textField.font, textColor: UIColor(rgb: 0xbcbcc3)) self.lastNameField = TextFieldNode() self.lastNameField.textField.font = Font.regular(20.0) self.lastNameField.textField.textAlignment = .natural self.lastNameField.textField.returnKeyType = .done - self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: "Last name", font: self.lastNameField.textField.font, textColor: UIColor(0xbcbcc3)) + self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: "Last name", font: self.lastNameField.textField.font, textColor: UIColor(rgb: 0xbcbcc3)) self.addPhotoButton = HighlightableButtonNode() - self.addPhotoButton.setAttributedTitle(NSAttributedString(string: "add\nphoto", font: Font.regular(16.0), textColor: UIColor(0xbcbcc3), paragraphAlignment: .center), for: .normal) - self.addPhotoButton.setBackgroundImage(generateCircleImage(diameter: 110.0, lineWidth: 1.0, color: UIColor(0xbcbcc3)), for: .normal) + self.addPhotoButton.setAttributedTitle(NSAttributedString(string: "add\nphoto", font: Font.regular(16.0), textColor: UIColor(rgb: 0xbcbcc3), paragraphAlignment: .center), for: .normal) + self.addPhotoButton.setBackgroundImage(generateCircleImage(diameter: 110.0, lineWidth: 1.0, color: UIColor(rgb: 0xbcbcc3)), for: .normal) super.init(viewBlock: { return UITracingLayerView() @@ -96,8 +96,8 @@ final class AuthorizationSequenceSignUpControllerNode: ASDisplayNode, UITextFiel } func updateData(firstName: String, lastName: String) { - self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: firstName, font: Font.regular(20.0), textColor: UIColor(0xbcbcc3)) - self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: lastName, font: Font.regular(20.0), textColor: UIColor(0xbcbcc3)) + self.firstNameField.textField.attributedPlaceholder = NSAttributedString(string: firstName, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3)) + self.lastNameField.textField.attributedPlaceholder = NSAttributedString(string: lastName, font: Font.regular(20.0), textColor: UIColor(rgb: 0xbcbcc3)) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { diff --git a/TelegramUI/AuthorizationSequenceSplashController.swift b/TelegramUI/AuthorizationSequenceSplashController.swift index 83103814e4..f1dd146875 100644 --- a/TelegramUI/AuthorizationSequenceSplashController.swift +++ b/TelegramUI/AuthorizationSequenceSplashController.swift @@ -13,10 +13,9 @@ final class AuthorizationSequenceSplashController: ViewController { var nextPressed: (() -> Void)? - override init(navigationBar: NavigationBar = NavigationBar()) { - super.init(navigationBar: navigationBar) + init() { + super.init(navigationBarTheme: nil) - self.navigationBar.isHidden = true self.controller.startMessaging = { [weak self] in self?.nextPressed?() } diff --git a/TelegramUI/AutomaticMediaDownloadSettings.swift b/TelegramUI/AutomaticMediaDownloadSettings.swift index 7c8c56c1ec..d1bd443cd6 100644 --- a/TelegramUI/AutomaticMediaDownloadSettings.swift +++ b/TelegramUI/AutomaticMediaDownloadSettings.swift @@ -12,8 +12,8 @@ public struct AutomaticMediaDownloadCategoryPeers: Coding, Equatable { } public init(decoder: Decoder) { - self.privateChats = (decoder.decodeInt32ForKey("p") as Int32) != 0 - self.groupsAndChannels = (decoder.decodeInt32ForKey("g") as Int32) != 0 + self.privateChats = decoder.decodeInt32ForKey("p", orElse: 0) != 0 + self.groupsAndChannels = decoder.decodeInt32ForKey("g", orElse: 0) != 0 } public func encode(_ encoder: Encoder) { @@ -115,7 +115,7 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable { public init(decoder: Decoder) { self.categories = decoder.decodeObjectForKey("c", decoder: { AutomaticMediaDownloadCategories(decoder: $0) }) as! AutomaticMediaDownloadCategories - self.saveIncomingPhotos = (decoder.decodeInt32ForKey("siph") as Int32) != 0 + self.saveIncomingPhotos = decoder.decodeInt32ForKey("siph", orElse: 0) != 0 } public func encode(_ encoder: Encoder) { diff --git a/TelegramUI/AvatarGalleryController.swift b/TelegramUI/AvatarGalleryController.swift index 08fcdc3c23..366b4652f9 100644 --- a/TelegramUI/AvatarGalleryController.swift +++ b/TelegramUI/AvatarGalleryController.swift @@ -80,12 +80,7 @@ class AvatarGalleryController: ViewController { self.account = account self.replaceRootController = replaceRootController - super.init() - - self.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.6) - self.navigationBar.stripeColor = UIColor.clear - self.navigationBar.foregroundColor = UIColor.white - self.navigationBar.accentColor = UIColor.white + super.init(navigationBarTheme: GalleryController.darkNavigationTheme) self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.donePressed)) @@ -147,29 +142,6 @@ class AvatarGalleryController: ViewController { $0.withUpdatedFooterContentNode(footerContentNode) }, transition: .immediate) })) - - self.centralItemAttributesDisposable.add(self.centralItemNavigationStyle.get().start(next: { [weak self] style in - if let strongSelf = self { - switch style { - case .dark: - strongSelf.statusBar.statusBarStyle = .White - strongSelf.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - strongSelf.navigationBar.stripeColor = UIColor.clear - strongSelf.navigationBar.foregroundColor = UIColor.white - strongSelf.navigationBar.accentColor = UIColor.white - strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor.black - strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = true - case .light: - strongSelf.statusBar.statusBarStyle = .Black - strongSelf.navigationBar.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0) - strongSelf.navigationBar.foregroundColor = UIColor.black - strongSelf.navigationBar.accentColor = UIColor(0x007ee5) - strongSelf.navigationBar.stripeColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) - strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor(0xbdbdc2) - strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = false - } - } - })) } required init(coder aDecoder: NSCoder) { diff --git a/TelegramUI/AvatarNode.swift b/TelegramUI/AvatarNode.swift index 57f460fd25..717fbefdcf 100644 --- a/TelegramUI/AvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -23,16 +23,16 @@ private class AvatarNodeParameters: NSObject { } private let gradientColors: [NSArray] = [ - [UIColor(0xff516a).cgColor, UIColor(0xff885e).cgColor], - [UIColor(0xffa85c).cgColor, UIColor(0xffcd6a).cgColor], - [UIColor(0x54cb68).cgColor, UIColor(0xa0de7e).cgColor], - [UIColor(0x2a9ef1).cgColor, UIColor(0x72d5fd).cgColor], - [UIColor(0x665fff).cgColor, UIColor(0x82b1ff).cgColor], - [UIColor(0xd669ed).cgColor, UIColor(0xe0a2f3).cgColor] + [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], + [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], + [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], + [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], + [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], + [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor] ] private let grayscaleColors: NSArray = [ - UIColor(0xefefef).cgColor, UIColor(0xeeeeee).cgColor + UIColor(rgb: 0xefefef).cgColor, UIColor(rgb: 0xeeeeee).cgColor ] private enum AvatarNodeState: Equatable { diff --git a/TelegramUI/BlockedPeersController.swift b/TelegramUI/BlockedPeersController.swift index 3b906fa041..949995c4d0 100644 --- a/TelegramUI/BlockedPeersController.swift +++ b/TelegramUI/BlockedPeersController.swift @@ -44,7 +44,7 @@ private enum BlockedPeersEntryStableId: Hashable { } private enum BlockedPeersEntry: ItemListNodeEntry { - case peerItem(Int32, Peer, ItemListPeerItemEditing, Bool) + case peerItem(Int32, PresentationTheme, PresentationStrings, Peer, ItemListPeerItemEditing, Bool) var section: ItemListSectionId { switch self { @@ -55,18 +55,24 @@ private enum BlockedPeersEntry: ItemListNodeEntry { var stableId: BlockedPeersEntryStableId { switch self { - case let .peerItem(_, peer, _, _): + case let .peerItem(_, _, _, peer, _, _): return .peer(peer.id) } } static func ==(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool { switch lhs { - case let .peerItem(lhsIndex, lhsPeer, lhsEditing, lhsEnabled): - if case let .peerItem(rhsIndex, rhsPeer, rhsEditing, rhsEnabled) = rhs { + case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsEditing, lhsEnabled): + if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsEditing, rhsEnabled) = rhs { if lhsIndex != rhsIndex { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if !lhsPeer.isEqual(rhsPeer) { return false } @@ -85,9 +91,9 @@ private enum BlockedPeersEntry: ItemListNodeEntry { static func <(lhs: BlockedPeersEntry, rhs: BlockedPeersEntry) -> Bool { switch lhs { - case let .peerItem(index, _, _, _): + case let .peerItem(index, _, _, _, _, _): switch rhs { - case let .peerItem(rhsIndex, _, _, _): + case let .peerItem(rhsIndex, _, _, _, _, _): return index < rhsIndex } } @@ -95,8 +101,8 @@ private enum BlockedPeersEntry: ItemListNodeEntry { func item(_ arguments: BlockedPeersControllerArguments) -> ListViewItem { switch self { - case let .peerItem(_, peer, editing, enabled): - return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + case let .peerItem(_, theme, strings, peer, editing, enabled): + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removePeer(peerId) @@ -149,13 +155,13 @@ private struct BlockedPeersControllerState: Equatable { } } -private func blockedPeersControllerEntries(state: BlockedPeersControllerState, peers: [Peer]?) -> [BlockedPeersEntry] { +private func blockedPeersControllerEntries(presentationData: PresentationData, state: BlockedPeersControllerState, peers: [Peer]?) -> [BlockedPeersEntry] { var entries: [BlockedPeersEntry] = [] if let peers = peers { var index: Int32 = 0 for peer in peers { - entries.append(.peerItem(index, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != peer.id)) + entries.append(.peerItem(index, presentationData.theme, presentationData.strings, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != peer.id)) index += 1 } } @@ -229,9 +235,9 @@ public func blockedPeersController(account: Account) -> ViewController { var previousPeers: [Peer]? - let signal = combineLatest(statePromise.get(), peersPromise.get()) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peersPromise.get()) |> deliverOnMainQueue - |> map { state, peers -> (ItemListControllerState, (ItemListNodeState, BlockedPeersEntry.ItemGenerationArguments)) in + |> map { presentationData, state, peers -> (ItemListControllerState, (ItemListNodeState, BlockedPeersEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? if let peers = peers, !peers.isEmpty { if state.editing { @@ -261,15 +267,15 @@ public func blockedPeersController(account: Account) -> ViewController { let previous = previousPeers previousPeers = peers - let controllerState = ItemListControllerState(title: .text("Blocked Users"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) - let listState = ItemListNodeState(entries: blockedPeersControllerEntries(state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Blocked Users"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: true) + let listState = ItemListNodeState(entries: blockedPeersControllerEntries(presentationData: presentationData, state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/CallController.swift b/TelegramUI/CallController.swift new file mode 100644 index 0000000000..dc3e28d848 --- /dev/null +++ b/TelegramUI/CallController.swift @@ -0,0 +1,160 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +public final class CallController: ViewController { + private var controllerNode: CallControllerNode { + return self.displayNode as! CallControllerNode + } + + private let _ready = Promise(false) + override public var ready: Promise { + return self._ready + } + + private let account: Account + public let call: PresentationCall + + private var presentationData: PresentationData + private var animatedAppearance = false + + private var peer: Peer? + + private var peerDisposable: Disposable? + private var disposable: Disposable? + + private var callMutedDisposable: Disposable? + private var isMuted = false + + private var speakerModeDisposable: Disposable? + private var speakerMode = false + + public init(account: Account, call: PresentationCall) { + self.account = account + self.call = call + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: nil) + + self.statusBar.statusBarStyle = .White + self.statusBar.ignoreInCall = true + + self.supportedOrientations = .portrait + + self.disposable = (call.state |> deliverOnMainQueue).start(next: { [weak self] callState in + self?.callStateUpdated(callState) + }) + + self.callMutedDisposable = (call.isMuted |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + strongSelf.isMuted = value + if strongSelf.isNodeLoaded { + strongSelf.controllerNode.isMuted = value + } + } + }) + + self.speakerModeDisposable = (call.speakerMode |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + strongSelf.speakerMode = value + if strongSelf.isNodeLoaded { + strongSelf.controllerNode.speakerMode = value + } + } + }) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.peerDisposable?.dispose() + self.disposable?.dispose() + self.callMutedDisposable?.dispose() + self.speakerModeDisposable?.dispose() + } + + private func callStateUpdated(_ callState: PresentationCallState) { + if self.isNodeLoaded { + self.controllerNode.updateCallState(callState) + } + } + + override public func loadDisplayNode() { + self.displayNode = CallControllerNode(account: self.account, presentationData: self.presentationData, statusBar: self.statusBar) + self.displayNodeDidLoad() + + self.controllerNode.toggleMute = { [weak self] in + self?.call.toggleIsMuted() + } + + self.controllerNode.toggleSpeaker = { [weak self] in + self?.call.toggleSpeaker() + } + + self.controllerNode.acceptCall = { [weak self] in + let _ = self?.call.answer() + } + + self.controllerNode.endCall = { [weak self] in + let _ = self?.call.hangUp() + } + + self.controllerNode.back = { [weak self] in + let _ = self?.dismiss() + } + + self.controllerNode.disissedInteractively = { [weak self] in + self?.animatedAppearance = false + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + + self.peerDisposable = (account.postbox.peerView(id: self.call.peerId) + |> deliverOnMainQueue).start(next: { [weak self] view in + if let strongSelf = self { + if let peer = view.peers[view.peerId] { + strongSelf.peer = peer + strongSelf.controllerNode.updatePeer(peer: peer) + strongSelf._ready.set(.single(true)) + } + } + }) + + self.controllerNode.isMuted = self.isMuted + self.controllerNode.speakerMode = self.speakerMode + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.animatedAppearance { + self.animatedAppearance = true + + self.controllerNode.animateIn() + } + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + override open func dismiss(completion: (() -> Void)? = nil) { + self.controllerNode.animateOut(completion: { [weak self] in + self?.animatedAppearance = false + self?.presentingViewController?.dismiss(animated: false, completion: nil) + + completion?() + }) + } + + @objc func backPressed() { + self.dismiss() + } +} diff --git a/TelegramUI/CallControllerButton.swift b/TelegramUI/CallControllerButton.swift new file mode 100644 index 0000000000..ead09af6d6 --- /dev/null +++ b/TelegramUI/CallControllerButton.swift @@ -0,0 +1,195 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +enum CallControllerButtonType { + case mute + case end + case accept + case speaker + case bluetooth +} + +private let buttonSize = CGSize(width: 75.0, height: 75.0) + +private func generateEmptyButtonImage(icon: UIImage?, strokeColor: UIColor?, fillColor: UIColor, knockout: Bool = false, angle: CGFloat = 0.0) -> UIImage? { + return generateImage(buttonSize, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + if let strokeColor = strokeColor { + context.setFillColor(strokeColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setFillColor(fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 1.5, y: 1.5), size: CGSize(width: size.width - 3.0, height: size.height - 3.0))) + } else { + context.setFillColor(fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) + } + + if let icon = icon { + if !angle.isZero { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.rotate(by: angle) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + let imageSize = icon.size + let imageRect = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.width - imageSize.height) / 2.0)), size: imageSize) + if knockout { + context.setBlendMode(.copy) + context.clip(to: imageRect, mask: icon.cgImage!) + context.setFillColor(UIColor.clear.cgColor) + context.fill(imageRect) + } else { + context.setBlendMode(.normal) + context.draw(icon.cgImage!, in: imageRect) + } + } + }) +} + +private func generateFilledButtonImage(color: UIColor, icon: UIImage?, angle: CGFloat = 0.0) -> UIImage? { + return generateImage(buttonSize, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.normal) + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + if let icon = icon { + if !angle.isZero { + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.rotate(by: angle) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + } + context.draw(icon.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - icon.size.width) / 2.0), y: floor((size.height - icon.size.height) / 2.0)), size: icon.size)) + } + }) +} + +private let emptyStroke = UIColor(white: 1.0, alpha: 0.8) +private let emptyHighlightedFill = UIColor(white: 1.0, alpha: 0.3) +private let invertedFill = UIColor(white: 1.0, alpha: 1.0) + +private let labelFont = Font.regular(14.5) + +final class CallControllerButtonNode: HighlightTrackingButtonNode { + private let regularImage: UIImage? + private let highlightedImage: UIImage? + private let filledImage: UIImage? + + private let backgroundNode: ASImageNode + private let labelNode: ASTextNode? + + init(type: CallControllerButtonType, label: String?) { + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displayWithoutProcessing = false + self.backgroundNode.displaysAsynchronously = false + + if let label = label { + let labelNode = ASTextNode() + labelNode.attributedText = NSAttributedString(string: label, font: labelFont, textColor: .white) + self.labelNode = labelNode + } else { + self.labelNode = nil + } + + var regularImage: UIImage? + var highlightedImage: UIImage? + var filledImage: UIImage? + + switch type { + case .mute: + regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallMuteButton"), strokeColor: emptyStroke, fillColor: .clear) + highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallMuteButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill) + filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallMuteButton"), strokeColor: nil, fillColor: invertedFill, knockout: true) + case .accept: + regularImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/CallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0) + highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0x74db58), icon: UIImage(bundleImageName: "Call/CallPhoneButton"), angle: CGFloat.pi * 3.0 / 4.0) + case .end: + regularImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/CallPhoneButton")) + highlightedImage = generateFilledButtonImage(color: UIColor(rgb: 0xd92326), icon: UIImage(bundleImageName: "Call/CallPhoneButton")) + case .speaker: + regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallSpeakerButton"), strokeColor: emptyStroke, fillColor: .clear) + highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallSpeakerButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill) + filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallSpeakerButton"), strokeColor: nil, fillColor: invertedFill, knockout: true) + case .bluetooth: + regularImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: .clear) + highlightedImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: emptyStroke, fillColor: emptyHighlightedFill) + filledImage = generateEmptyButtonImage(icon: UIImage(bundleImageName: "Call/CallBluetoothButton"), strokeColor: nil, fillColor: invertedFill, knockout: true) + } + + self.regularImage = regularImage + self.highlightedImage = highlightedImage + self.filledImage = filledImage + + super.init() + + self.addSubnode(self.backgroundNode) + + if let labelNode = self.labelNode { + self.addSubnode(labelNode) + } + + self.backgroundNode.image = regularImage + self.currentImage = regularImage + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + strongSelf.internalHighlighted = highlighted + strongSelf.updateState(highlighted: highlighted, selected: strongSelf.isSelected) + } + } + } + + private var internalHighlighted = false + + override var isSelected: Bool { + didSet { + self.updateState(highlighted: self.internalHighlighted, selected: self.isSelected) + } + } + + private var currentImage: UIImage? + + private func updateState(highlighted: Bool, selected: Bool) { + let image: UIImage? + if selected { + image = self.filledImage + } else if highlighted { + image = self.highlightedImage + } else { + image = self.regularImage + } + + if self.currentImage !== image { + let currentContents = self.backgroundNode.layer.contents + self.backgroundNode.layer.removeAnimation(forKey: "contents") + if let currentContents = currentContents, let image = image { + self.backgroundNode.image = image + self.backgroundNode.layer.animate(from: currentContents as AnyObject, to: image.cgImage!, keyPath: "contents", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: image === self.currentImage || image === self.filledImage ? 0.25 : 0.15) + } else { + self.backgroundNode.image = image + } + self.currentImage = image + } + } + + func animateRollTransition() { + self.backgroundNode.layer.animate(from: 0.0 as NSNumber, to: (-CGFloat.pi * 5 / 4) as NSNumber, keyPath: "transform.rotation.z", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3, removeOnCompletion: false) + self.labelNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width)) + + if let labelNode = self.labelNode { + let labelSize = labelNode.measure(CGSize(width: 200.0, height: 100.0)) + labelNode.frame = CGRect(origin: CGPoint(x: floor((size.width - labelSize.width) / 2.0), y: 81.0), size: labelSize) + } + } +} diff --git a/TelegramUI/CallControllerButtonsNode.swift b/TelegramUI/CallControllerButtonsNode.swift new file mode 100644 index 0000000000..13316ff544 --- /dev/null +++ b/TelegramUI/CallControllerButtonsNode.swift @@ -0,0 +1,196 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +enum CallControllerButtonsSpeakerMode { + case bluetooth + case speaker +} + +enum CallControllerButtonsMode: Equatable { + case active(CallControllerButtonsSpeakerMode) + case incoming + + static func ==(lhs: CallControllerButtonsMode, rhs: CallControllerButtonsMode) -> Bool { + switch lhs { + case let .active(mode): + if case .active(mode) = rhs { + return true + } else { + return false + } + case .incoming: + if case .incoming = rhs { + return true + } else { + return false + } + } + } +} + +final class CallControllerButtonsNode: ASDisplayNode { + private let acceptButton: CallControllerButtonNode + private let declineButton: CallControllerButtonNode + + private let muteButton: CallControllerButtonNode + private let endButton: CallControllerButtonNode + private let speakerButton: CallControllerButtonNode + + private var mode: CallControllerButtonsMode? + + private var validLayout: CGFloat? + + var isMuted = false { + didSet { + self.muteButton.isSelected = self.isMuted + } + } + + var speakerMode = false { + didSet { + self.speakerButton.isSelected = self.speakerMode + } + } + + var accept: (() -> Void)? + var mute: (() -> Void)? + var end: (() -> Void)? + var speaker: (() -> Void)? + + init(strings: PresentationStrings) { + self.acceptButton = CallControllerButtonNode(type: .accept, label: strings.Call_Accept) + self.acceptButton.alpha = 0.0 + self.declineButton = CallControllerButtonNode(type: .end, label: strings.Call_Decline) + self.declineButton.alpha = 0.0 + + self.muteButton = CallControllerButtonNode(type: .mute, label: nil) + self.muteButton.alpha = 0.0 + self.endButton = CallControllerButtonNode(type: .end, label: nil) + self.endButton.alpha = 0.0 + self.speakerButton = CallControllerButtonNode(type: .speaker, label: nil) + self.speakerButton.alpha = 0.0 + + super.init() + + self.addSubnode(self.acceptButton) + self.addSubnode(self.declineButton) + self.addSubnode(self.muteButton) + self.addSubnode(self.endButton) + self.addSubnode(self.speakerButton) + + self.acceptButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) + self.declineButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) + self.muteButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) + self.endButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) + self.speakerButton.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: .touchUpInside) + } + + func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) { + let previousLayout = self.validLayout + self.validLayout = constrainedWidth + + if let mode = self.mode, previousLayout != self.validLayout { + self.updateButtonsLayout(mode: mode, width: constrainedWidth, animated: false) + } + } + + func updateMode(_ mode: CallControllerButtonsMode) { + if self.mode != mode { + let previousMode = self.mode + self.mode = mode + if let validLayout = self.validLayout { + self.updateButtonsLayout(mode: mode, width: validLayout, animated: previousMode != nil) + } + } + } + + private func updateButtonsLayout(mode: CallControllerButtonsMode, width: CGFloat, animated: Bool) { + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .spring) + } else { + transition = .immediate + } + + let threeButtonSpacing: CGFloat = 28.0 + let twoButtonSpacing: CGFloat = 105.0 + let buttonSize = CGSize(width: 75.0, height: 75.0) + + let threeButtonsWidth = 3.0 * buttonSize.width + max(0.0, 3.0 - 1.0) * threeButtonSpacing + let twoButtonsWidth = 2.0 * buttonSize.width + max(0.0, 2.0 - 1.0) * twoButtonSpacing + + var origin = CGPoint(x: floor((width - threeButtonsWidth) / 2.0), y: 0.0) + for button in [self.muteButton, self.endButton, self.speakerButton] { + transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize)) + origin.x += buttonSize.width + threeButtonSpacing + } + + origin = CGPoint(x: floor((width - twoButtonsWidth) / 2.0), y: 0.0) + for button in [self.declineButton, self.acceptButton] { + transition.updateFrame(node: button, frame: CGRect(origin: origin, size: buttonSize)) + origin.x += buttonSize.width + twoButtonSpacing + } + + switch mode { + case .incoming: + for button in [self.declineButton, self.acceptButton] { + button.alpha = 1.0 + } + for button in [self.muteButton, self.endButton, self.speakerButton] { + button.alpha = 0.0 + } + case .active: + for button in [self.muteButton, self.speakerButton] { + if animated && button.alpha.isZero { + button.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + } + button.alpha = 1.0 + } + var animatingAcceptButton = false + if self.endButton.alpha.isZero { + if animated { + if !self.acceptButton.alpha.isZero { + animatingAcceptButton = true + self.endButton.layer.animatePosition(from: self.acceptButton.position, to: self.endButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.acceptButton.animateRollTransition() + self.endButton.layer.animate(from: (CGFloat.pi * 5 / 4) as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.rotation.z", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3) + self.acceptButton.layer.animatePosition(from: self.acceptButton.position, to: self.endButton.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.acceptButton.alpha = 0.0 + strongSelf.acceptButton.layer.removeAnimation(forKey: "position") + strongSelf.acceptButton.layer.removeAnimation(forKey: "transform.rotation.z") + } + }) + } + self.endButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + self.endButton.alpha = 1.0 + } + + if !self.declineButton.alpha.isZero { + if animated { + self.declineButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + self.declineButton.alpha = 0.0 + } + + if self.acceptButton.alpha.isZero && !animatingAcceptButton { + self.acceptButton.alpha = 0.0 + } + } + } + + @objc func buttonPressed(_ button: CallControllerButtonNode) { + if button === self.muteButton { + self.mute?() + } else if button === self.endButton || button === self.declineButton { + self.end?() + } else if button === self.speakerButton { + self.speaker?() + } else if button === self.acceptButton { + self.accept?() + } + } +} diff --git a/TelegramUI/CallControllerKeyPreviewNode.swift b/TelegramUI/CallControllerKeyPreviewNode.swift new file mode 100644 index 0000000000..60a5095581 --- /dev/null +++ b/TelegramUI/CallControllerKeyPreviewNode.swift @@ -0,0 +1,107 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramLegacyComponents + +private let emojiFont = Font.regular(28.0) +private let textFont = Font.regular(15.0) + +final class CallControllerKeyPreviewNode: ASDisplayNode { + private let keyTextNode: ASTextNode + private let infoTextNode: ASTextNode + + private let effectView: UIVisualEffectView + + private let dismiss: () -> Void + + init(keyText: String, infoText: String, dismiss: @escaping () -> Void) { + self.keyTextNode = ASTextNode() + self.keyTextNode.displaysAsynchronously = false + self.infoTextNode = ASTextNode() + self.infoTextNode.displaysAsynchronously = false + self.dismiss = dismiss + + self.effectView = UIVisualEffectView() + if #available(iOS 9.0, *) { + } else { + self.effectView.effect = UIBlurEffect(style: .dark) + self.effectView.alpha = 0.0 + } + + super.init() + + self.keyTextNode.attributedText = NSAttributedString(string: keyText, attributes: [NSFontAttributeName: Font.regular(58.0), NSKernAttributeName: 9.0 as NSNumber]) + + self.infoTextNode.attributedText = NSAttributedString(string: infoText, font: Font.regular(14.0), textColor: UIColor.white, paragraphAlignment: .center) + + self.view.addSubview(self.effectView) + self.addSubnode(self.keyTextNode) + self.addSubnode(self.infoTextNode) + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapResture(_:)))) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.effectView.frame = CGRect(origin: CGPoint(), size: size) + + let keyTextSize = self.keyTextNode.measure(CGSize(width: 300.0, height: 300.0)) + transition.updateFrame(node: self.keyTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - keyTextSize.width) / 2) + 6.0, y: floor((size.height - keyTextSize.height) / 2) - 50.0), size: keyTextSize)) + + let infoTextSize = self.infoTextNode.measure(CGSize(width: size.width - 20.0, height: CGFloat.greatestFiniteMagnitude)) + transition.updateFrame(node: self.infoTextNode, frame: CGRect(origin: CGPoint(x: floor((size.width - infoTextSize.width) / 2.0), y: floor((size.height - infoTextSize.height) / 2.0) + 30.0), size: infoTextSize)) + } + + func animateIn(from rect: CGRect, fromNode: ASDisplayNode) { + self.keyTextNode.layer.animatePosition(from: CGPoint(x: rect.midX, y: rect.midY), to: self.keyTextNode.layer.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + if let transitionView = fromNode.view.snapshotView(afterScreenUpdates: false) { + self.view.addSubview(transitionView) + transitionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + transitionView.layer.animatePosition(from: CGPoint(x: rect.midX, y: rect.midY), to: self.keyTextNode.layer.position, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak transitionView] _ in + transitionView?.removeFromSuperview() + }) + transitionView.layer.animateScale(from: 1.0, to: self.keyTextNode.frame.size.width / rect.size.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + self.keyTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + + self.keyTextNode.layer.animateScale(from: rect.size.width / self.keyTextNode.frame.size.width, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + + self.infoTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + + UIView.animate(withDuration: 0.3, animations: { + if #available(iOS 9.0, *) { + self.effectView.effect = TGBlurEffect.call()! + } else { + self.effectView.alpha = 1.0 + } + }) + } + + func animateOut(to rect: CGRect, toNode: ASDisplayNode, completion: @escaping () -> Void) { + self.keyTextNode.layer.animatePosition(from: self.keyTextNode.layer.position, to: CGPoint(x: rect.midX, y: rect.midY), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + completion() + }) + self.keyTextNode.layer.animateScale(from: 1.0, to: rect.size.width / self.keyTextNode.frame.size.width, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + + self.infoTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + + UIView.animate(withDuration: 0.3, animations: { + if #available(iOS 9.0, *) { + self.effectView.effect = nil + } else { + self.effectView.alpha = 0.0 + } + }) + } + + @objc func tapResture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.dismiss() + } + } +} + diff --git a/TelegramUI/CallControllerNode.swift b/TelegramUI/CallControllerNode.swift new file mode 100644 index 0000000000..7e34e0c02d --- /dev/null +++ b/TelegramUI/CallControllerNode.swift @@ -0,0 +1,369 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +private func generateBackArrowImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 13.0, height: 22.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + let _ = try? drawSvgPath(context, path: "M10.6569398,0.0 L0.0,11 L10.6569398,22 L13,19.1782395 L5.07681762,11 L13,2.82176047 Z ") + }) +} + +final class CallControllerNode: ASDisplayNode { + private let account: Account + + private let statusBar: StatusBar + + private var presentationData: PresentationData + private var peer: Peer? + + private let containerNode: ASDisplayNode + + private let imageNode: TransformImageNode + private let dimNode: ASDisplayNode + private let backButtonArrowNode: ASImageNode + private let backButtonNode: HighlightableButtonNode + private let statusNode: CallControllerStatusNode + private let buttonsNode: CallControllerButtonsNode + private var keyPreviewNode: CallControllerKeyPreviewNode? + + private var keyTextData: (Data, String)? + private let keyButtonNode: HighlightableButtonNode + + private var validLayout: (ContainerViewLayout, CGFloat)? + + var isMuted: Bool = false { + didSet { + self.buttonsNode.isMuted = self.isMuted + } + } + + var speakerMode: Bool = false { + didSet { + self.buttonsNode.speakerMode = self.speakerMode + } + } + + var toggleMute: (() -> Void)? + var toggleSpeaker: (() -> Void)? + var acceptCall: (() -> Void)? + var endCall: (() -> Void)? + var back: (() -> Void)? + var disissedInteractively: (() -> Void)? + + init(account: Account, presentationData: PresentationData, statusBar: StatusBar) { + self.account = account + self.presentationData = presentationData + self.statusBar = statusBar + + self.containerNode = ASDisplayNode() + + self.imageNode = TransformImageNode() + self.dimNode = ASDisplayNode() + self.dimNode.isLayerBacked = true + self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4) + + self.backButtonArrowNode = ASImageNode() + self.backButtonArrowNode.displayWithoutProcessing = true + self.backButtonArrowNode.displaysAsynchronously = false + self.backButtonArrowNode.image = generateBackArrowImage(color: .white) + self.backButtonNode = HighlightableButtonNode() + + self.statusNode = CallControllerStatusNode() + self.buttonsNode = CallControllerButtonsNode(strings: self.presentationData.strings) + self.keyButtonNode = HighlightableButtonNode() + + super.init(viewBlock: { + return UITracingLayerView() + }, didLoad: nil) + + self.containerNode.backgroundColor = .black + + self.addSubnode(self.containerNode) + + self.backButtonNode.setTitle(presentationData.strings.Common_Back, with: Font.regular(17.0), with: .white, for: []) + self.backButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -20.0, bottom: -8.0, right: -8.0) + self.backButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.backButtonNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backButtonArrowNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backButtonNode.alpha = 0.4 + strongSelf.backButtonArrowNode.alpha = 0.4 + } else { + strongSelf.backButtonNode.alpha = 1.0 + strongSelf.backButtonArrowNode.alpha = 1.0 + strongSelf.backButtonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.backButtonArrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + + self.containerNode.addSubnode(self.imageNode) + self.containerNode.addSubnode(self.dimNode) + self.containerNode.addSubnode(self.statusNode) + self.containerNode.addSubnode(self.buttonsNode) + self.containerNode.addSubnode(self.keyButtonNode) + self.containerNode.addSubnode(self.backButtonArrowNode) + self.containerNode.addSubnode(self.backButtonNode) + + self.buttonsNode.mute = { [weak self] in + self?.toggleMute?() + } + + self.buttonsNode.speaker = { [weak self] in + self?.toggleSpeaker?() + } + + self.buttonsNode.end = { [weak self] in + self?.endCall?() + } + + self.buttonsNode.accept = { [weak self] in + self?.acceptCall?() + } + + self.keyButtonNode.addTarget(self, action: #selector(self.keyPressed), forControlEvents: .touchUpInside) + + self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside) + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + } + + func updatePeer(peer: Peer) { + if !arePeersEqual(self.peer, peer) { + self.peer = peer + + self.imageNode.setSignal(account: self.account, signal: chatAvatarGalleryPhoto(account: self.account, representations: peer.profileImageRepresentations, autoFetchFullSize: true)) + + self.statusNode.title = peer.displayTitle + + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + } + + func updateCallState(_ callState: PresentationCallState) { + let statusValue: CallControllerStatusValue + switch callState { + case .waiting, .connecting: + statusValue = .text(self.presentationData.strings.Call_StatusConnecting) + case let .requesting(ringing): + if ringing { + statusValue = .text(self.presentationData.strings.Call_StatusRinging) + } else { + statusValue = .text(self.presentationData.strings.Call_StatusRequesting) + } + case .terminating, .terminated: + statusValue = .text(self.presentationData.strings.Call_StatusEnded) + case .ringing: + statusValue = .text(self.presentationData.strings.Call_StatusIncoming) + case let .active(timestamp, keyVisualHash): + let strings = self.presentationData.strings + statusValue = .timer({ value in + return strings.Call_StatusOngoing(value).0 + }, timestamp) + if self.keyTextData?.0 != keyVisualHash { + let text = stringForEmojiHashOfData(keyVisualHash, 4)! + self.keyTextData = (keyVisualHash, text) + + self.keyButtonNode.setAttributedTitle(NSAttributedString(string: text, attributes: [NSFontAttributeName: Font.regular(22.0), NSKernAttributeName: 2.5 as NSNumber]), for: []) + + let keyTextSize = self.keyButtonNode.measure(CGSize(width: 200.0, height: 200.0)) + self.keyButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.keyButtonNode.frame = CGRect(origin: self.keyButtonNode.frame.origin, size: keyTextSize) + + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } + } + } + switch callState { + case .terminated, .terminating: + if !self.statusNode.alpha.isEqual(to: 0.5) { + self.statusNode.alpha = 0.5 + self.buttonsNode.alpha = 0.5 + self.keyButtonNode.alpha = 0.5 + self.backButtonArrowNode.alpha = 0.5 + self.backButtonNode.alpha = 0.5 + + self.statusNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25) + self.buttonsNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25) + self.keyButtonNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25) + } + default: + if !self.statusNode.alpha.isEqual(to: 1.0) { + self.statusNode.alpha = 1.0 + self.buttonsNode.alpha = 1.0 + self.keyButtonNode.alpha = 1.0 + self.backButtonArrowNode.alpha = 1.0 + self.backButtonNode.alpha = 1.0 + } + } + self.statusNode.status = statusValue + + switch callState { + case .ringing: + self.buttonsNode.updateMode(.incoming) + default: + self.buttonsNode.updateMode(.active(.speaker)) + } + } + + func animateIn() { + var bounds = self.bounds + bounds.origin = CGPoint() + self.bounds = bounds + self.layer.removeAnimation(forKey: "bounds") + self.statusBar.layer.removeAnimation(forKey: "opacity") + self.containerNode.layer.removeAnimation(forKey: "opacity") + self.containerNode.layer.removeAnimation(forKey: "scale") + self.statusBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.containerNode.layer.animateScale(from: 1.04, to: 1.0, duration: 0.3) + self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + func animateOut(completion: @escaping () -> Void) { + self.statusBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.containerNode.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (layout, navigationBarHeight) + + transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + + if let keyPreviewNode = self.keyPreviewNode { + transition.updateFrame(node: keyPreviewNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + keyPreviewNode.updateLayout(size: layout.size, transition: .immediate) + } + + transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: layout.size)) + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: 640.0, height: 640.0).aspectFilled(layout.size), boundingSize: layout.size, intrinsicInsets: UIEdgeInsets()) + let apply = self.imageNode.asyncLayout()(arguments) + apply() + + let backSize = self.backButtonNode.measure(CGSize(width: 320.0, height: 100.0)) + if let image = self.backButtonArrowNode.image { + transition.updateFrame(node: self.backButtonArrowNode, frame: CGRect(origin: CGPoint(x: 10.0, y: 31.0), size: image.size)) + } + transition.updateFrame(node: self.backButtonNode, frame: CGRect(origin: CGPoint(x: 29.0, y: 31.0), size: backSize)) + + let statusOffset: CGFloat + if layout.metrics.widthClass == .regular && layout.metrics.heightClass == .regular { + if layout.size.height.isEqual(to: 1366.0) { + statusOffset = 160.0 + } else { + statusOffset = 120.0 + } + } else { + if layout.size.height.isEqual(to: 736.0) { + statusOffset = 80.0 + } else if layout.size.width.isEqual(to: 320.0) { + statusOffset = 60.0 + } else { + statusOffset = 64.0 + } + } + + let buttonsHeight: CGFloat = 75.0 + let buttonsOffset: CGFloat + if layout.size.width.isEqual(to: 320.0) { + if layout.size.height.isEqual(to: 480.0) { + buttonsOffset = 53.0 + } else { + buttonsOffset = 63.0 + } + } else { + buttonsOffset = 83.0 + } + + let statusHeight = self.statusNode.updateLayout(constrainedWidth: layout.size.width, transition: transition) + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusOffset), size: CGSize(width: layout.size.width, height: statusHeight))) + + self.buttonsNode.updateLayout(constrainedWidth: layout.size.width, transition: transition) + transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - (buttonsOffset - 40.0) - buttonsHeight), size: CGSize(width: layout.size.width, height: buttonsHeight))) + + let keyTextSize = self.keyButtonNode.frame.size + transition.updateFrame(node: self.keyButtonNode, frame: CGRect(origin: CGPoint(x: layout.size.width - keyTextSize.width - 8.0, y: 28.0), size: keyTextSize)) + } + + @objc func keyPressed() { + if self.keyPreviewNode == nil, let keyText = self.keyTextData?.1, let peer = self.peer { + let keyPreviewNode = CallControllerKeyPreviewNode(keyText: keyText, infoText: self.presentationData.strings.Call_EmojiDescription(peer.compactDisplayTitle).0, dismiss: { [weak self] in + if let _ = self?.keyPreviewNode { + self?.backPressed() + } + }) + self.containerNode.insertSubnode(keyPreviewNode, aboveSubnode: self.dimNode) + self.keyPreviewNode = keyPreviewNode + + if let (validLayout, _) = self.validLayout { + keyPreviewNode.updateLayout(size: validLayout.size, transition: .immediate) + + self.keyButtonNode.isHidden = true + keyPreviewNode.animateIn(from: self.keyButtonNode.frame, fromNode: self.keyButtonNode) + } + } + } + + @objc func backPressed() { + if let keyPreviewNode = self.keyPreviewNode { + self.keyPreviewNode = nil + keyPreviewNode.animateOut(to: self.keyButtonNode.frame, toNode: self.keyButtonNode, completion: { [weak self, weak keyPreviewNode] in + self?.keyButtonNode.isHidden = false + keyPreviewNode?.removeFromSupernode() + }) + } else { + self.back?() + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .changed: + let offset = recognizer.translation(in: self.view).y + var bounds = self.bounds + bounds.origin.y = -offset + self.bounds = bounds + case .ended: + let velocity = recognizer.velocity(in: self.view).y + if abs(velocity) < 100.0 { + var bounds = self.bounds + let previous = bounds + bounds.origin = CGPoint() + self.bounds = bounds + self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + } else { + var bounds = self.bounds + let previous = bounds + bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height) + self.bounds = bounds + self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: kCAMediaTimingFunctionEaseOut, completion: { [weak self] _ in + self?.disissedInteractively?() + }) + } + case .cancelled: + var bounds = self.bounds + let previous = bounds + bounds.origin = CGPoint() + self.bounds = bounds + self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + default: + break + } + } +} diff --git a/TelegramUI/CallControllerStatusNode.swift b/TelegramUI/CallControllerStatusNode.swift new file mode 100644 index 0000000000..002d8afb64 --- /dev/null +++ b/TelegramUI/CallControllerStatusNode.swift @@ -0,0 +1,130 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +private let compactNameFont = Font.light(28.0) +private let regularNameFont = Font.light(36.0) + +private let compactStatusFont = Font.regular(18.0) +private let regularStatusFont = Font.regular(18.0) + +enum CallControllerStatusValue: Equatable { + case text(String) + case timer((String) -> String, Double) + + static func ==(lhs: CallControllerStatusValue, rhs: CallControllerStatusValue) -> Bool { + switch lhs { + case let .text(text): + if case .text(text) = rhs { + return true + } else { + return false + } + case let .timer(_, referenceTime): + if case .timer(_, referenceTime) = rhs { + return true + } else { + return false + } + } + } +} + +final class CallControllerStatusNode: ASDisplayNode { + private let titleNode: TextNode + private let statusNode: TextNode + private let statusMeasureNode: TextNode + + var title: String = "" + var status: CallControllerStatusValue = .text("") { + didSet { + if self.status != oldValue { + self.statusTimer?.invalidate() + + if case .timer = self.status { + self.statusTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + if let strongSelf = self, let validLayoutWidth = strongSelf.validLayoutWidth { + let _ = strongSelf.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate) + } + }, queue: Queue.mainQueue()) + self.statusTimer?.start() + } else { + if let validLayoutWidth = self.validLayoutWidth { + let _ = self.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate) + } + } + } + } + } + + private var statusTimer: SwiftSignalKit.Timer? + private var validLayoutWidth: CGFloat? + + override init() { + self.titleNode = TextNode() + self.statusNode = TextNode() + self.statusNode.displaysAsynchronously = false + self.statusMeasureNode = TextNode() + + super.init() + + self.isUserInteractionEnabled = false + + self.addSubnode(self.titleNode) + self.addSubnode(self.statusNode) + } + + deinit { + self.statusTimer?.invalidate() + } + + func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + self.validLayoutWidth = constrainedWidth + + let nameFont: UIFont + let statusFont: UIFont + if constrainedWidth < 330.0 { + nameFont = compactNameFont + statusFont = compactStatusFont + } else { + nameFont = regularNameFont + statusFont = regularStatusFont + } + + let statusText: String + let statusMeasureText: String + switch self.status { + case let .text(text): + statusText = text + statusMeasureText = text + case let .timer(format, referenceTime): + let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime) + let durationString: String + let measureDurationString: String + if duration > 60 * 60 { + durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60]) + measureDurationString = "00:00:00" + } else { + durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60]) + measureDurationString = "00:00" + } + statusText = format(durationString) + statusMeasureText = format(measureDurationString) + } + + let spacing: CGFloat = 4.0 + let (titleLayout, titleApply) = TextNode.asyncLayout(self.titleNode)(NSAttributedString(string: self.title, font: nameFont, textColor: .white), nil, 1, .end, CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)) + let (statusMeasureLayout, statusMeasureApply) = TextNode.asyncLayout(self.statusMeasureNode)(NSAttributedString(string: statusMeasureText, font: statusFont, textColor: .white), nil, 1, .end, CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)) + let (statusLayout, statusApply) = TextNode.asyncLayout(self.statusNode)(NSAttributedString(string: statusText, font: statusFont, textColor: .white), nil, 1, .end, CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)) + + let _ = titleApply() + let _ = statusApply() + let _ = statusMeasureApply() + + self.titleNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - titleLayout.size.width) / 2.0), y: 0.0), size: titleLayout.size) + self.statusNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - statusMeasureLayout.size.width) / 2.0), y: titleLayout.size.height + spacing), size: statusLayout.size) + + return titleLayout.size.height + spacing + statusLayout.size.height + } +} diff --git a/TelegramUI/CallKitIntergation.swift b/TelegramUI/CallKitIntergation.swift new file mode 100644 index 0000000000..fc563d3686 --- /dev/null +++ b/TelegramUI/CallKitIntergation.swift @@ -0,0 +1,229 @@ +import Foundation +import CallKit +import AVFoundation +import Postbox +import SwiftSignalKit + +final class CallKitIntegration { + private let providerDelegate: AnyObject + + private let audioSessionActivePromise = ValuePromise(false, ignoreRepeated: true) + var audioSessionActive: Signal { + return self.audioSessionActivePromise.get() + } + + init?(startCall: @escaping (UUID, String) -> Signal, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal, audioSessionActivationChanged: @escaping (Bool) -> Void) { + #if (arch(i386) || arch(x86_64)) && os(iOS) + return nil + #else + if #available(iOSApplicationExtension 10.0, *) { + self.providerDelegate = CallKitProviderDelegate(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, audioSessionActivationChanged: audioSessionActivationChanged) + } else { + return nil + } + #endif + } + + func startCall(peerId: PeerId, displayTitle: String) { + if #available(iOSApplicationExtension 10.0, *) { + (self.providerDelegate as! CallKitProviderDelegate).startCall(peerId: peerId, displayTitle: displayTitle) + } + } + + func answerCall(uuid: UUID) { + if #available(iOSApplicationExtension 10.0, *) { + (self.providerDelegate as! CallKitProviderDelegate).answerCall(uuid: uuid) + } + } + + func dropCall(uuid: UUID) { + if #available(iOSApplicationExtension 10.0, *) { + (self.providerDelegate as! CallKitProviderDelegate).dropCall(uuid: uuid) + } + } + + func reportIncomingCall(uuid: UUID, handle: String, displayTitle: String, completion: ((NSError?) -> Void)?) { + if #available(iOSApplicationExtension 10.0, *) { + (self.providerDelegate as! CallKitProviderDelegate).reportIncomingCall(uuid: uuid, handle: handle, displayTitle: displayTitle, completion: completion) + } + } + + func reportOutgoingCallConnected(uuid: UUID, at date: Date) { + if #available(iOSApplicationExtension 10.0, *) { + (self.providerDelegate as! CallKitProviderDelegate).reportOutgoingCallConnected(uuid: uuid, at: date) + } + } +} + +@available(iOSApplicationExtension 10.0, *) +class CallKitProviderDelegate: NSObject, CXProviderDelegate { + private let provider: CXProvider + private let callController = CXCallController() + + private let startCall: (UUID, String) -> Signal + private let answerCall: (UUID) -> Void + private let endCall: (UUID) -> Signal + private let audioSessionActivationChanged: (Bool) -> Void + + private let disposableSet = DisposableSet() + + fileprivate let audioSessionActivePromise: ValuePromise + + init(audioSessionActivePromise: ValuePromise, startCall: @escaping (UUID, String) -> Signal, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal, audioSessionActivationChanged: @escaping (Bool) -> Void) { + self.audioSessionActivePromise = audioSessionActivePromise + self.startCall = startCall + self.answerCall = answerCall + self.endCall = endCall + self.audioSessionActivationChanged = audioSessionActivationChanged + + self.provider = CXProvider(configuration: CallKitProviderDelegate.providerConfiguration) + + super.init() + + self.provider.setDelegate(self, queue: nil) + } + + static var providerConfiguration: CXProviderConfiguration { + let providerConfiguration = CXProviderConfiguration(localizedName: "Telegram") + + providerConfiguration.supportsVideo = false + providerConfiguration.maximumCallsPerCallGroup = 1 + providerConfiguration.maximumCallGroups = 1 + providerConfiguration.supportedHandleTypes = [.phoneNumber, .generic] + if let image = UIImage(bundleImageName: "Call/CallKitLogo") { + providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(image) + } + + return providerConfiguration + } + + private func requestTransaction(_ transaction: CXTransaction, completion: ((Bool) -> Void)? = nil) { + self.callController.request(transaction) { error in + if let error = error { + print("Error requesting transaction: \(error)") + } + completion?(error == nil) + } + } + + func endCall(uuid: UUID) { + let endCallAction = CXEndCallAction(call: uuid) + let transaction = CXTransaction(action: endCallAction) + self.requestTransaction(transaction) + } + + func dropCall(uuid: UUID) { + self.provider.reportCall(with: uuid, endedAt: nil, reason: CXCallEndedReason.remoteEnded) + } + + func answerCall(uuid: UUID) { + + } + + func startCall(peerId: PeerId, displayTitle: String) { + let uuid = UUID() + let handle = CXHandle(type: .generic, value: "\(peerId.id)") + let startCallAction = CXStartCallAction(call: uuid, handle: handle) + startCallAction.contactIdentifier = displayTitle + + startCallAction.isVideo = false + let transaction = CXTransaction(action: startCallAction) + + self.requestTransaction(transaction, completion: { _ in + let update = CXCallUpdate() + update.remoteHandle = handle + update.localizedCallerName = displayTitle + update.supportsHolding = false + update.supportsGrouping = false + update.supportsUngrouping = false + update.supportsDTMF = false + + self.provider.reportCall(with: uuid, updated: update) + }) + } + + func reportIncomingCall(uuid: UUID, handle: String, displayTitle: String, completion: ((NSError?) -> Void)?) { + let update = CXCallUpdate() + update.remoteHandle = CXHandle(type: .generic, value: handle) + update.localizedCallerName = displayTitle + update.supportsHolding = false + update.supportsGrouping = false + update.supportsUngrouping = false + update.supportsDTMF = false + + self.provider.reportNewIncomingCall(with: uuid, update: update, completion: { error in + completion?(error as NSError?) + }) + } + + func reportOutgoingCallConnecting(uuid: UUID, at date: Date) { + self.provider.reportOutgoingCall(with: uuid, startedConnectingAt: date) + } + + func reportOutgoingCallConnected(uuid: UUID, at date: Date) { + self.provider.reportOutgoingCall(with: uuid, connectedAt: date) + } + + func providerDidReset(_ provider: CXProvider) { + /*stopAudio() + + for call in callManager.calls { + call.end() + } + + callManager.removeAllCalls()*/ + } + + func provider(_ provider: CXProvider, perform action: CXStartCallAction) { + let disposable = MetaDisposable() + self.disposableSet.add(disposable) + disposable.set((self.startCall(action.callUUID, action.handle.value) + |> deliverOnMainQueue + |> afterDisposed { [weak self, weak disposable] in + if let strongSelf = self, let disposable = disposable { + strongSelf.disposableSet.remove(disposable) + } + }).start(next: { result in + if result { + action.fulfill() + } else { + action.fail() + } + })) + } + + func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { + self.answerCall(action.callUUID) + + action.fulfill() + } + + func provider(_ provider: CXProvider, perform action: CXEndCallAction) { + let disposable = MetaDisposable() + self.disposableSet.add(disposable) + disposable.set((self.endCall(action.callUUID) + |> deliverOnMainQueue + |> afterDisposed { [weak self, weak disposable] in + if let strongSelf = self, let disposable = disposable { + strongSelf.disposableSet.remove(disposable) + } + }).start(next: { result in + if result { + action.fulfill(withDateEnded: Date()) + } else { + action.fail() + } + })) + } + + func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { + self.audioSessionActivationChanged(true) + self.audioSessionActivePromise.set(true) + } + + func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { + self.audioSessionActivationChanged(false) + self.audioSessionActivePromise.set(false) + } +} + diff --git a/TelegramUI/CallListCallItem.swift b/TelegramUI/CallListCallItem.swift new file mode 100644 index 0000000000..aa273ee016 --- /dev/null +++ b/TelegramUI/CallListCallItem.swift @@ -0,0 +1,536 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Postbox +import Display +import SwiftSignalKit +import TelegramCore + +private let titleFont = Font.regular(17.0) +private let statusFont = Font.regular(14.0) +private let dateFont = Font.regular(15.0) + +private func callDurationString(strings: PresentationStrings, duration: Int32) -> String { + if duration < 60 { + return strings.Call_ShortSeconds(duration) + } else { + return strings.Call_ShortMinutes(duration / 60) + } +} + +class CallListCallItem: ListViewItem { + let theme: PresentationTheme + let strings: PresentationStrings + let account: Account + let topMessage: Message + let messages: [Message] + let editing: Bool + let revealed: Bool + let interaction: CallListNodeInteraction + + let selectable: Bool = true + let headerAccessoryItem: ListViewAccessoryItem? + let header: ListViewItemHeader? + + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, topMessage: Message, messages: [Message], editing: Bool, revealed: Bool, interaction: CallListNodeInteraction) { + self.theme = theme + self.strings = strings + self.account = account + self.topMessage = topMessage + self.messages = messages + self.editing = editing + self.revealed = revealed + self.interaction = interaction + + self.headerAccessoryItem = nil + self.header = nil + } + + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = CallListCallItemNode() + let makeLayout = node.asyncLayout() + let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) + let (nodeLayout, nodeApply) = makeLayout(self, width, first, last, firstWithHeader) + node.contentSize = nodeLayout.contentSize + node.insets = nodeLayout.insets + + completion(node, { + return (nil, { + nodeApply().1(false) + }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? CallListCallItemNode { + Queue.mainQueue().async { + let layout = node.asyncLayout() + async { + let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) + let (nodeLayout, apply) = layout(self, width, first, last, firstWithHeader) + var animated = true + if case .None = animation { + animated = false + } + Queue.mainQueue().async { + completion(nodeLayout, { + apply().1(animated) + }) + } + } + } + } + } + + func selected(listView: ListView) { + listView.clearHighlightAnimated(true) + self.interaction.call(self.topMessage.id.peerId) + } + + static func mergeType(item: CallListCallItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool, firstWithHeader: Bool) { + var first = false + var last = false + var firstWithHeader = false + if let previousItem = previousItem { + if let header = item.header { + if let previousItem = previousItem as? CallListCallItem { + firstWithHeader = header.id != previousItem.header?.id + } else { + firstWithHeader = true + } + } + } else { + first = true + firstWithHeader = item.header != nil + } + if let nextItem = nextItem { + if let header = item.header { + if let nextItem = nextItem as? CallListCallItem { + last = header.id != nextItem.header?.id + } else { + last = true + } + } + } else { + last = true + } + return (first, last, firstWithHeader) + } +} + +private let separatorHeight = 1.0 / UIScreen.main.scale + +class CallListCallItemNode: ItemListRevealOptionsItemNode { + private let backgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + private let avatarNode: AvatarNode + private let titleNode: TextNode + private let statusNode: TextNode + private let dateNode: TextNode + private let typeIconNode: ASImageNode + private let infoButtonNode: HighlightableButtonNode + + var editableControlNode: ItemListEditableControlNode? + + private var avatarState: (Account, Peer?)? + private var layoutParams: (CallListCallItem, CGFloat, Bool, Bool, Bool)? + + required init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.avatarNode = AvatarNode(font: Font.regular(15.0)) + self.avatarNode.isLayerBacked = true + + self.titleNode = TextNode() + self.statusNode = TextNode() + self.dateNode = TextNode() + + self.typeIconNode = ASImageNode() + self.typeIconNode.isLayerBacked = true + self.typeIconNode.displayWithoutProcessing = true + self.typeIconNode.displaysAsynchronously = false + + self.infoButtonNode = HighlightableButtonNode() + self.infoButtonNode.hitTestSlop = UIEdgeInsets(top: 4.0, left: 4.0, bottom: 4.0, right: 4.0) + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.avatarNode) + self.addSubnode(self.typeIconNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.statusNode) + self.addSubnode(self.dateNode) + self.addSubnode(self.infoButtonNode) + + self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside) + } + + override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + if let (item, _, _, _, _) = self.layoutParams { + let (first, last, firstWithHeader) = CallListCallItem.mergeType(item: item, previousItem: previousItem, nextItem: nextItem) + self.layoutParams = (item, width, first, last, firstWithHeader) + let makeLayout = self.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(item, width, first, last, firstWithHeader) + self.contentSize = nodeLayout.contentSize + self.insets = nodeLayout.insets + let _ = nodeApply() + } + } + + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + func asyncLayout() -> (_ item: CallListCallItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (Bool) -> Void)) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeStatusLayout = TextNode.asyncLayout(self.statusNode) + let makeDateLayout = TextNode.asyncLayout(self.dateNode) + let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) + let currentItem = self.layoutParams?.0 + + return { [weak self] item, width, first, last, firstWithHeader in + var updatedTheme: PresentationTheme? + var updatedInfoIcon = false + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + + updatedInfoIcon = true + } + + let editingOffset: CGFloat + var editableControlSizeAndApply: (CGSize, () -> ItemListEditableControlNode)? + if item.editing { + let sizeAndApply = editableControlLayout(56.0) + editableControlSizeAndApply = sizeAndApply + editingOffset = sizeAndApply.0.width + } else { + editingOffset = 0.0 + } + + var leftInset: CGFloat = 86.0 + let rightInset: CGFloat = 13.0 + var infoIconRightInset: CGFloat = rightInset + + var dateRightInset: CGFloat = 43.0 + if item.editing { + leftInset += editingOffset + dateRightInset += 5.0 + infoIconRightInset -= 36.0 + } + + var titleAttributedString: NSAttributedString? + var statusAttributedString: NSAttributedString? + + var titleColor = item.theme.list.itemPrimaryTextColor + var hasMissed = false + var hasIncoming = false + var hasOutgoing = false + + var hadDuration = false + var callDuration: Int32? + + for message in item.messages { + inner: for media in message.media { + if let action = media as? TelegramMediaAction { + if case let .phoneCall(_, discardReason, duration) = action.action { + if message.flags.contains(.Incoming) { + hasIncoming = true + + if let discardReason = discardReason, case .missed = discardReason { + titleColor = item.theme.list.itemDestructiveColor + hasMissed = true + } + } else { + hasOutgoing = true + } + if callDuration == nil && !hadDuration { + hadDuration = true + callDuration = duration + } else { + callDuration = nil + } + } + break inner + } + } + } + + if let peer = item.topMessage.peers[item.topMessage.id.peerId] { + if let user = peer as? TelegramUser { + if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor)) + string.append(NSAttributedString(string: lastName, font: titleFont, textColor: titleColor)) + if item.messages.count > 1 { + string.append(NSAttributedString(string: " (\(item.messages.count))", font: titleFont, textColor: titleColor)) + } + titleAttributedString = string + } else if let firstName = user.firstName, !firstName.isEmpty { + titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: titleColor) + } else if let lastName = user.lastName, !lastName.isEmpty { + titleAttributedString = NSAttributedString(string: lastName, font: titleFont, textColor: titleColor) + } else { + titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleFont, textColor: titleColor) + } + } else if let group = peer as? TelegramGroup { + titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: titleColor) + } else if let channel = peer as? TelegramChannel { + titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor) + } + + if hasMissed { + statusAttributedString = NSAttributedString(string: item.strings.Calls_Missed, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + } else if hasIncoming && hasOutgoing { + statusAttributedString = NSAttributedString(string: item.strings.Notification_CallOutgoingShort + ", " + item.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + } else if hasIncoming { + if let callDuration = callDuration, callDuration != 0 { + statusAttributedString = NSAttributedString(string: item.strings.Notification_CallTimeFormat(item.strings.Notification_CallIncomingShort, callDurationString(strings: item.strings, duration: callDuration)).0, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + } else { + statusAttributedString = NSAttributedString(string: item.strings.Notification_CallIncomingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + } + } else { + if let callDuration = callDuration, callDuration != 0 { + statusAttributedString = NSAttributedString(string: item.strings.Notification_CallTimeFormat(item.strings.Notification_CallOutgoingShort, callDurationString(strings: item.strings, duration: callDuration)).0, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + } else { + statusAttributedString = NSAttributedString(string: item.strings.Notification_CallOutgoingShort, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) + } + } + } + + var t = Int(item.topMessage.timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo) + + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.topMessage.timestamp, relativeTo: timestamp) + + let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + + let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + + let (dateLayout, dateApply) = makeDateLayout(NSAttributedString(string: dateText, font: dateFont, textColor: item.theme.list.itemSecondaryTextColor), nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) + + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 56.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + + let outgoingIcon = PresentationResourcesCallList.outgoingIcon(item.theme) + let infoIcon = PresentationResourcesCallList.infoButton(item.theme) + + return (nodeLayout, { [weak self] in + if let strongSelf = self { + if let peer = item.topMessage.peers[item.topMessage.id.peerId] { + strongSelf.avatarNode.setPeer(account: item.account, peer: peer) + } + + return (strongSelf.avatarNode.ready, { [weak strongSelf] animated in + if let strongSelf = strongSelf { + strongSelf.layoutParams = (item, width, first, last, firstWithHeader) + + let revealOffset = strongSelf.revealOffset + + let transition: ContainedViewLayoutTransition + if animated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + + if let _ = updatedTheme { + strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + + if let editableControlSizeAndApply = editableControlSizeAndApply { + if strongSelf.editableControlNode == nil { + let editableControlNode = editableControlSizeAndApply.1() + editableControlNode.tapped = { + if let strongSelf = self { + strongSelf.setRevealOptionsOpened(true, animated: true) + strongSelf.revealOptionsInteractivelyOpened() + } + } + strongSelf.editableControlNode = editableControlNode + strongSelf.addSubnode(editableControlNode) + let editableControlFrame = CGRect(origin: CGPoint(x: revealOffset, y: 0.0), size: editableControlSizeAndApply.0) + editableControlNode.frame = editableControlFrame + transition.animatePosition(node: editableControlNode, from: CGPoint(x: editableControlFrame.midX - editableControlFrame.size.width, y: editableControlFrame.midY)) + editableControlNode.alpha = 0.0 + transition.updateAlpha(node: editableControlNode, alpha: 1.0) + } + } else if let editableControlNode = strongSelf.editableControlNode { + var editableControlFrame = editableControlNode.frame + editableControlFrame.origin.x = -editableControlFrame.size.width + strongSelf.editableControlNode = nil + transition.updateAlpha(node: editableControlNode, alpha: 0.0) + transition.updateFrame(node: editableControlNode, frame: editableControlFrame, completion: { [weak editableControlNode] _ in + editableControlNode?.removeFromSupernode() + }) + } + + transition.updateFrame(node: strongSelf.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: 8.0), size: CGSize(width: 40.0, height: 40.0))) + + let _ = titleApply() + transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 8.0), size: titleLayout.size)) + + let _ = statusApply() + transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 30.0), size: statusLayout.size)) + + let _ = dateApply() + transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + width - dateRightInset - dateLayout.size.width, y: floor((nodeLayout.contentSize.height - dateLayout.size.height) / 2.0) + 2.0), size: dateLayout.size)) + + if let outgoingIcon = outgoingIcon { + if strongSelf.typeIconNode.image !== outgoingIcon { + strongSelf.typeIconNode.image = outgoingIcon + } + transition.updateFrame(node: strongSelf.typeIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 76.0, y: floor((nodeLayout.contentSize.height - outgoingIcon.size.height) / 2.0)), size: outgoingIcon.size)) + } + strongSelf.typeIconNode.isHidden = !hasOutgoing + + if let infoIcon = infoIcon { + if updatedInfoIcon { + strongSelf.infoButtonNode.setImage(infoIcon, for: []) + } + transition.updateFrame(node: strongSelf.infoButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - infoIconRightInset - infoIcon.size.width, y: floor((nodeLayout.contentSize.height - infoIcon.size.height) / 2.0)), size: infoIcon.size)) + } + transition.updateAlpha(node: strongSelf.infoButtonNode, alpha: item.editing ? 0.0 : 1.0) + + let topHighlightInset: CGFloat = (first || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) + transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - 65.0), height: separatorHeight))) + strongSelf.separatorNode.isHidden = last + + strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.strings.Common_Delete, icon: nil, color: UIColor(rgb: 0xff3824))]) + } + }) + } else { + return (nil, { _ in }) + } + }) + } + } + + override func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { + let bounds = self.bounds + accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0)) + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.3, removeOnCompletion: false) + } + + override public func header() -> ListViewItemHeader? { + if let (item, _, _, _, _) = self.layoutParams { + return item.header + } else { + return nil + } + } + + @objc func infoPressed() { + if let item = self.layoutParams?.0 { + item.interaction.openInfo(item.topMessage.id.peerId) + } + } + + override func revealOptionsInteractivelyOpened() { + if let item = self.layoutParams?.0 { + item.interaction.setMessageIdWithRevealedOptions(item.topMessage.id, nil) + } + } + + override func revealOptionsInteractivelyClosed() { + if let item = self.layoutParams?.0 { + item.interaction.setMessageIdWithRevealedOptions(nil, item.topMessage.id) + } + } + + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + if let item = self.layoutParams?.0 { + let revealOffset = offset + + let editingOffset: CGFloat + if let editableControlNode = self.editableControlNode { + editingOffset = editableControlNode.bounds.size.width + var editableControlFrame = editableControlNode.frame + editableControlFrame.origin.x = offset + transition.updateFrame(node: editableControlNode, frame: editableControlFrame) + } else { + editingOffset = 0.0 + } + + let leftInset: CGFloat = 86.0 + editingOffset + let rightInset: CGFloat = 13.0 + var infoIconRightInset: CGFloat = rightInset + + var dateRightInset: CGFloat = 43.0 + if item.editing { + dateRightInset += 5.0 + infoIconRightInset -= 36.0 + } + + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 52.0, y: 8.0), size: CGSize(width: 40.0, height: 40.0))) + + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 8.0), size: self.titleNode.bounds.size)) + + transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset, y: 30.0), size: self.statusNode.bounds.size)) + + transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: editingOffset + revealOffset + self.bounds.size.width - dateRightInset - self.dateNode.bounds.size.width, y: self.dateNode.frame.minY), size: self.dateNode.bounds.size)) + + transition.updateFrame(node: self.typeIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 76.0, y: self.typeIconNode.frame.minY), size: self.typeIconNode.bounds.size)) + + transition.updateFrame(node: self.infoButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + self.bounds.size.width - infoIconRightInset - self.infoButtonNode.bounds.width, y: self.infoButtonNode.frame.minY), size: self.infoButtonNode.bounds.size)) + } + } + + override func revealOptionSelected(_ option: ItemListRevealOption) { + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + if let item = self.layoutParams?.0 { + item.interaction.delete(item.messages.map { $0.id }) + } + } +} diff --git a/TelegramUI/CallListController.swift b/TelegramUI/CallListController.swift new file mode 100644 index 0000000000..325fa27c4a --- /dev/null +++ b/TelegramUI/CallListController.swift @@ -0,0 +1,196 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +public final class CallListController: ViewController { + private var controllerNode: CallListControllerNode { + return self.displayNode as! CallListControllerNode + } + + private let _ready = Promise(false) + override public var ready: Promise { + return self._ready + } + + private let account: Account + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let segmentedTitleView: ItemListControllerSegmentedTitleView + + private let createActionDisposable = MetaDisposable() + + public init(account: Account) { + self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.segmentedTitleView = ItemListControllerSegmentedTitleView(segments: [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed], index: 0, color: self.presentationData.theme.rootController.navigationBar.accentTextColor) + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.navigationItem.titleView = self.segmentedTitleView + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + + self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle + self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconCalls") + self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconCallsSelected") + + self.segmentedTitleView.indexUpdated = { [weak self] index in + if let strongSelf = self { + strongSelf.controllerNode.updateType(index == 0 ? .all : .missed) + } + } + + 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.scrollToTop = { [weak self] in + self?.controllerNode.scrollToLatest() + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.createActionDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.segmentedTitleView.segments = [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed] + self.segmentedTitleView.color = self.presentationData.theme.rootController.navigationBar.accentTextColor + + self.tabBarItem.title = self.presentationData.strings.Calls_TabTitle + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + if self.isNodeLoaded { + self.controllerNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + } + + } + + override public func loadDisplayNode() { + self.displayNode = CallListControllerNode(account: self.account, presentationData: self.presentationData, call: { [weak self] peerId in + if let strongSelf = self { + strongSelf.call(peerId) + } + }, openInfo: { [weak self] peerId in + if let strongSelf = self { + let _ = (strongSelf.account.postbox.loadedPeerWithId(peerId) + |> take(1) + |> deliverOnMainQueue).start(next: { peer in + if let strongSelf = self { + if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { + (strongSelf.navigationController as? NavigationController)?.pushViewController(infoController) + } + } + }) + } + }) + self._ready.set(self.controllerNode.ready) + self.displayNodeDidLoad() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } + + @objc func callPressed() { + let controller = ContactSelectionController(account: self.account, title: { $0.Calls_NewCall }) + self.createActionDisposable.set((controller.result + |> take(1) + |> deliverOnMainQueue).start(next: { [weak controller, weak self] peerId in + controller?.dismissSearch() + if let strongSelf = self, let peerId = peerId { + strongSelf.call(peerId, began: { + if let strongSelf = self { + if let hasOngoingCall = strongSelf.account.telegramApplicationContext.hasOngoingCall { + let _ = (hasOngoingCall + |> filter { $0 } + |> timeout(1.0, queue: Queue.mainQueue(), alternate: .single(true)) + |> delay(0.5, queue: Queue.mainQueue()) + |> deliverOnMainQueue).start(next: { _ in + if let strongSelf = self, let controller = controller, let navigationController = controller.navigationController as? NavigationController { + if navigationController.viewControllers.last === controller { + let _ = navigationController.popViewController(animated: true) + } + } + }) + } + } + }) + } + })) + (self.navigationController as? NavigationController)?.pushViewController(controller) + } + + @objc func editPressed() { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + + self.controllerNode.updateState { state in + return state.withUpdatedEditing(true) + } + } + + @objc func donePressed() { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + + self.controllerNode.updateState { state in + return state.withUpdatedEditing(false) + } + } + + private func call(_ peerId: PeerId, began: (() -> Void)? = nil) { + let callResult = self.account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: false) + if let callResult = callResult { + if case let .alreadyInProgress(currentPeerId) = callResult { + if currentPeerId == peerId { + began?() + self.account.telegramApplicationContext.navigateToCurrentCall?() + } else { + let presentationData = self.presentationData + let _ = (self.account.postbox.modify { modifier -> (Peer?, Peer?) in + return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId)) + } |> deliverOnMainQueue).start(next: { [weak self] peer, current in + if let strongSelf = self, let peer = peer, let current = current { + strongSelf.present(standardTextAlertController(title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + if let strongSelf = self { + let _ = strongSelf.account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true) + began?() + } + })]), in: .window) + } + }) + } + } else { + began?() + } + } + } +} diff --git a/TelegramUI/CallListControllerNode.swift b/TelegramUI/CallListControllerNode.swift new file mode 100644 index 0000000000..8a9d2f4be9 --- /dev/null +++ b/TelegramUI/CallListControllerNode.swift @@ -0,0 +1,417 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import SwiftSignalKit + +private struct CallListNodeListViewTransition { + let callListView: CallListNodeView + let deleteItems: [ListViewDeleteItem] + let insertItems: [ListViewInsertItem] + let updateItems: [ListViewUpdateItem] + let options: ListViewDeleteAndInsertOptions + let scrollToItem: ListViewScrollToItem? + let stationaryItemRange: (Int, Int)? +} + +private extension CallListViewEntry { + var lowestIndex: MessageIndex { + switch self { + case let .hole(index): + return index + case let .message(_, messages): + var lowest = MessageIndex(messages[0]) + for i in 1 ..< messages.count { + let index = MessageIndex(messages[i]) + if index < lowest { + lowest = index + } + } + return lowest + } + } + + var highestIndex: MessageIndex { + switch self { + case let .hole(index): + return index + case let .message(_, messages): + var highest = MessageIndex(messages[0]) + for i in 1 ..< messages.count { + let index = MessageIndex(messages[i]) + if index > highest { + highest = index + } + } + return highest + } + } +} + +final class CallListNodeInteraction { + let setMessageIdWithRevealedOptions: (MessageId?, MessageId?) -> Void + let call: (PeerId) -> Void + let openInfo: (PeerId) -> Void + let delete: ([MessageId]) -> Void + + init(setMessageIdWithRevealedOptions: @escaping (MessageId?, MessageId?) -> Void, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void, delete: @escaping ([MessageId]) -> Void) { + self.setMessageIdWithRevealedOptions = setMessageIdWithRevealedOptions + self.call = call + self.openInfo = openInfo + self.delete = delete + } +} + +struct CallListNodeState: Equatable { + let theme: PresentationTheme + let strings: PresentationStrings + let editing: Bool + let messageIdWithRevealedOptions: MessageId? + + func withUpdatedPresentationData(theme: PresentationTheme, strings: PresentationStrings) -> CallListNodeState { + return CallListNodeState(theme: theme, strings: strings, editing: self.editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions) + } + + func withUpdatedEditing(_ editing: Bool) -> CallListNodeState { + return CallListNodeState(theme: self.theme, strings: self.strings, editing: editing, messageIdWithRevealedOptions: self.messageIdWithRevealedOptions) + } + + func withUpdatedMessageIdWithRevealedOptions(_ messageIdWithRevealedOptions: MessageId?) -> CallListNodeState { + return CallListNodeState(theme: self.theme, strings: self.strings, editing: self.editing, messageIdWithRevealedOptions: messageIdWithRevealedOptions) + } + + static func ==(lhs: CallListNodeState, rhs: CallListNodeState) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.editing != rhs.editing { + return false + } + if lhs.messageIdWithRevealedOptions != rhs.messageIdWithRevealedOptions { + return false + } + return true + } +} + +private func mappedInsertEntries(account: Account, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { + return entries.map { entry -> ListViewInsertItem in + switch entry.entry { + case let .messageEntry(topMessage, messages, theme, strings, editing, hasActiveRevealControls): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, account: account, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) + case let .holeEntry(theme): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint) + } + } +} + +private func mappedUpdateEntries(account: Account, nodeInteraction: CallListNodeInteraction, entries: [CallListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { + return entries.map { entry -> ListViewUpdateItem in + switch entry.entry { + case let .messageEntry(topMessage, messages, theme, strings, editing, hasActiveRevealControls): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: CallListCallItem(theme: theme, strings: strings, account: account, topMessage: topMessage, messages: messages, editing: editing, revealed: hasActiveRevealControls, interaction: nodeInteraction), directionHint: entry.directionHint) + case let .holeEntry(theme): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint) + } + } +} + +private func mappedCallListNodeViewListTransition(account: Account, nodeInteraction: CallListNodeInteraction, transition: CallListNodeViewTransition) -> CallListNodeListViewTransition { + return CallListNodeListViewTransition(callListView: transition.callListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, nodeInteraction: nodeInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, nodeInteraction: nodeInteraction, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange) +} + +private final class CallListOpaqueTransactionState { + let callListView: CallListNodeView + + init(callListView: CallListNodeView) { + self.callListView = callListView + } +} + +final class CallListControllerNode: ASDisplayNode { + private let account: Account + private var presentationData: PresentationData + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + private let _ready = ValuePromise() + private var didSetReady = false + var ready: Signal { + return _ready.get() + } + + var peerSelected: ((PeerId) -> Void)? + var activateSearch: (() -> Void)? + var deletePeerChat: ((PeerId) -> Void)? + + private let viewProcessingQueue = Queue() + private var callListView: CallListNodeView? + + private var dequeuedInitialTransitionOnLayout = false + private var enqueuedTransition: (CallListNodeListViewTransition, () -> Void)? + + private var currentState: CallListNodeState + private let statePromise: ValuePromise + + private var currentLocationAndType = CallListNodeLocationAndType(location: .initial(count: 50), type: .all) + private let callListLocationAndType = ValuePromise() + private let callListDisposable = MetaDisposable() + + private let listNode: ListView + + private let call: (PeerId) -> Void + private let openInfo: (PeerId) -> Void + + init(account: Account, presentationData: PresentationData, call: @escaping (PeerId) -> Void, openInfo: @escaping (PeerId) -> Void) { + self.account = account + self.presentationData = presentationData + self.call = call + self.openInfo = openInfo + + self.currentState = CallListNodeState(theme: presentationData.theme, strings: presentationData.strings, editing: false, messageIdWithRevealedOptions: nil) + self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) + + self.listNode = ListView() + + super.init(viewBlock: { + return UITracingLayerView() + }, didLoad: nil) + + self.addSubnode(self.listNode) + + self.backgroundColor = presentationData.theme.chatList.backgroundColor + + let nodeInteraction = CallListNodeInteraction(setMessageIdWithRevealedOptions: { [weak self] messageId, fromMessageId in + if let strongSelf = self { + strongSelf.updateState { state in + if (messageId == nil && fromMessageId == state.messageIdWithRevealedOptions) || (messageId != nil && fromMessageId == nil) { + return state.withUpdatedMessageIdWithRevealedOptions(messageId) + } else { + return state + } + } + } + }, call: { [weak self] peerId in + self?.call(peerId) + }, openInfo: { [weak self] peerId in + self?.openInfo(peerId) + }, delete: { [weak self] messageIds in + if let strongSelf = self { + let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: messageIds, type: .forLocalPeer).start() + } + }) + + let viewProcessingQueue = self.viewProcessingQueue + + let callListViewUpdate = self.callListLocationAndType.get() + |> distinctUntilChanged + |> mapToSignal { locationAndType in + return callListViewForLocationAndType(locationAndType: locationAndType, account: account) + } + + let previousView = Atomic(value: nil) + + let callListNodeViewTransition = combineLatest(callListViewUpdate, self.statePromise.get()) |> mapToQueue { (update, state) -> Signal in + let processedView = CallListNodeView(originalView: update.view, filteredEntries: callListNodeEntriesForView(update.view, state: state)) + let previous = previousView.swap(processedView) + + let reason: CallListNodeViewTransitionReason + var prepareOnMainQueue = false + + var previousWasEmptyOrSingleHole = false + if let previous = previous { + if previous.filteredEntries.count == 1 { + if case .holeEntry = previous.filteredEntries[0] { + previousWasEmptyOrSingleHole = true + } + } + } else { + previousWasEmptyOrSingleHole = true + } + + if previousWasEmptyOrSingleHole { + reason = .initial + if previous == nil { + prepareOnMainQueue = true + } + } else { + switch update.type { + case .InitialUnread: + reason = .initial + prepareOnMainQueue = true + case .Generic: + reason = .interactiveChanges + case .UpdateVisible: + reason = .reload + case .FillHole: + reason = .reload + } + } + + return preparedCallListNodeViewTransition(from: previous, to: processedView, reason: reason, account: account, scrollPosition: update.scrollPosition) + |> map({ mappedCallListNodeViewListTransition(account: account, nodeInteraction: nodeInteraction, transition: $0) }) + |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) + } + + let appliedTransition = callListNodeViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in + if let strongSelf = self { + return strongSelf.enqueueTransition(transition) + } + return .complete() + } + + self.listNode.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in + if let strongSelf = self, let range = range.loadedRange, let view = (transactionOpaqueState as? CallListOpaqueTransactionState)?.callListView.originalView { + var location: CallListNodeLocation? + if range.firstIndex < 5 && view.later != nil { + location = .navigation(index: view.entries[view.entries.count - 1].highestIndex) + } else if range.firstIndex >= 5 && range.lastIndex >= view.entries.count - 5 && view.earlier != nil { + location = .navigation(index: view.entries[0].lowestIndex) + } + + if let location = location, location != strongSelf.currentLocationAndType.location { + strongSelf.currentLocationAndType = CallListNodeLocationAndType(location: location, type: strongSelf.currentLocationAndType.type) + strongSelf.callListLocationAndType.set(strongSelf.currentLocationAndType) + } + } + } + + self.callListDisposable.set(appliedTransition.start()) + + self.callListLocationAndType.set(self.currentLocationAndType) + } + + deinit { + self.callListDisposable.dispose() + } + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if theme !== self.currentState.theme || strings !== self.currentState.strings { + self.backgroundColor = theme.chatList.backgroundColor + + self.updateState { + return $0.withUpdatedPresentationData(theme: theme, strings: strings) + } + } + } + + func updateState(_ f: (CallListNodeState) -> CallListNodeState) { + let state = f(self.currentState) + if state != self.currentState { + self.currentState = state + self.statePromise.set(state) + } + } + + func updateType(_ type: CallListViewType) { + if type != self.currentLocationAndType.type { + if let view = self.callListView?.originalView, !view.entries.isEmpty { + self.currentLocationAndType = CallListNodeLocationAndType(location: .changeType(index: view.entries[view.entries.count - 1].highestIndex), type: type) + self.callListLocationAndType.set(self.currentLocationAndType) + } + } + } + + private func enqueueTransition(_ transition: CallListNodeListViewTransition) -> Signal { + return Signal { [weak self] subscriber in + if let strongSelf = self { + if let _ = strongSelf.enqueuedTransition { + preconditionFailure() + } + + strongSelf.enqueuedTransition = (transition, { + subscriber.putCompletion() + }) + + if strongSelf.isNodeLoaded { + strongSelf.dequeueTransition() + } else { + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf._ready.set(true) + } + } + } else { + subscriber.putCompletion() + } + + return EmptyDisposable + } |> runOn(Queue.mainQueue()) + } + + private func dequeueTransition() { + if let (transition, completion) = self.enqueuedTransition { + self.enqueuedTransition = nil + + let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in + if let strongSelf = self { + strongSelf.callListView = transition.callListView + + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf._ready.set(true) + } + + completion() + } + } + + self.listNode.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: CallListOpaqueTransactionState(callListView: transition.callListView), completion: completion) + } + } + + func scrollToLatest() { + if let view = self.callListView?.originalView, view.later == nil { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } else { + let location: CallListNodeLocation = .scroll(index: MessageIndex.absoluteUpperBound(), sourceIndex: MessageIndex.absoluteLowerBound(), scrollPosition: .Top, animated: true) + self.currentLocationAndType = CallListNodeLocationAndType(location: location, type: self.currentLocationAndType.type) + self.callListLocationAndType.set(self.currentLocationAndType) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.input]) + insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default + } + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !self.dequeuedInitialTransitionOnLayout { + self.dequeuedInitialTransitionOnLayout = true + self.dequeueTransition() + } + } +} diff --git a/TelegramUI/CallListNodeEntries.swift b/TelegramUI/CallListNodeEntries.swift new file mode 100644 index 0000000000..6b0738882c --- /dev/null +++ b/TelegramUI/CallListNodeEntries.swift @@ -0,0 +1,128 @@ +import Foundation +import Postbox +import TelegramCore + +enum CallListNodeEntryId: Hashable { + case hole(MessageIndex) + case message(MessageIndex) + + var hashValue: Int { + switch self { + case let .hole(index): + return index.hashValue + case let .message(index): + return index.hashValue + } + } + + static func <(lhs: CallListNodeEntryId, rhs: CallListNodeEntryId) -> Bool { + return lhs.hashValue < rhs.hashValue + } + + static func ==(lhs: CallListNodeEntryId, rhs: CallListNodeEntryId) -> Bool { + switch lhs { + case let .hole(index): + if case .hole(index) = rhs { + return true + } else { + return false + } + case let .message(index): + if case .message(index) = rhs { + return true + } else { + return false + } + } + } +} + +private func areMessagesEqual(_ lhsMessage: Message, _ rhsMessage: Message) -> Bool { + if lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } + if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags { + return false + } + return true +} + +enum CallListNodeEntry: Comparable, Identifiable { + case messageEntry(topMessage: Message, messages: [Message], theme: PresentationTheme, strings: PresentationStrings, editing: Bool, hasActiveRevealControls: Bool) + case holeEntry(index: MessageIndex, theme: PresentationTheme) + + var index: MessageIndex { + switch self { + case let .messageEntry(message, _, _, _, _, _): + return MessageIndex(message) + case let .holeEntry(index, _): + return index + } + } + + var stableId: CallListNodeEntryId { + switch self { + case let .messageEntry(message, _, _, _, _, _): + return .message(MessageIndex(message)) + case let .holeEntry(index, _): + return .hole(index) + } + } + + static func <(lhs: CallListNodeEntry, rhs: CallListNodeEntry) -> Bool { + return lhs.index < rhs.index + } + + static func ==(lhs: CallListNodeEntry, rhs: CallListNodeEntry) -> Bool { + switch lhs { + case let .messageEntry(lhsMessage, lhsMessages, lhsTheme, lhsStrings, lhsEditing, lhsHasRevealControls): + if case let .messageEntry(rhsMessage, rhsMessages, rhsTheme, rhsStrings, rhsEditing, rhsHasRevealControls) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if lhsEditing != rhsEditing { + return false + } + if lhsHasRevealControls != rhsHasRevealControls { + return false + } + if !areMessagesEqual(lhsMessage, rhsMessage) { + return false + } + if lhsMessages.count != rhsMessages.count { + return false + } + for i in 0 ..< lhsMessages.count { + if !areMessagesEqual(lhsMessages[i], rhsMessages[i]) { + return false + } + } + return true + } else { + return false + } + case let .holeEntry(lhsIndex, lhsTheme): + if case let .holeEntry(rhsIndex, rhsTheme) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme { + return true + } else { + return false + } + } + } +} + +func callListNodeEntriesForView(_ view: CallListView, state: CallListNodeState) -> [CallListNodeEntry] { + var result: [CallListNodeEntry] = [] + for entry in view.entries { + switch entry { + case let .message(topMessage, messages): + result.append(.messageEntry(topMessage: topMessage, messages: messages, theme: state.theme, strings: state.strings, editing: state.editing, hasActiveRevealControls: state.messageIdWithRevealedOptions == topMessage.id)) + case let .hole(index): + result.append(.holeEntry(index: index, theme: state.theme)) + } + } + return result +} diff --git a/TelegramUI/CallListNodeLocation.swift b/TelegramUI/CallListNodeLocation.swift new file mode 100644 index 0000000000..0001c6261e --- /dev/null +++ b/TelegramUI/CallListNodeLocation.swift @@ -0,0 +1,83 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +enum CallListNodeLocation: Equatable { + case initial(count: Int) + case changeType(index: MessageIndex) + case navigation(index: MessageIndex) + case scroll(index: MessageIndex, sourceIndex: MessageIndex, scrollPosition: ListViewScrollPosition, animated: Bool) + + static func ==(lhs: CallListNodeLocation, rhs: CallListNodeLocation) -> Bool { + switch lhs { + case let .navigation(index): + switch rhs { + case .navigation(index): + return true + default: + return false + } + default: + return false + } + } +} + +struct CallListNodeLocationAndType: Equatable { + let location: CallListNodeLocation + let type: CallListViewType + + static func ==(lhs: CallListNodeLocationAndType, rhs: CallListNodeLocationAndType) -> Bool { + return lhs.location == rhs.location && lhs.type == rhs.type + } +} + +struct CallListNodeViewUpdate { + let view: CallListView + let type: ViewUpdateType + let scrollPosition: CallListNodeViewScrollPosition? +} + +func callListViewForLocationAndType(locationAndType: CallListNodeLocationAndType, account: Account) -> Signal { + switch locationAndType.location { + case let .initial(count): + return account.viewTracker.callListView(type: locationAndType.type, index: MessageIndex.absoluteUpperBound(), count: count) |> map { view -> CallListNodeViewUpdate in + return CallListNodeViewUpdate(view: view, type: .Generic, scrollPosition: nil) + } + case let .changeType(index): + return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in + let genericType: ViewUpdateType + genericType = .Generic + return CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil) + } + case let .navigation(index): + var first = true + return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in + let genericType: ViewUpdateType + if first { + first = false + genericType = .UpdateVisible + } else { + genericType = .Generic + } + return CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil) + } + case let .scroll(index, sourceIndex, scrollPosition, animated): + let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up + let callScrollPosition: CallListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) + var first = true + return account.viewTracker.callListView(type: locationAndType.type, index: index, count: 120) |> map { view -> CallListNodeViewUpdate in + let genericType: ViewUpdateType + let scrollPosition: CallListNodeViewScrollPosition? = first ? callScrollPosition : nil + if first { + first = false + genericType = .UpdateVisible + } else { + genericType = .Generic + } + return CallListNodeViewUpdate(view: view, type: genericType, scrollPosition: scrollPosition) + } + } +} diff --git a/TelegramUI/CallListViewTransition.swift b/TelegramUI/CallListViewTransition.swift new file mode 100644 index 0000000000..15fe618ef8 --- /dev/null +++ b/TelegramUI/CallListViewTransition.swift @@ -0,0 +1,170 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +struct CallListNodeView { + let originalView: CallListView + let filteredEntries: [CallListNodeEntry] +} + +enum CallListNodeViewTransitionReason { + case initial + case interactiveChanges + case holeChanges(filledHoleDirections: [MessageIndex: HoleFillDirection], removeHoleDirections: [MessageIndex: HoleFillDirection]) + case reload +} + +struct CallListNodeViewTransitionInsertEntry { + let index: Int + let previousIndex: Int? + let entry: CallListNodeEntry + let directionHint: ListViewItemOperationDirectionHint? +} + +struct CallListNodeViewTransitionUpdateEntry { + let index: Int + let previousIndex: Int + let entry: CallListNodeEntry + let directionHint: ListViewItemOperationDirectionHint? +} + +struct CallListNodeViewTransition { + let callListView: CallListNodeView + let deleteItems: [ListViewDeleteItem] + let insertEntries: [CallListNodeViewTransitionInsertEntry] + let updateEntries: [CallListNodeViewTransitionUpdateEntry] + let options: ListViewDeleteAndInsertOptions + let scrollToItem: ListViewScrollToItem? + let stationaryItemRange: (Int, Int)? +} + +enum CallListNodeViewScrollPosition { + case index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) +} + +func preparedCallListNodeViewTransition(from fromView: CallListNodeView?, to toView: CallListNodeView, reason: CallListNodeViewTransitionReason, account: Account, scrollPosition: CallListNodeViewScrollPosition?) -> Signal { + return Signal { subscriber in + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) + + var adjustedDeleteIndices: [ListViewDeleteItem] = [] + let previousCount: Int + if let fromView = fromView { + previousCount = fromView.filteredEntries.count + } else { + previousCount = 0; + } + for index in deleteIndices { + adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil)) + } + var adjustedIndicesAndItems: [CallListNodeViewTransitionInsertEntry] = [] + var adjustedUpdateItems: [CallListNodeViewTransitionUpdateEntry] = [] + let updatedCount = toView.filteredEntries.count + + var options: ListViewDeleteAndInsertOptions = [] + var maxAnimatedInsertionIndex = -1 + var stationaryItemRange: (Int, Int)? + var scrollToItem: ListViewScrollToItem? + + switch reason { + case .initial: + let _ = options.insert(.LowLatency) + let _ = options.insert(.Synchronous) + case .interactiveChanges: + let _ = options.insert(.AnimateAlpha) + let _ = options.insert(.AnimateInsertion) + + for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) { + let adjustedIndex = updatedCount - 1 - index + if adjustedIndex == maxAnimatedInsertionIndex + 1 { + maxAnimatedInsertionIndex += 1 + } + } + case .reload: + break + case let .holeChanges(filledHoleDirections, removeHoleDirections): + if let (_, removeDirection) = removeHoleDirections.first { + switch removeDirection { + case .LowerToUpper: + var holeIndex: MessageIndex? + for (index, _) in filledHoleDirections { + if holeIndex == nil || index < holeIndex! { + holeIndex = index + } + } + + if let holeIndex = holeIndex { + for i in 0 ..< toView.filteredEntries.count { + if toView.filteredEntries[i].index >= holeIndex { + let index = toView.filteredEntries.count - 1 - (i - 1) + stationaryItemRange = (index, Int.max) + break + } + } + } + case .UpperToLower: + break + case .AroundIndex: + break + } + } + } + + for (index, entry, previousIndex) in indicesAndItems { + let adjustedIndex = updatedCount - 1 - index + + let adjustedPrevousIndex: Int? + if let previousIndex = previousIndex { + adjustedPrevousIndex = previousCount - 1 - previousIndex + } else { + adjustedPrevousIndex = nil + } + + var directionHint: ListViewItemOperationDirectionHint? + if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex { + directionHint = .Down + } + + adjustedIndicesAndItems.append(CallListNodeViewTransitionInsertEntry(index: adjustedIndex, previousIndex: adjustedPrevousIndex, entry: entry, directionHint: directionHint)) + } + + for (index, entry, previousIndex) in updateIndices { + let adjustedIndex = updatedCount - 1 - index + let adjustedPreviousIndex = previousCount - 1 - previousIndex + + let directionHint: ListViewItemOperationDirectionHint? = nil + adjustedUpdateItems.append(CallListNodeViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint)) + } + + if let scrollPosition = scrollPosition { + switch scrollPosition { + case let .index(scrollIndex, position, directionHint, animated): + var index = toView.filteredEntries.count - 1 + for entry in toView.filteredEntries { + if entry.index >= scrollIndex { + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + break + } + index -= 1 + } + + if scrollToItem == nil { + var index = 0 + for entry in toView.filteredEntries.reversed() { + if entry.index < scrollIndex { + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + break + } + index += 1 + } + } + } + } + + subscriber.putNext(CallListNodeViewTransition(callListView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange)) + subscriber.putCompletion() + + return EmptyDisposable + } +} diff --git a/TelegramUI/ChangePhoneNumberCodeController.swift b/TelegramUI/ChangePhoneNumberCodeController.swift index bc5d59e296..15ee880c6f 100644 --- a/TelegramUI/ChangePhoneNumberCodeController.swift +++ b/TelegramUI/ChangePhoneNumberCodeController.swift @@ -38,8 +38,8 @@ private enum ChangePhoneNumberCodeTag: ItemListItemTag { } private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry { - case codeEntry(String) - case codeInfo(String) + case codeEntry(PresentationTheme, String) + case codeInfo(PresentationTheme, String) var section: ItemListSectionId { return ChangePhoneNumberCodeSection.code.rawValue @@ -56,14 +56,14 @@ private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry { static func ==(lhs: ChangePhoneNumberCodeEntry, rhs: ChangePhoneNumberCodeEntry) -> Bool { switch lhs { - case let .codeEntry(text): - if case .codeEntry(text) = rhs { + case let .codeEntry(lhsTheme, lhsText): + if case let .codeEntry(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .codeInfo(text): - if case .codeInfo(text) = rhs { + case let .codeInfo(lhsTheme, lhsText): + if case let .codeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -77,14 +77,14 @@ private enum ChangePhoneNumberCodeEntry: ItemListNodeEntry { func item(_ arguments: ChangePhoneNumberCodeControllerArguments) -> ListViewItem { switch self { - case let .codeEntry(text): - return ItemListSingleLineInputItem(title: NSAttributedString(string: "Code", textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ChangePhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in + case let .codeEntry(theme, text): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: "Code", textColor: .black), text: text, placeholder: "", type: .number, spacing: 10.0, tag: ChangePhoneNumberCodeTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) - case let .codeInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) + case let .codeInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } @@ -126,15 +126,15 @@ private struct ChangePhoneNumberCodeControllerState: Equatable { } } -private func changePhoneNumberCodeControllerEntries(state: ChangePhoneNumberCodeControllerState, codeData: ChangeAccountPhoneNumberData, timeout: Int32?) -> [ChangePhoneNumberCodeEntry] { +private func changePhoneNumberCodeControllerEntries(presentationData: PresentationData, state: ChangePhoneNumberCodeControllerState, codeData: ChangeAccountPhoneNumberData, timeout: Int32?) -> [ChangePhoneNumberCodeEntry] { var entries: [ChangePhoneNumberCodeEntry] = [] - entries.append(.codeEntry(state.codeText)) + entries.append(.codeEntry(presentationData.theme, state.codeText)) var text = authorizationCurrentOptionText(codeData.type).string if let nextType = codeData.nextType { text += "\n\n" + authorizationNextOptionText(nextType, timeout: timeout).string } - entries.append(.codeInfo(text)) + entries.append(.codeInfo(presentationData.theme, text)) return entries } @@ -260,8 +260,9 @@ func changePhoneNumberCodeController(account: Account, phoneNumber: String, code checkCode() }) - let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, currentDataPromise.get() |> deliverOnMainQueue, timeout.get() |> deliverOnMainQueue) - |> map { state, data, timeout -> (ItemListControllerState, (ItemListNodeState, ChangePhoneNumberCodeEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, currentDataPromise.get() |> deliverOnMainQueue, timeout.get() |> deliverOnMainQueue) + |> deliverOnMainQueue + |> map { presentationData, state, data, timeout -> (ItemListControllerState, (ItemListNodeState, ChangePhoneNumberCodeEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? if state.checking { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) @@ -270,21 +271,20 @@ func changePhoneNumberCodeController(account: Account, phoneNumber: String, code if state.codeText.isEmpty { nextEnabled = false } - rightNavigationButton = ItemListNavigationButton(title: "Next", style: .bold, enabled: nextEnabled, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: nextEnabled, action: { checkCode() }) } - let controllerState = ItemListControllerState(title: .text(formatPhoneNumber(phoneNumber)), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: false) - let listState = ItemListNodeState(entries: changePhoneNumberCodeControllerEntries(state: state, codeData: data, timeout: timeout), style: .blocks, focusItemTag: ChangePhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(formatPhoneNumber(phoneNumber)), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: changePhoneNumberCodeControllerEntries(presentationData: presentationData, state: state, codeData: data, timeout: timeout), style: .blocks, focusItemTag: ChangePhoneNumberCodeTag.input, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { diff --git a/TelegramUI/ChangePhoneNumberController.swift b/TelegramUI/ChangePhoneNumberController.swift index 74cb82949f..5d4077e8e6 100644 --- a/TelegramUI/ChangePhoneNumberController.swift +++ b/TelegramUI/ChangePhoneNumberController.swift @@ -29,14 +29,20 @@ final class ChangePhoneNumberController: ViewController { private let hapticFeedback = HapticFeedback() + private var presentationData: PresentationData + init(account: Account) { self.account = account - super.init(navigationBar: NavigationBar()) + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.title = "Change Number" - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.nextPressed)) - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.title = self.presentationData.strings.ChangePhoneNumberNumber_Title + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) } required init(coder aDecoder: NSCoder) { @@ -57,7 +63,7 @@ final class ChangePhoneNumberController: ViewController { } override public func loadDisplayNode() { - self.displayNode = ChangePhoneNumberControllerNode() + self.displayNode = ChangePhoneNumberControllerNode(presentationData: self.presentationData) self.displayNodeDidLoad() self.controllerNode.selectCountryCode = { [weak self] in if let strongSelf = self { diff --git a/TelegramUI/ChangePhoneNumberControllerNode.swift b/TelegramUI/ChangePhoneNumberControllerNode.swift index 23464d001c..68950b023c 100644 --- a/TelegramUI/ChangePhoneNumberControllerNode.swift +++ b/TelegramUI/ChangePhoneNumberControllerNode.swift @@ -17,7 +17,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 45.0, height: context.closePath() context.fillPath() - context.setStrokeColor(UIColor(0xbcbbc1).cgColor) + context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor) context.setLineWidth(lineWidth) context.move(to: CGPoint(x: size.width, y: size.height - arrowSize - lineWidth / 2.0)) @@ -35,7 +35,7 @@ private let countryButtonBackground = generateImage(CGSize(width: 45.0, height: private let countryButtonHighlightedBackground = generateImage(CGSize(width: 45.0, height: 44.0 + 6.0), rotatedContext: { size, context in let arrowSize: CGFloat = 6.0 context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0xbcbbc1).cgColor) + context.setFillColor(UIColor(rgb: 0xbcbbc1).cgColor) context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height - arrowSize))) context.move(to: CGPoint(x: size.width, y: size.height - arrowSize)) context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - arrowSize)) @@ -50,7 +50,7 @@ private let phoneInputBackground = generateImage(CGSize(width: 60.0, height: 44. context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.white.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0xbcbbc1).cgColor) + context.setStrokeColor(UIColor(rgb: 0xbcbbc1).cgColor) context.setLineWidth(lineWidth) context.move(to: CGPoint(x: 0.0, y: size.height - lineWidth / 2.0)) context.addLine(to: CGPoint(x: size.width, y: size.height - lineWidth / 2.0)) @@ -89,16 +89,20 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode { } } - override init() { + var presentationData: PresentationData + + init(presentationData: PresentationData) { + self.presentationData = presentationData + self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true self.titleNode.displaysAsynchronously = false - self.titleNode.attributedText = NSAttributedString(string: "NEW NUMBER", font: Font.regular(14.0), textColor: UIColor(0x6d6d72)) + self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChangePhoneNumberNumber_NewNumber, font: Font.regular(14.0), textColor: self.presentationData.theme.list.sectionHeaderTextColor) self.noticeNode = ASTextNode() self.noticeNode.isLayerBacked = true self.noticeNode.displaysAsynchronously = false - self.noticeNode.attributedText = NSAttributedString(string: "We will send an SMS with a confirmation code to your new number.", font: Font.regular(14.0), textColor: UIColor(0x6d6d72)) + self.noticeNode.attributedText = NSAttributedString(string: self.presentationData.strings.ChangePhoneNumberNumber_Help, font: Font.regular(14.0), textColor: self.presentationData.theme.list.freeTextColor) self.countryButton = ASButtonNode() self.countryButton.setBackgroundImage(countryButtonBackground, for: []) @@ -116,7 +120,7 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode { return UITracingLayerView() }, didLoad: nil) - self.backgroundColor = UIColor(0xefefef) + self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor self.addSubnode(self.titleNode) self.addSubnode(self.noticeNode) @@ -127,7 +131,7 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode { self.countryButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 15.0, bottom: 4.0, right: 0.0) self.countryButton.contentHorizontalAlignment = .left - self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: "Your phone number", font: Font.regular(17.0), textColor: UIColor(0xbcbcc3)) + self.phoneInputNode.numberField.textField.attributedPlaceholder = NSAttributedString(string: self.presentationData.strings.Login_PhonePlaceholder, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPlaceholderTextColor) self.countryButton.addTarget(self, action: #selector(self.countryPressed), forControlEvents: .touchUpInside) @@ -136,7 +140,7 @@ final class ChangePhoneNumberControllerNode: ASDisplayNode { if let code = Int(code), let countryName = countryCodeToName[code] { strongSelf.countryButton.setTitle(countryName, with: Font.regular(17.0), with: .black, for: []) } else { - strongSelf.countryButton.setTitle("Select Country", with: Font.regular(17.0), with: .black, for: []) + strongSelf.countryButton.setTitle(strongSelf.presentationData.strings.Login_CountryCode, with: Font.regular(17.0), with: .black, for: []) } } } diff --git a/TelegramUI/ChangePhoneNumberIntroController.swift b/TelegramUI/ChangePhoneNumberIntroController.swift index 83873d35fa..b9bb858752 100644 --- a/TelegramUI/ChangePhoneNumberIntroController.swift +++ b/TelegramUI/ChangePhoneNumberIntroController.swift @@ -4,6 +4,8 @@ import AsyncDisplayKit import TelegramCore private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode { + var presentationData: PresentationData + let iconNode: ASImageNode let labelNode: ASTextNode let buttonNode: HighlightableButtonNode @@ -11,7 +13,9 @@ private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode { var dismiss: (() -> Void)? var action: (() -> Void)? - override init() { + init(presentationData: PresentationData) { + self.presentationData = presentationData + self.iconNode = ASImageNode() self.labelNode = ASTextNode() self.buttonNode = HighlightableButtonNode() @@ -20,16 +24,17 @@ private final class ChangePhoneNumberIntroControllerNode: ASDisplayNode { return UITracingLayerView() }, didLoad: nil) - self.iconNode.image = UIImage(bundleImageName: "Settings/ChangePhoneIntroIcon")?.precomposed() - self.labelNode.attributedText = NSAttributedString(string: "You can change your Telegram number here. Your account and all your cloud data — messages, media, contacts, etc. will be moved to the new number.\n\nImportant: all your Telegram contacts will get your new number added to their address book, provided they had your old number and you haven't blocked them in Telegram.", font: Font.regular(14.0), textColor: UIColor(0x6d6d72), paragraphAlignment: .center) - self.buttonNode.setTitle("Change Number", with: Font.regular(19.0), with: UIColor(0x007ee5), for: .normal) + self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Settings/ChangePhoneIntroIcon"), color: presentationData.theme.list.freeTextColor) + let textColor = self.presentationData.theme.list.freeTextColor + self.labelNode.attributedText = parseMarkdownIntoAttributedString(self.presentationData.strings.PhoneNumberHelp_Help, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: textColor), bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: textColor), link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: textColor), linkAttribute: { _ in return nil }), textAlignment: .center) + self.buttonNode.setTitle(self.presentationData.strings.PhoneNumberHelp_ChangeNumber, with: Font.regular(19.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal) self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) self.addSubnode(self.iconNode) self.addSubnode(self.labelNode) self.addSubnode(self.buttonNode) - self.backgroundColor = UIColor(0xefeff4) + self.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor } func animateIn() { @@ -72,13 +77,19 @@ final class ChangePhoneNumberIntroController: ViewController { private let account: Account private var didPlayPresentationAnimation = false + private var presentationData: PresentationData + init(account: Account, phoneNumber: String) { self.account = account - super.init(navigationBar: NavigationBar()) + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style self.title = phoneNumber - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) //self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed)) } @@ -87,7 +98,7 @@ final class ChangePhoneNumberIntroController: ViewController { } override func loadDisplayNode() { - self.displayNode = ChangePhoneNumberIntroControllerNode() + self.displayNode = ChangePhoneNumberIntroControllerNode(presentationData: self.presentationData) (self.displayNode as! ChangePhoneNumberIntroControllerNode).dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) } @@ -117,7 +128,7 @@ final class ChangePhoneNumberIntroController: ViewController { } func proceed() { - self.present(standardTextAlertController(title: nil, text: "All your Telegram contacts will get your new number added to their address book, provided they had your old number and you haven't blocked them in Telegram.", actions: [TextAlertAction(type: .defaultAction, title: "Cancel", action: {}), TextAlertAction(type: .genericAction, title: "OK", action: { [weak self] in + self.present(standardTextAlertController(title: nil, text: self.presentationData.strings.PhoneNumberHelp_Alert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChangePhoneNumberController(account: strongSelf.account), animated: true) } diff --git a/TelegramUI/ChannelAdminController.swift b/TelegramUI/ChannelAdminController.swift new file mode 100644 index 0000000000..4c111ac81f --- /dev/null +++ b/TelegramUI/ChannelAdminController.swift @@ -0,0 +1,478 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class ChannelAdminControllerArguments { + let account: Account + let toggleRight: (TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags) -> Void + let dismissAdmin: () -> Void + + init(account: Account, toggleRight: @escaping (TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags) -> Void, dismissAdmin: @escaping () -> Void) { + self.account = account + self.toggleRight = toggleRight + self.dismissAdmin = dismissAdmin + } +} + +private enum ChannelAdminSection: Int32 { + case info + case rights + case dismiss +} + +private enum ChannelAdminEntryStableId: Hashable { + case info + case right(TelegramChannelAdminRightsFlags) + case dismiss + + var hashValue: Int { + switch self { + case .info: + return 0 + case .dismiss: + return 1 + case let .right(flags): + return flags.rawValue.hashValue + } + } + + static func ==(lhs: ChannelAdminEntryStableId, rhs: ChannelAdminEntryStableId) -> Bool { + switch lhs { + case .info: + if case .info = rhs { + return true + } else { + return false + } + case let right(flags): + if case .right(flags) = rhs { + return true + } else { + return false + } + case .dismiss: + if case .dismiss = rhs { + return true + } else { + return false + } + } + } +} + +private enum ChannelAdminEntry: ItemListNodeEntry { + case info(PresentationTheme, PresentationStrings, Peer, TelegramUserPresence?) + case rightItem(PresentationTheme, Int, String, TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags, Bool, Bool) + case dismiss(PresentationTheme, String) + + var section: ItemListSectionId { + switch self { + case .info: + return ChannelAdminSection.info.rawValue + case .rightItem: + return ChannelAdminSection.rights.rawValue + case .dismiss: + return ChannelAdminSection.dismiss.rawValue + } + } + + var stableId: ChannelAdminEntryStableId { + switch self { + case .info: + return .info + case let .rightItem(_, _, _, right, _, _, _): + return .right(right) + case .dismiss: + return .dismiss + } + } + + static func ==(lhs: ChannelAdminEntry, rhs: ChannelAdminEntry) -> Bool { + switch lhs { + case let .info(lhsTheme, lhsStrings, lhsPeer, lhsPresence): + if case let .info(rhsTheme, rhsStrings, rhsPeer, rhsPresence) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if !arePeersEqual(lhsPeer, rhsPeer) { + return false + } + if lhsPresence != rhsPresence { + return false + } + + return true + } else { + return false + } + case let .rightItem(lhsTheme, lhsIndex, lhsText, lhsRight, lhsFlags, lhsValue, lhsEnabled): + if case let .rightItem(rhsTheme, rhsIndex, rhsText, rhsRight, rhsFlags, rhsValue, rhsEnabled) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsIndex != rhsIndex { + return false + } + if lhsText != rhsText { + return false + } + if lhsRight != rhsRight { + return false + } + if lhsFlags != rhsFlags { + return false + } + if lhsValue != rhsValue { + return false + } + if lhsEnabled != rhsEnabled { + return false + } + return true + } else { + return false + } + case let .dismiss(lhsTheme, lhsText): + if case let .dismiss(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + } + } + + static func <(lhs: ChannelAdminEntry, rhs: ChannelAdminEntry) -> Bool { + switch lhs { + case .info: + switch rhs { + case .info: + return false + default: + return true + } + case let .rightItem(_, lhsIndex, _, _, _, _, _): + switch rhs { + case .info: + return false + case let .rightItem(_, rhsIndex, _, _, _, _, _): + return lhsIndex < rhsIndex + default: + return true + } + case .dismiss: + return false + } + } + + func item(_ arguments: ChannelAdminControllerArguments) -> ListViewItem { + switch self { + case let .info(theme, strings, peer, presence): + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: presence, cachedData: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true), editingNameUpdated: { _ in + }, avatarTapped: { + }) + case let .rightItem(theme, _, text, right, flags, value, enabled): + return ItemListSwitchItem(theme: theme, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { _ in + arguments.toggleRight(right, flags) + }) + case let .dismiss(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + arguments.dismissAdmin() + }, tag: nil) + } + } +} + +private struct ChannelAdminControllerState: Equatable { + let updatedFlags: TelegramChannelAdminRightsFlags? + let updating: Bool + + init(updatedFlags: TelegramChannelAdminRightsFlags? = nil, updating: Bool = false) { + self.updatedFlags = updatedFlags + self.updating = updating + } + + static func ==(lhs: ChannelAdminControllerState, rhs: ChannelAdminControllerState) -> Bool { + if lhs.updatedFlags != rhs.updatedFlags { + return false + } + if lhs.updating != rhs.updating { + return false + } + return true + } + + func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChannelAdminRightsFlags?) -> ChannelAdminControllerState { + return ChannelAdminControllerState(updatedFlags: updatedFlags, updating: self.updating) + } + + func withUpdatedUpdating(_ updating: Bool) -> ChannelAdminControllerState { + return ChannelAdminControllerState(updatedFlags: self.updatedFlags, updating: updating) + } +} + +private func stringForRight(strings: PresentationStrings, right: TelegramChannelAdminRightsFlags, isGroup: Bool) -> String { + if right.contains(.canChangeInfo) { + return isGroup ? strings.Group_EditAdmin_PermissionChangeInfo : strings.Channel_EditAdmin_PermissionChangeInfo + } else if right.contains(.canPostMessages) { + return strings.Channel_EditAdmin_PermissionPostMessages + } else if right.contains(.canEditMessages) { + return strings.Channel_EditAdmin_PermissionEditMessages + } else if right.contains(.canDeleteMessages) { + return strings.Channel_EditAdmin_PermissionDeleteMessages + } else if right.contains(.canBanUsers) { + return strings.Channel_EditAdmin_PermissionBanUsers + } else if right.contains(.canInviteUsers) { + return strings.Channel_EditAdmin_PermissionInviteUsers + } else if right.contains(.canChangeInviteLink) { + return strings.Channel_EditAdmin_PermissionChangeInviteLink + } else if right.contains(.canPinMessages) { + return strings.Channel_EditAdmin_PermissionPinMessages + } else if right.contains(.canAddAdmins) { + return strings.Channel_EditAdmin_PermissionAddAdmins + } else { + return "" + } +} + +private func rightDependencies(_ right: TelegramChannelAdminRightsFlags) -> [TelegramChannelAdminRightsFlags] { + if right.contains(.canChangeInfo) { + return [] + } else if right.contains(.canPostMessages) { + return [] + } else if right.contains(.canEditMessages) { + return [] + } else if right.contains(.canDeleteMessages) { + return [] + } else if right.contains(.canBanUsers) { + return [] + } else if right.contains(.canInviteUsers) { + return [] + } else if right.contains(.canChangeInviteLink) { + return [.canInviteUsers] + } else if right.contains(.canPinMessages) { + return [] + } else if right.contains(.canAddAdmins) { + return [] + } else { + return [] + } +} + +private func canEditAdminRights(accountPeerId: PeerId, channelView: PeerView, initialParticipant: ChannelParticipant?) -> Bool { + if let channel = channelView.peers[channelView.peerId] as? TelegramChannel { + if channel.flags.contains(.isCreator) { + return true + } else if let initialParticipant = initialParticipant { + switch initialParticipant { + case .creator: + return false + case let .member(_, _, adminInfo, _): + if let adminInfo = adminInfo { + return adminInfo.canBeEditedByAccountPeer || adminInfo.promotedBy == accountPeerId + } else { + return false + } + } + } else { + return channel.hasAdminRights(.canAddAdmins) + } + } else { + return false + } +} + +private func channelAdminControllerEntries(presentationData: PresentationData, state: ChannelAdminControllerState, accountPeerId: PeerId, channelView: PeerView, adminView: PeerView, initialParticipant: ChannelParticipant?) -> [ChannelAdminEntry] { + var entries: [ChannelAdminEntry] = [] + + if let channel = channelView.peers[channelView.peerId] as? TelegramChannel, let admin = adminView.peers[adminView.peerId] { + entries.append(.info(presentationData.theme, presentationData.strings, admin, adminView.peerPresences[admin.id] as? TelegramUserPresence)) + + let isGroup: Bool + let maskRightsFlags: TelegramChannelAdminRightsFlags + let rightsOrder: [TelegramChannelAdminRightsFlags] + + switch channel.info { + case .broadcast: + isGroup = false + maskRightsFlags = .broadcastSpecific + rightsOrder = [ + .canChangeInfo, + .canPostMessages, + .canEditMessages, + .canDeleteMessages, + .canAddAdmins + ] + case .group: + isGroup = true + maskRightsFlags = .groupSpecific + rightsOrder = [ + .canChangeInfo, + .canDeleteMessages, + .canBanUsers, + .canInviteUsers, + .canChangeInviteLink, + .canPinMessages, + .canAddAdmins + ] + } + + if canEditAdminRights(accountPeerId: accountPeerId, channelView: channelView, initialParticipant: initialParticipant) { + let accountUserRightsFlags: TelegramChannelAdminRightsFlags + if channel.flags.contains(.isCreator) { + accountUserRightsFlags = maskRightsFlags + } else if let adminRights = channel.adminRights { + accountUserRightsFlags = maskRightsFlags.intersection(adminRights.flags) + } else { + accountUserRightsFlags = [] + } + + let currentRightsFlags: TelegramChannelAdminRightsFlags + if let updatedFlags = state.updatedFlags { + currentRightsFlags = updatedFlags + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _) = initialParticipant, let adminRights = maybeAdminRights { + currentRightsFlags = adminRights.rights.flags + } else { + currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins) + } + + var index = 0 + for right in rightsOrder { + if accountUserRightsFlags.contains(right) { + entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating)) + index += 1 + } + } + + if let initialParticipant = initialParticipant { + var canDismiss = false + if channel.flags.contains(.isCreator) { + canDismiss = true + } else { + switch initialParticipant { + case .creator: + break + case let .member(_, _, adminInfo, _): + if let adminInfo = adminInfo { + if adminInfo.promotedBy == accountPeerId || adminInfo.canBeEditedByAccountPeer { + canDismiss = true + } + } + } + } + if canDismiss { + entries.append(.dismiss(presentationData.theme, presentationData.strings.Channel_Moderator_AccessLevelRevoke)) + } + } + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _) = initialParticipant, let adminInfo = maybeAdminInfo { + var index = 0 + for right in rightsOrder { + entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup), right, adminInfo.rights.flags, adminInfo.rights.flags.contains(right), false)) + index += 1 + } + } + } + + return entries +} + +public func channelAdminController(account: Account, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant?, updated: @escaping (TelegramChannelAdminRights) -> Void) -> ViewController { + let statePromise = ValuePromise(ChannelAdminControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: ChannelAdminControllerState()) + let updateState: ((ChannelAdminControllerState) -> ChannelAdminControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let actionsDisposable = DisposableSet() + + let updateRightsDisposable = MetaDisposable() + actionsDisposable.add(updateRightsDisposable) + + var dismissImpl: (() -> Void)? + + let arguments = ChannelAdminControllerArguments(account: account, toggleRight: { right, flags in + updateState { current in + var updated = flags + if flags.contains(right) { + updated.remove(right) + } else { + updated.insert(right) + } + return current.withUpdatedUpdatedFlags(updated) + } + }, dismissAdmin: { + updateState { current in + return current.withUpdatedUpdating(true) + } + updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: [])) |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + updated(TelegramChannelAdminRights(flags: [])) + dismissImpl?() + })) + }) + + let combinedView = account.postbox.combinedView(keys: [.peer(peerId: peerId), .peer(peerId: adminId)]) + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), combinedView) + |> deliverOnMainQueue + |> map { presentationData, state, combinedView -> (ItemListControllerState, (ItemListNodeState, ChannelAdminEntry.ItemGenerationArguments)) in + let channelView = combinedView.views[.peer(peerId: peerId)] as! PeerView + let adminView = combinedView.views[.peer(peerId: adminId)] as! PeerView + let canEdit = canEditAdminRights(accountPeerId: account.peerId, channelView: channelView, initialParticipant: initialParticipant) + + let leftNavigationButton: ItemListNavigationButton + if canEdit { + leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + dismissImpl?() + }) + } else { + leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + dismissImpl?() + }) + } + + var rightNavigationButton: ItemListNavigationButton? + if state.updating { + rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) + } else if canEdit { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + var updateFlags: TelegramChannelAdminRightsFlags? + updateState { current in + updateFlags = current.updatedFlags + if let _ = updateFlags { + return current.withUpdatedUpdating(true) + } else { + return current + } + } + if let updateFlags = updateFlags { + updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: updateFlags)) |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + updated(TelegramChannelAdminRights(flags: updateFlags)) + dismissImpl?() + })) + } + }) + } + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Channel_Management_LabelEditor), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + + let listState = ItemListNodeState(entries: channelAdminControllerEntries(presentationData: presentationData, state: state, accountPeerId: account.peerId, channelView: channelView, adminView: adminView, initialParticipant: initialParticipant), style: .blocks, emptyStateItem: nil, animateChanges: true) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(account: account, state: signal) + dismissImpl = { [weak controller] in + controller?.dismiss() + } + return controller +} diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift index 077d377b2c..9115c785f6 100644 --- a/TelegramUI/ChannelAdminsController.swift +++ b/TelegramUI/ChannelAdminsController.swift @@ -4,8 +4,6 @@ import SwiftSignalKit import Postbox import TelegramCore -private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed() - private final class ChannelAdminsControllerArguments { let account: Account @@ -13,13 +11,15 @@ private final class ChannelAdminsControllerArguments { let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let removeAdmin: (PeerId) -> Void let addAdmin: () -> Void + let openAdmin: (ChannelParticipant) -> Void - init(account: Account, updateCurrentAdministrationType: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removeAdmin: @escaping (PeerId) -> Void, addAdmin: @escaping () -> Void) { + init(account: Account, updateCurrentAdministrationType: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removeAdmin: @escaping (PeerId) -> Void, addAdmin: @escaping () -> Void, openAdmin: @escaping (ChannelParticipant) -> Void) { self.account = account self.updateCurrentAdministrationType = updateCurrentAdministrationType self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removeAdmin = removeAdmin self.addAdmin = addAdmin + self.openAdmin = openAdmin } } @@ -60,13 +60,13 @@ private enum ChannelAdminsEntryStableId: Hashable { } private enum ChannelAdminsEntry: ItemListNodeEntry { - case administrationType(CurrentAdministrationType) - case administrationInfo(String) + case administrationType(PresentationTheme, String, String) + case administrationInfo(PresentationTheme, String) - case adminsHeader(String) - case adminPeerItem(Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool) - case addAdmin(Bool) - case adminsInfo(String) + case adminsHeader(PresentationTheme, String) + case adminPeerItem(PresentationTheme, PresentationStrings, Bool, Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool) + case addAdmin(PresentationTheme, String, Bool) + case adminsInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -89,33 +89,42 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { return .index(3) case .adminsInfo: return .index(4) - case let .adminPeerItem(_, participant, _, _): + case let .adminPeerItem(_, _, _, _, participant, _, _): return .peer(participant.peer.id) } } static func ==(lhs: ChannelAdminsEntry, rhs: ChannelAdminsEntry) -> Bool { switch lhs { - case let .administrationType(type): - if case .administrationType(type) = rhs { + case let .administrationType(lhsTheme, lhsText, lhsValue): + if case let .administrationType(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .administrationInfo(text): - if case .administrationInfo(text) = rhs { + case let .administrationInfo(lhsTheme, lhsText): + if case let .administrationInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .adminsHeader(title): - if case .adminsHeader(title) = rhs { + case let .adminsHeader(lhsTheme, lhsText): + if case let .adminsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .adminPeerItem(lhsIndex, lhsParticipant, lhsEditing, lhsEnabled): - if case let .adminPeerItem(rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs { + case let .adminPeerItem(lhsTheme, lhsStrings, lhsIsGroup, lhsIndex, lhsParticipant, lhsEditing, lhsEnabled): + if case let .adminPeerItem(rhsTheme, rhsStrings, rhsIsGroup, rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } + if lhsIsGroup != rhsIsGroup { + return false + } if lhsIndex != rhsIndex { return false } @@ -132,14 +141,14 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { } else { return false } - case let .adminsInfo(text): - if case .adminsInfo(text) = rhs { + case let .adminsInfo(lhsTheme, lhsText): + if case let .adminsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .addAdmin(editing): - if case .addAdmin(editing) = rhs { + case let .addAdmin(lhsTheme, lhsText, lhsEditing): + if case let .addAdmin(rhsTheme, rhsText, rhsEditing) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEditing == rhsEditing { return true } else { return false @@ -165,11 +174,11 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { default: return true } - case let .adminPeerItem(index, _, _, _): + case let .adminPeerItem(_, _, _, index, _, _, _): switch rhs { case .administrationType, .administrationInfo, .adminsHeader: return false - case let .adminPeerItem(rhsIndex, _, _, _): + case let .adminPeerItem(_, _, _, rhsIndex, _, _, _): return index < rhsIndex default: return true @@ -188,40 +197,49 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { func item(_ arguments: ChannelAdminsControllerArguments) -> ListViewItem { switch self { - case let .administrationType(type): - let label: String - switch type { - case .adminsCanAddMembers: - label = "Only Admins" - case .everyoneCanAddMembers: - label = "All Members" - } - return ItemListDisclosureItem(title: "Who can add members", label: label, sectionId: self.section, style: .blocks, action: { + case let .administrationType(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.updateCurrentAdministrationType() }) - case let .administrationInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .adminsHeader(title): - return ItemListSectionHeaderItem(text: title, sectionId: self.section) - case let .adminPeerItem(_, participant, editing, enabled): + case let .administrationInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .adminsHeader(theme, title): + return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section) + case let .adminPeerItem(theme, strings, isGroup, _, participant, editing, enabled): let peerText: String + let action: (() -> Void)? switch participant.participant { case .creator: - peerText = "Creator" - default: - peerText = "Moderator" + peerText = strings.Channel_Management_LabelCreator + action = nil + case let .member(_, _, adminInfo, _): + if let adminInfo = adminInfo { + let baseFlags: TelegramChannelAdminRightsFlags + if isGroup { + baseFlags = .groupSpecific + } else { + baseFlags = .broadcastSpecific + } + let flags = adminInfo.rights.flags.intersection(baseFlags) + peerText = strings.Channel_Management_LabelRights(Int32(flags.count)) + } else { + peerText = "" + } + action = { + arguments.openAdmin(participant.participant) + } } - return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + return ItemListPeerItem(theme: theme, account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removeAdmin(peerId) }) - case let .addAdmin(editing): - return ItemListPeerActionItem(icon: addMemberPlusIcon, title: "Add Admin", sectionId: self.section, editing: editing, action: { + case let .addAdmin(theme, text, editing): + return ItemListPeerActionItem(theme: defaultPresentationTheme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: editing, action: { arguments.addAdmin() }) - case let .adminsInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) + case let .adminsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } @@ -305,12 +323,12 @@ private struct ChannelAdminsControllerState: Equatable { } } -private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdminsControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelAdminsEntry] { +private func channelAdminsControllerEntries(presentationData: PresentationData, accountPeerId: PeerId, view: PeerView, state: ChannelAdminsControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelAdminsEntry] { var entries: [ChannelAdminsEntry] = [] if let peer = view.peers[view.peerId] as? TelegramChannel { var isGroup = false - if case let .group(info) = peer.info { + if case let .group(info) = peer.info, peer.flags.contains(.isCreator) { isGroup = true let selectedType: CurrentAdministrationType @@ -323,20 +341,24 @@ private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdmins selectedType = .adminsCanAddMembers } } - - entries.append(.administrationType(selectedType)) + let selectedTypeValue: String let infoText: String switch selectedType { case .everyoneCanAddMembers: - infoText = "Everybody can add new members" + selectedTypeValue = presentationData.strings.ChannelMembers_WhoCanAddMembers_AllMembers + infoText = presentationData.strings.ChannelMembers_AllMembersMayInviteOnHelp case .adminsCanAddMembers: - infoText = "Only Admins can add new mebers" + selectedTypeValue = presentationData.strings.ChannelMembers_WhoCanAddMembers_Admins + infoText = presentationData.strings.ChannelMembers_AllMembersMayInviteOffHelp } - entries.append(.administrationInfo(infoText)) + + entries.append(.administrationType(presentationData.theme, presentationData.strings.ChannelMembers_WhoCanAddMembers, selectedTypeValue)) + + entries.append(.administrationInfo(presentationData.theme, infoText)) } if let participants = participants { - entries.append(.adminsHeader(isGroup ? "GROUP ADMINS" : "CHANNEL ADMINS")) + entries.append(.adminsHeader(presentationData.theme, isGroup ? presentationData.strings.ChannelMembers_GroupAdminsTitle : presentationData.strings.ChannelMembers_ChannelAdminsTitle)) var combinedParticipants: [RenderedChannelParticipant] = participants var existingParticipantIds = Set() @@ -356,38 +378,43 @@ private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdmins switch lhs.participant { case .creator: lhsInvitedAt = Int32.min - case let .editor(_, _, invitedAt): - lhsInvitedAt = invitedAt - case let .moderator(_, _, invitedAt): - lhsInvitedAt = invitedAt - case let .member(_, invitedAt): + case let .member(_, invitedAt, _, _): lhsInvitedAt = invitedAt } let rhsInvitedAt: Int32 switch rhs.participant { case .creator: rhsInvitedAt = Int32.min - case let .editor(_, _, invitedAt): - rhsInvitedAt = invitedAt - case let .moderator(_, _, invitedAt): - rhsInvitedAt = invitedAt - case let .member(_, invitedAt): + case let .member(_, invitedAt, _, _): rhsInvitedAt = invitedAt } return lhsInvitedAt < rhsInvitedAt }) { if !state.removedPeerIds.contains(participant.peer.id) { var editable = true - if case .creator = participant.participant { - editable = false + switch participant.participant { + case .creator: + editable = false + case let .member(id, _, adminInfo, _): + if id == accountPeerId { + editable = false + } else if let adminInfo = adminInfo { + if peer.flags.contains(.isCreator) || adminInfo.promotedBy == accountPeerId { + editable = true + } else { + editable = false + } + } else { + editable = false + } } - entries.append(.adminPeerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), existingParticipantIds.contains(participant.peer.id))) + entries.append(.adminPeerItem(presentationData.theme, presentationData.strings, isGroup, index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), existingParticipantIds.contains(participant.peer.id))) index += 1 } } - entries.append(.addAdmin(state.editing)) - entries.append(.adminsInfo(isGroup ? "You can add admins to help you manage your group" : "You can add admins to help you manage your channel")) + entries.append(.addAdmin(presentationData.theme, presentationData.strings.Channel_Management_AddModerator, state.editing)) + entries.append(.adminsInfo(presentationData.theme, presentationData.strings.Channel_Management_AddModeratorHelp)) } } @@ -416,51 +443,55 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil) + let presentationDataSignal = (account.applicationContext as! TelegramApplicationContext).presentationData + let arguments = ChannelAdminsControllerArguments(account: account, updateCurrentAdministrationType: { - let actionSheet = ActionSheetController() - let result = ValuePromise() - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "All Members", color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - result.set(true) - }), - ActionSheetButtonItem(title: "Only Admins", color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - result.set(false) - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - let updateSignal = result.get() - |> take(1) - |> mapToSignal { value -> Signal in - updateState { state in - return state.withUpdatedSelectedType(value ? .everyoneCanAddMembers : .adminsCanAddMembers) - } - - return account.postbox.loadedPeerWithId(peerId) - |> mapToSignal { peer -> Signal in - if let peer = peer as? TelegramChannel, case let .group(info) = peer.info { - var updatedValue: Bool? - if value && !info.flags.contains(.everyMemberCanInviteMembers) { - updatedValue = true - } else if !value && info.flags.contains(.everyMemberCanInviteMembers) { - updatedValue = false - } - if let updatedValue = updatedValue { - return updateGroupManagementType(account: account, peerId: peerId, type: updatedValue ? .unrestricted : .restrictedToAdmins) + let _ = (presentationDataSignal |> take(1) |> deliverOnMainQueue).start(next: { presentationData in + let actionSheet = ActionSheetController() + let result = ValuePromise() + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.ChannelMembers_WhoCanAddMembers_AllMembers, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + result.set(true) + }), + ActionSheetButtonItem(title: presentationData.strings.ChannelMembers_WhoCanAddMembers_Admins, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + result.set(false) + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + let updateSignal = result.get() + |> take(1) + |> mapToSignal { value -> Signal in + updateState { state in + return state.withUpdatedSelectedType(value ? .everyoneCanAddMembers : .adminsCanAddMembers) + } + + return account.postbox.loadedPeerWithId(peerId) + |> mapToSignal { peer -> Signal in + if let peer = peer as? TelegramChannel, case let .group(info) = peer.info { + var updatedValue: Bool? + if value && !info.flags.contains(.everyMemberCanInviteMembers) { + updatedValue = true + } else if !value && info.flags.contains(.everyMemberCanInviteMembers) { + updatedValue = false + } + if let updatedValue = updatedValue { + return updateGroupManagementType(account: account, peerId: peerId, type: updatedValue ? .unrestricted : .restrictedToAdmins) + } else { + return .complete() + } } else { return .complete() } - } else { - return .complete() } - } - } - updateAdministrationDisposable.set(updateSignal.start()) - presentControllerImpl?(actionSheet, nil) + } + updateAdministrationDisposable.set(updateSignal.start()) + presentControllerImpl?(actionSheet, nil) + }) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { @@ -511,7 +542,7 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon })) }, addAdmin: { var confirmationImpl: ((PeerId) -> Signal)? - let contactsController = ContactSelectionController(account: account, title: "Add admin", confirmation: { peerId in + let contactsController = ContactSelectionController(account: account, title: { $0.Channel_Management_AddModerator }, confirmation: { peerId in if let confirmationImpl = confirmationImpl { return confirmationImpl(peerId) } else { @@ -538,7 +569,7 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon return result.get() } } - let addAdmin = contactsController.result + /*let addAdmin = contactsController.result |> deliverOnMainQueue |> mapToSignal { memberId -> Signal in if let memberId = memberId { @@ -618,7 +649,47 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon } } presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - addAdminDisposable.set(addAdmin.start()) + addAdminDisposable.set(addAdmin.start())*/ + }, openAdmin: { participant in + if case let .member(adminId, timestamp, _, _) = participant { + presentControllerImpl?(channelAdminController(account: account, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { updatedRights in + let applyAdmin: Signal = adminsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { admins -> Signal in + if let admins = admins { + var updatedAdmins = admins + if updatedRights.isEmpty { + for i in 0 ..< updatedAdmins.count { + if updatedAdmins[i].peer.id == adminId { + updatedAdmins.remove(at: i) + break + } + } + } else { + var found = false + for i in 0 ..< updatedAdmins.count { + if updatedAdmins[i].peer.id == adminId { + if case let .member(id, date, _, banInfo) = updatedAdmins[i].participant { + updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: updatedAdmins[i].peer) + } + found = true + break + } + } + if !found { + //updatedAdmins.append(RenderedChannelParticipant(participant: .member(id, date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: banInfo), peer: peer)) + } + } + adminsPromise.set(.single(updatedAdmins)) + } + + return .complete() + } + addAdminDisposable.set(applyAdmin.start()) + }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } }) let peerView = account.viewTracker.peerView(peerId) |> deliverOnMainQueue @@ -629,19 +700,19 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon var previousPeers: [RenderedChannelParticipant]? - let signal = combineLatest(statePromise.get(), peerView, adminsPromise.get() |> deliverOnMainQueue) + let signal = combineLatest(presentationDataSignal, statePromise.get(), peerView, adminsPromise.get() |> deliverOnMainQueue) |> deliverOnMainQueue - |> map { state, view, admins -> (ItemListControllerState, (ItemListNodeState, ChannelAdminsEntry.ItemGenerationArguments)) in + |> map { presentationData, state, view, admins -> (ItemListControllerState, (ItemListNodeState, ChannelAdminsEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? if let admins = admins, admins.count > 1 { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) - } else { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + } else if let peer = view.peers[peerId] as? TelegramChannel, peer.flags.contains(.isCreator) { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } @@ -652,15 +723,15 @@ public func channelAdminsController(account: Account, peerId: PeerId) -> ViewCon let previous = previousPeers previousPeers = admins - let controllerState = ItemListControllerState(title: .text("Admins"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) - let listState = ItemListNodeState(entries: ChannelAdminsControllerEntries(view: view, state: state, participants: admins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.ChatAdmins_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: nil, animateChanges: true) + let listState = ItemListNodeState(entries: channelAdminsControllerEntries(presentationData: presentationData, accountPeerId: account.peerId, view: view, state: state, participants: admins), style: .blocks, animateChanges: previous != nil && admins != nil && previous!.count >= admins!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() - } + } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift index b56b11b4d2..a89ee408ca 100644 --- a/TelegramUI/ChannelBlacklistController.swift +++ b/TelegramUI/ChannelBlacklistController.swift @@ -159,22 +159,14 @@ private func channelBlacklistControllerEntries(view: PeerView, state: ChannelBla switch lhs.participant { case .creator: lhsInvitedAt = Int32.min - case let .editor(_, _, invitedAt): - lhsInvitedAt = invitedAt - case let .moderator(_, _, invitedAt): - lhsInvitedAt = invitedAt - case let .member(_, invitedAt): + case let .member(_, invitedAt, _, _): lhsInvitedAt = invitedAt } let rhsInvitedAt: Int32 switch rhs.participant { case .creator: rhsInvitedAt = Int32.min - case let .editor(_, _, invitedAt): - rhsInvitedAt = invitedAt - case let .moderator(_, _, invitedAt): - rhsInvitedAt = invitedAt - case let .member(_, invitedAt): + case let .member(_, invitedAt, _, _): rhsInvitedAt = invitedAt } return lhsInvitedAt < rhsInvitedAt @@ -262,9 +254,9 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View var previousPeers: [RenderedChannelParticipant]? - let signal = combineLatest(statePromise.get(), peerView, peersPromise.get()) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView, peersPromise.get()) |> deliverOnMainQueue - |> map { state, view, peers -> (ItemListControllerState, (ItemListNodeState, ChannelBlacklistEntry.ItemGenerationArguments)) in + |> map { presentationData, state, view, peers -> (ItemListControllerState, (ItemListNodeState, ChannelBlacklistEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? if let peers = peers, !peers.isEmpty { if state.editing { @@ -300,7 +292,7 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View let previous = previousPeers previousPeers = peers - let controllerState = ItemListControllerState(title: .text("Blacklist"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Blacklist"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: true) let listState = ItemListNodeState(entries: channelBlacklistControllerEntries(view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) return (controllerState, (listState, arguments)) @@ -308,7 +300,7 @@ public func channelBlacklistController(account: Account, peerId: PeerId) -> View actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index caffd0a331..dba42b4539 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -55,7 +55,7 @@ private enum ChannelInfoEntryTag { } private enum ChannelInfoEntry: ItemListNodeEntry { - case info(peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: TelegramMediaImageRepresentation?) + case info(PresentationTheme, PresentationStrings, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: TelegramMediaImageRepresentation?) case about(text: String) case addressName(value: String) case channelPhotoSetup @@ -118,8 +118,14 @@ private enum ChannelInfoEntry: ItemListNodeEntry { static func ==(lhs: ChannelInfoEntry, rhs: ChannelInfoEntry) -> Bool { switch lhs { - case let .info(lhsPeer, lhsCachedData, lhsState, lhsUpdatingAvatar): - if case let .info(rhsPeer, rhsCachedData, rhsState, rhsUpdatingAvatar) = rhs { + case let .info(lhsTheme, lhsStrings, lhsPeer, lhsCachedData, lhsState, lhsUpdatingAvatar): + if case let .info(rhsTheme, rhsStrings, rhsPeer, rhsCachedData, rhsState, rhsUpdatingAvatar) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false @@ -209,16 +215,16 @@ private enum ChannelInfoEntry: ItemListNodeEntry { func item(_ arguments: ChannelInfoControllerArguments) -> ListViewItem { switch self { - case let .info(peer, cachedData, state, updatingAvatar): - return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in + case let .info(theme, strings, peer, cachedData, state, updatingAvatar): + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { arguments.tapAvatarAction() }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar) case let .about(text): - return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section, action: nil) + return ItemListTextWithLabelItem(theme: defaultPresentationTheme, label: "about", text: text, multiline: true, sectionId: self.section, action: nil) case let .addressName(value): - return ItemListTextWithLabelItem(label: "share link", text: "https://t.me/\(value)", multiline: false, sectionId: self.section, action: { + return ItemListTextWithLabelItem(theme: defaultPresentationTheme, label: "share link", text: "https://t.me/\(value)", multiline: false, sectionId: self.section, action: { arguments.displayAddressNameContextMenu("https://t.me/\(value)") }, tag: ChannelInfoEntryTag.addressName) case .channelPhotoSetup: @@ -230,7 +236,7 @@ private enum ChannelInfoEntry: ItemListNodeEntry { arguments.openChannelTypeSetup() }) case let .channelDescriptionSetup(text): - return ItemListMultilineInputItem(text: text, placeholder: "Channel Description", sectionId: self.section, style: .plain, textUpdated: { updatedText in + return ItemListMultilineInputItem(theme: defaultPresentationTheme, text: text, placeholder: "Channel Description", sectionId: self.section, style: .plain, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }, action: { @@ -339,32 +345,23 @@ private struct ChannelInfoEditingState: Equatable { } } -private func channelInfoEntries(account: Account, view: PeerView, state: ChannelInfoState) -> [ChannelInfoEntry] { +private func channelInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: ChannelInfoState) -> [ChannelInfoEntry] { var entries: [ChannelInfoEntry] = [] if let peer = view.peers[view.peerId] as? TelegramChannel { - var canManageChannel = false - var canManageMembers = false + let canEditChannel = peer.hasAdminRights(.canChangeInfo) + let canEditMembers = peer.hasAdminRights(.canBanUsers) let isPublic = peer.username != nil - switch peer.role { - case .creator: - canManageChannel = true - canManageMembers = true - case .moderator: - canManageMembers = true - case .editor, .member: - break - } - let infoState = ItemListAvatarAndNameInfoItemState(editingName: canManageChannel ? state.editingState?.editingName : nil, updatingName: nil) - entries.append(.info(peer: peer, cachedData: view.cachedData, state: infoState, updatingAvatar: state.updatingAvatar)) + let infoState = ItemListAvatarAndNameInfoItemState(editingName: canEditChannel ? state.editingState?.editingName : nil, updatingName: nil) + entries.append(.info(presentationData.theme, presentationData.strings, peer: peer, cachedData: view.cachedData, state: infoState, updatingAvatar: state.updatingAvatar)) - if state.editingState != nil && canManageChannel { + if state.editingState != nil && canEditChannel { entries.append(.channelPhotoSetup) } if let cachedChannelData = view.cachedData as? CachedChannelData { - if let editingState = state.editingState, canManageChannel { + if let editingState = state.editingState, canEditChannel { entries.append(.channelDescriptionSetup(text: editingState.editingDescriptionText)) } else { if let about = cachedChannelData.about, !about.isEmpty { @@ -373,18 +370,18 @@ private func channelInfoEntries(account: Account, view: PeerView, state: Channel } } - if state.editingState != nil && canManageChannel { + if state.editingState != nil && peer.flags.contains(.isCreator) { entries.append(.channelTypeSetup(isPublic: isPublic)) } else if let username = peer.username, !username.isEmpty { entries.append(.addressName(value: username)) } if let cachedChannelData = view.cachedData as? CachedChannelData { - if state.editingState != nil && canManageMembers { + if state.editingState != nil && canEditMembers { if let bannedCount = cachedChannelData.participantsSummary.bannedCount { entries.append(.banned(count: bannedCount)) } - } else if canManageMembers { + } else { if let adminCount = cachedChannelData.participantsSummary.adminCount { entries.append(.admins(count: adminCount)) } @@ -399,7 +396,7 @@ private func channelInfoEntries(account: Account, view: PeerView, state: Channel } entries.append(ChannelInfoEntry.sharedMedia) - if peer.role == .creator { + if peer.flags.contains(.isCreator) { if state.editingState != nil { entries.append(ChannelInfoEntry.deleteChannel) } @@ -630,19 +627,16 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr displayAddressNameContextMenuImpl?(text) }) - let signal = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId)) - |> map { state, view -> (ItemListControllerState, (ItemListNodeState, ChannelInfoEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId)) + |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, ChannelInfoEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) var canManageChannel = false if let peer = peer as? TelegramChannel { - switch peer.role { - case .creator: + if peer.flags.contains(.isCreator) { + canManageChannel = true + } else if let adminRights = peer.adminRights, !adminRights.isEmpty { canManageChannel = true - case .moderator: - break - case .editor, .member: - break } } @@ -722,15 +716,15 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr }) } - let controllerState = ItemListControllerState(title: .text("Info"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton) - let listState = ItemListNodeState(entries: channelInfoEntries(account: account, view: view, state: state), style: .plain) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Info"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) + let listState = ItemListNodeState(entries: channelInfoEntries(account: account, presentationData: presentationData, view: view, state: state), style: .plain) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) pushControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.pushViewController(value) diff --git a/TelegramUI/ChannelMembersController.swift b/TelegramUI/ChannelMembersController.swift index f234af93d0..e8bf30270b 100644 --- a/TelegramUI/ChannelMembersController.swift +++ b/TelegramUI/ChannelMembersController.swift @@ -201,41 +201,32 @@ private func ChannelMembersControllerEntries(account: Account, view: PeerView, s switch lhs.participant { case .creator: lhsInvitedAt = Int32.min - case let .editor(_, _, invitedAt): - lhsInvitedAt = invitedAt - case let .moderator(_, _, invitedAt): - lhsInvitedAt = invitedAt - case let .member(_, invitedAt): + case let .member(_, invitedAt, _, _): lhsInvitedAt = invitedAt } let rhsInvitedAt: Int32 switch rhs.participant { case .creator: rhsInvitedAt = Int32.min - case let .editor(_, _, invitedAt): - rhsInvitedAt = invitedAt - case let .moderator(_, _, invitedAt): - rhsInvitedAt = invitedAt - case let .member(_, invitedAt): + case let .member(_, invitedAt, _, _): rhsInvitedAt = invitedAt } return lhsInvitedAt < rhsInvitedAt }) { var editable = true - var isCreator = false + var canEditMembers = false if let peer = view.peers[view.peerId] as? TelegramChannel { - isCreator = peer.role == .creator + canEditMembers = peer.hasAdminRights(.canBanUsers) } if participant.peer.id == account.peerId { editable = false } else { - if case .creator = participant.participant { - editable = false - } else if case .moderator = participant.participant { - editable = isCreator - } else if case .editor = participant.participant { - editable = isCreator + switch participant.participant { + case .creator: + editable = false + case .member: + editable = canEditMembers } } entries.append(.peerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id)) @@ -267,7 +258,7 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo let arguments = ChannelMembersControllerArguments(account: account, addMember: { var confirmationImpl: ((PeerId) -> Signal)? - let contactsController = ContactSelectionController(account: account, title: "Add Member", confirmation: { peerId in + let contactsController = ContactSelectionController(account: account, title: { $0.GroupInfo_AddParticipantTitle }, confirmation: { peerId in if let confirmationImpl = confirmationImpl { return confirmationImpl(peerId) } else { @@ -320,7 +311,7 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo } if !found { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - updatedPeers.append(RenderedChannelParticipant(participant: ChannelParticipant.member(id: peer.id, invitedAt: timestamp), peer: peer)) + updatedPeers.append(RenderedChannelParticipant(participant: ChannelParticipant.member(id: peer.id, invitedAt: timestamp, adminInfo: nil, banInfo: nil), peer: peer)) peersPromise.set(.single(updatedPeers)) } } @@ -389,9 +380,9 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo var previousPeers: [RenderedChannelParticipant]? - let signal = combineLatest(statePromise.get(), peerView, peersPromise.get()) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView, peersPromise.get()) |> deliverOnMainQueue - |> map { state, view, peers -> (ItemListControllerState, (ItemListNodeState, ChannelMembersEntry.ItemGenerationArguments)) in + |> map { presentationData, state, view, peers -> (ItemListControllerState, (ItemListNodeState, ChannelMembersEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? if let peers = peers, !peers.isEmpty { if state.editing { @@ -417,7 +408,7 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo let previous = previousPeers previousPeers = peers - let controllerState = ItemListControllerState(title: .text("Members"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Members"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: true) let listState = ItemListNodeState(entries: ChannelMembersControllerEntries(account: account, view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) return (controllerState, (listState, arguments)) @@ -425,7 +416,7 @@ public func channelMembersController(account: Account, peerId: PeerId) -> ViewCo actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/ChannelVisibilityController.swift b/TelegramUI/ChannelVisibilityController.swift index 9e479cf150..0435ff5249 100644 --- a/TelegramUI/ChannelVisibilityController.swift +++ b/TelegramUI/ChannelVisibilityController.swift @@ -33,20 +33,20 @@ private enum ChannelVisibilityEntryTag { } private enum ChannelVisibilityEntry: ItemListNodeEntry { - case typeHeader(String) - case typePublic(Bool) - case typePrivate(Bool) - case typeInfo(String) + case typeHeader(PresentationTheme, String) + case typePublic(PresentationTheme, String, Bool) + case typePrivate(PresentationTheme, String, Bool) + case typeInfo(PresentationTheme, String) - case publicLinkAvailability(Bool) - case privateLink(String?) - case editablePublicLink(String?, String) - case privateLinkInfo(String) - case publicLinkInfo(String) - case publicLinkStatus(String, AddressNameValidationStatus) + case publicLinkAvailability(PresentationTheme, String, Bool) + case privateLink(PresentationTheme, String, String?) + case editablePublicLink(PresentationTheme, String) + case privateLinkInfo(PresentationTheme, String) + case publicLinkInfo(PresentationTheme, String) + case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus) - case existingLinksInfo(String) - case existingLinkPeerItem(Int32, Peer, ItemListPeerItemEditing, Bool) + case existingLinksInfo(PresentationTheme, String) + case existingLinkPeerItem(Int32, PresentationTheme, PresentationStrings, Peer, ItemListPeerItemEditing, Bool) var section: ItemListSectionId { switch self { @@ -85,84 +85,90 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { case .existingLinksInfo: return 10 - case let .existingLinkPeerItem(index, _, _, _): + case let .existingLinkPeerItem(index, _, _, _, _, _): return 11 + index } } static func ==(lhs: ChannelVisibilityEntry, rhs: ChannelVisibilityEntry) -> Bool { switch lhs { - case let .typeHeader(title): - if case .typeHeader(title) = rhs { + case let .typeHeader(lhsTheme, lhsTitle): + if case let .typeHeader(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle { return true } else { return false } - case let .typePublic(selected): - if case .typePublic(selected) = rhs { + case let .typePublic(lhsTheme, lhsTitle, lhsSelected): + if case let .typePublic(rhsTheme, rhsTitle, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSelected == rhsSelected { return true } else { return false } - case let .typePrivate(selected): - if case .typePrivate(selected) = rhs { + case let .typePrivate(lhsTheme, lhsTitle, lhsSelected): + if case let .typePrivate(rhsTheme, rhsTitle, rhsSelected) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSelected == rhsSelected { return true } else { return false } - case let .typeInfo(text): - if case .typeInfo(text) = rhs { + case let .typeInfo(lhsTheme, lhsText): + if case let .typeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .publicLinkAvailability(value): - if case .publicLinkAvailability(value) = rhs { + case let .publicLinkAvailability(lhsTheme, lhsText, lhsValue): + if case let .publicLinkAvailability(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .privateLink(lhsLink): - if case let .privateLink(rhsLink) = rhs, lhsLink == rhsLink { + case let .privateLink(lhsTheme, lhsText, lhsLink): + if case let .privateLink(rhsTheme, rhsText, rhsLink) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLink == rhsLink { return true } else { return false } - case let .editablePublicLink(lhsCurrentText, lhsText): - if case let .editablePublicLink(rhsCurrentText, rhsText) = rhs, lhsCurrentText == rhsCurrentText, lhsText == rhsText { + case let .editablePublicLink(lhsTheme, lhsCurrentText): + if case let .editablePublicLink(rhsTheme, rhsCurrentText) = rhs, lhsTheme === rhsTheme, lhsCurrentText == rhsCurrentText { return true } else { return false } - case let .privateLinkInfo(text): - if case .privateLinkInfo(text) = rhs { + case let .privateLinkInfo(lhsTheme, lhsText): + if case let .privateLinkInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .publicLinkInfo(text): - if case .publicLinkInfo(text) = rhs { + case let .publicLinkInfo(lhsTheme, lhsText): + if case let .publicLinkInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .publicLinkStatus(addressName, status): - if case .publicLinkStatus(addressName, status) = rhs { + case let .publicLinkStatus(lhsTheme, lhsText, lhsStatus): + if case let .publicLinkStatus(rhsTheme, rhsText, rhsStatus) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStatus == rhsStatus { return true } else { return false } - case let .existingLinksInfo(text): - if case .existingLinksInfo(text) = rhs { + case let .existingLinksInfo(lhsTheme, lhsText): + if case let .existingLinksInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .existingLinkPeerItem(lhsIndex, lhsPeer, lhsEditing, lhsEnabled): - if case let .existingLinkPeerItem(rhsIndex, rhsPeer, rhsEditing, rhsEnabled) = rhs { + case let .existingLinkPeerItem(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsEditing, lhsEnabled): + if case let .existingLinkPeerItem(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsEditing, rhsEnabled) = rhs { if lhsIndex != rhsIndex { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if !lhsPeer.isEqual(rhsPeer) { return false } @@ -185,74 +191,59 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { func item(_ arguments: ChannelVisibilityControllerArguments) -> ListViewItem { switch self { - case let .typeHeader(title): - return ItemListSectionHeaderItem(text: title, sectionId: self.section) - case let .typePublic(selected): - return ItemListCheckboxItem(title: "Public", checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + case let .typeHeader(theme, title): + return ItemListSectionHeaderItem(theme: theme, text: title, sectionId: self.section) + case let .typePublic(theme, text, selected): + return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateCurrentType(.publicChannel) }) - case let .typePrivate(selected): - return ItemListCheckboxItem(title: "Private", checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + case let .typePrivate(theme, text, selected): + return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateCurrentType(.privateChannel) }) - case let .typeInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .publicLinkAvailability(value): - if value { - return ItemListActivityTextItem(displayActivity: true, text: NSAttributedString(string: "Checking", textColor: UIColor(0x6d6d72)), sectionId: self.section) - } else { - return ItemListActivityTextItem(displayActivity: false, text: NSAttributedString(string: "Sorry, you have reserved too many public usernames. You can revoke the link from one of your older groups or channels, or create a private entity instead.", textColor: UIColor(0xcf3030)), sectionId: self.section) - } - case let .privateLink(link): - return ItemListActionItem(title: link ?? "Loading...", kind: link != nil ? .neutral : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { - if let link = link { - arguments.displayPrivateLinkMenu(link) + case let .typeInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .publicLinkAvailability(theme, text, value): + return ItemListActivityTextItem(displayActivity: value, text: NSAttributedString(string: text, textColor: value ? theme.list.freeTextColor : theme.list.freeTextErrorColor), sectionId: self.section) + case let .privateLink(theme, text, value): + return ItemListActionItem(theme: theme, title: text, kind: value != nil ? .neutral : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { + if let value = value { + arguments.displayPrivateLinkMenu(value) } }, tag: ChannelVisibilityEntryTag.privateLink) - case let .editablePublicLink(currentText, text): - return ItemListSingleLineInputItem(title: NSAttributedString(string: "t.me/", textColor: .black), text: text, placeholder: "", sectionId: self.section, textUpdated: { updatedText in + case let .editablePublicLink(theme, currentText): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: currentText, placeholder: "", sectionId: self.section, textUpdated: { updatedText in arguments.updatePublicLinkText(currentText, updatedText) }, action: { }) - case let .privateLinkInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .publicLinkInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .publicLinkStatus(addressName, status): + case let .privateLinkInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .publicLinkInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .publicLinkStatus(theme, text, status): var displayActivity = false - let text: NSAttributedString + let color: UIColor switch status { - case let .invalidFormat(error): - switch error { - case .startsWithDigit: - text = NSAttributedString(string: "Names can't start with a digit.", textColor: UIColor(0xcf3030)) - case .startsWithUnderscore: - text = NSAttributedString(string: "Names can't start with an underscore.", textColor: UIColor(0xcf3030)) - case .endsWithUnderscore: - text = NSAttributedString(string: "Names can't end with an underscore.", textColor: UIColor(0xcf3030)) - case .tooShort: - text = NSAttributedString(string: "Names must have at least 5 characters.", textColor: UIColor(0xcf3030)) - case .invalidCharacters: - text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030)) - } + case .invalidFormat: + color = theme.list.freeTextErrorColor case let .availability(availability): switch availability { case .available: - text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(0x26972c)) + color = theme.list.freeTextSuccessColor case .invalid: - text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030)) + color = theme.list.freeTextErrorColor case .taken: - text = NSAttributedString(string: "\(addressName) is already taken.", textColor: UIColor(0xcf3030)) + color = theme.list.freeTextErrorColor } case .checking: - text = NSAttributedString(string: "Checking name...", textColor: UIColor(0x6d6d72)) + color = theme.list.freeTextColor displayActivity = true } - return ItemListActivityTextItem(displayActivity: displayActivity, text: text, sectionId: self.section) - case let .existingLinksInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .existingLinkPeerItem(_, peer, editing, enabled): + return ItemListActivityTextItem(displayActivity: displayActivity, text: NSAttributedString(string: text, textColor: color), sectionId: self.section) + case let .existingLinksInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .existingLinkPeerItem(_, theme, strings, peer, editing, enabled): var label = "" if let addressName = peer.addressName { label = "t.me/" + addressName @@ -345,7 +336,7 @@ private struct ChannelVisibilityControllerState: Equatable { } } -private func channelVisibilityControllerEntries(view: PeerView, publicChannelsToRevoke: [Peer]?, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] { +private func channelVisibilityControllerEntries(presentationData: PresentationData, view: PeerView, publicChannelsToRevoke: [Peer]?, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] { var entries: [ChannelVisibilityEntry] = [] if let peer = view.peers[view.peerId] as? TelegramChannel { @@ -376,22 +367,22 @@ private func channelVisibilityControllerEntries(view: PeerView, publicChannelsTo } } - entries.append(.typeHeader(isGroup ? "GROUP TYPE" : "CHANNEL TYPE")) - entries.append(.typePublic(selectedType == .publicChannel)) - entries.append(.typePrivate(selectedType == .privateChannel)) + entries.append(.typeHeader(presentationData.theme, isGroup ? presentationData.strings.GroupInfo_GroupType : presentationData.strings.Channel_Edit_LinkItem)) + entries.append(.typePublic(presentationData.theme, presentationData.strings.Channel_Setup_TypePublic, selectedType == .publicChannel)) + entries.append(.typePrivate(presentationData.theme, presentationData.strings.Channel_Setup_TypePrivate, selectedType == .privateChannel)) switch selectedType { case .publicChannel: if isGroup { - entries.append(.typeInfo("Public groups can be found in search, chat history is available to everyone and anyone can join.")) + entries.append(.typeInfo(presentationData.theme, "Public groups can be found in search, chat history is available to everyone and anyone can join.")) } else { - entries.append(.typeInfo("Public channels can be found in search and anyone can join.")) + entries.append(.typeInfo(presentationData.theme, "Public channels can be found in search and anyone can join.")) } case .privateChannel: if isGroup { - entries.append(.typeInfo("Private groups can only be joined if you were invited of have an invite link.")) + entries.append(.typeInfo(presentationData.theme, "Private groups can only be joined if you were invited of have an invite link.")) } else { - entries.append(.typeInfo("Private channels can only be joined if you were invited of have an invite link.")) + entries.append(.typeInfo(presentationData.theme, "Private channels can only be joined if you were invited of have an invite link.")) } } @@ -404,7 +395,7 @@ private func channelVisibilityControllerEntries(view: PeerView, publicChannelsTo if displayAvailability { if let publicChannelsToRevoke = publicChannelsToRevoke { - entries.append(.publicLinkAvailability(false)) + entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_RemoveExistingUsernamesInfo, false)) var index: Int32 = 0 for peer in publicChannelsToRevoke.sorted(by: { lhs, rhs in var lhsDate: Int32 = 0 @@ -417,25 +408,60 @@ private func channelVisibilityControllerEntries(view: PeerView, publicChannelsTo } return lhsDate > rhsDate }) { - entries.append(.existingLinkPeerItem(index, peer, ItemListPeerItemEditing(editable: true, editing: true, revealed: state.revealedRevokePeerId == peer.id), state.revokingPeerId == nil)) + entries.append(.existingLinkPeerItem(index, presentationData.theme, presentationData.strings, peer, ItemListPeerItemEditing(editable: true, editing: true, revealed: state.revealedRevokePeerId == peer.id), state.revokingPeerId == nil)) index += 1 } } else { - entries.append(.publicLinkAvailability(true)) + entries.append(.publicLinkAvailability(presentationData.theme, presentationData.strings.Group_Username_CreatePublicLinkHelp, true)) } } else { - entries.append(.editablePublicLink(peer.addressName, currentAddressName)) + entries.append(.editablePublicLink(presentationData.theme, currentAddressName)) if let status = state.addressNameValidationStatus { - entries.append(.publicLinkStatus(currentAddressName, status)) + let text: String + switch status { + case let .invalidFormat(error): + switch error { + case .startsWithDigit: + text = "Names can't start with a digit." + case .startsWithUnderscore: + text = "Names can't start with an underscore." + case .endsWithUnderscore: + text = "Names can't end with an underscore." + case .tooShort: + text = "Names must have at least 5 characters." + case .invalidCharacters: + text = "Sorry, this name is invalid." + } + case let .availability(availability): + switch availability { + case .available: + text = "\(currentAddressName) is available." + case .invalid: + text = "Sorry, this name is invalid." + case .taken: + text = "\(currentAddressName) is already taken." + } + case .checking: + text = "Checking name..." + } + + entries.append(.publicLinkStatus(presentationData.theme, text, status)) } - entries.append(.publicLinkInfo("People can share this link with others and find your group using Telegram search.")) + entries.append(.publicLinkInfo(presentationData.theme, "People can share this link with others and find your group using Telegram search.")) } case .privateChannel: - entries.append(.privateLink((view.cachedData as? CachedChannelData)?.exportedInvitation?.link)) - if isGroup { - entries.append(.publicLinkInfo("People can join your group by following this link. You can revoke the link at any time.")) + let link = (view.cachedData as? CachedChannelData)?.exportedInvitation?.link + let text: String + if let link = link { + text = link } else { - entries.append(.publicLinkInfo("People can join your channel by following this link. You can revoke the link at any time.")) + text = "Loading..." + } + entries.append(.privateLink(presentationData.theme, text, link)) + if isGroup { + entries.append(.publicLinkInfo(presentationData.theme, "People can join your group by following this link. You can revoke the link at any time.")) + } else { + entries.append(.publicLinkInfo(presentationData.theme, "People can join your channel by following this link. You can revoke the link at any time.")) } } } @@ -585,8 +611,9 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: let peerView = account.viewTracker.peerView(peerId) |> deliverOnMainQueue - let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, peerView, peersDisablingAddressNameAssignment.get() |> deliverOnMainQueue) - |> map { state, view, publicChannelsToRevoke -> (ItemListControllerState, (ItemListNodeState, ChannelVisibilityEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, peerView, peersDisablingAddressNameAssignment.get() |> deliverOnMainQueue) + |> deliverOnMainQueue + |> map { presentationData, state, view, publicChannelsToRevoke -> (ItemListControllerState, (ItemListNodeState, ChannelVisibilityEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) var rightNavigationButton: ItemListNavigationButton? @@ -666,16 +693,15 @@ public func channelVisibilityController(account: Account, peerId: PeerId, mode: }) } - let controllerState = ItemListControllerState(title: .text(isGroup ? "Group Type" : "Channel Link"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, animateChanges: false) - let listState = ItemListNodeState(entries: channelVisibilityControllerEntries(view: view, publicChannelsToRevoke: publicChannelsToRevoke, state: state), style: .blocks, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(isGroup ? "Group Type" : "Channel Link"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: false) + let listState = ItemListNodeState(entries: channelVisibilityControllerEntries(presentationData: presentationData, view: view, publicChannelsToRevoke: publicChannelsToRevoke, state: state), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + let controller = ItemListController(account: account, state: signal) dismissImpl = { [weak controller] in controller?.dismiss() } diff --git a/TelegramUI/ChatBotInfoItem.swift b/TelegramUI/ChatBotInfoItem.swift index d083732bf4..94ca991820 100644 --- a/TelegramUI/ChatBotInfoItem.swift +++ b/TelegramUI/ChatBotInfoItem.swift @@ -12,10 +12,14 @@ private let messageFixedFont: UIFont = UIFont(name: "Menlo-Regular", size: 16.0) final class ChatBotInfoItem: ListViewItem { fileprivate let text: String fileprivate let controllerInteraction: ChatControllerInteraction + fileprivate let theme: PresentationTheme + fileprivate let strings: PresentationStrings - init(text: String, controllerInteraction: ChatControllerInteraction) { + init(text: String, controllerInteraction: ChatControllerInteraction, theme: PresentationTheme, strings: PresentationStrings) { self.text = text self.controllerInteraction = controllerInteraction + self.theme = theme + self.strings = strings } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { @@ -59,8 +63,6 @@ final class ChatBotInfoItem: ListViewItem { } } -private let infoItemBackground = messageSingleBubbleLikeImage(incoming: true, highlighted: false) - final class ChatBotInfoItemNode: ListViewItemNode { var controllerInteraction: ChatControllerInteraction? @@ -70,13 +72,14 @@ final class ChatBotInfoItemNode: ListViewItemNode { var currentTextAndEntities: (String, [MessageTextEntity])? + private var theme: PresentationTheme? + init() { self.offsetContainer = ASDisplayNode() self.backgroundNode = ASImageNode() self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = infoItemBackground self.textNode = TextNode() super.init(layerBacked: false, dynamicBounce: true, rotated: true) @@ -98,7 +101,13 @@ final class ChatBotInfoItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ChatBotInfoItem, _ width: CGFloat) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) let currentTextAndEntities = self.currentTextAndEntities + let currentTheme = self.theme return { [weak self] item, width in + var updatedBackgroundImage: UIImage? + if currentTheme !== item.theme { + updatedBackgroundImage = PresentationResourcesChat.chatInfoItemBackgroundImage(item.theme) + } + var updatedTextAndEntities: (String, [MessageTextEntity]) if let (text, entities) = currentTextAndEntities { if text == item.text { @@ -110,7 +119,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { updatedTextAndEntities = (item.text, generateTextEntities(item.text)) } - let attributedText = stringWithAppliedEntities(updatedTextAndEntities.0, entities: updatedTextAndEntities.1, baseFont: messageFont, boldFont: messageBoldFont, fixedFont: messageFixedFont) + let attributedText = stringWithAppliedEntities(updatedTextAndEntities.0, entities: updatedTextAndEntities.1, baseColor: item.theme.chat.bubble.infoPrimaryTextColor, linkColor: item.theme.chat.bubble.infoLinkTextColor, baseFont: messageFont, boldFont: messageBoldFont, fixedFont: messageFixedFont) let horizontalEdgeInset: CGFloat = 10.0 let horizontalContentInset: CGFloat = 12.0 @@ -125,9 +134,15 @@ final class ChatBotInfoItemNode: ListViewItemNode { let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: textLayout.size.height + verticalItemInset * 2.0 + verticalContentInset * 2.0 + 4.0), insets: UIEdgeInsets()) return (itemLayout, { _ in if let strongSelf = self { + strongSelf.theme = item.theme + + if let updatedBackgroundImage = updatedBackgroundImage { + strongSelf.backgroundNode.image = updatedBackgroundImage + } + strongSelf.controllerInteraction = item.controllerInteraction strongSelf.currentTextAndEntities = updatedTextAndEntities - textApply() + let _ = textApply() strongSelf.offsetContainer.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize) strongSelf.backgroundNode.frame = backgroundFrame strongSelf.textNode.frame = textFrame @@ -158,17 +173,20 @@ final class ChatBotInfoItemNode: ListViewItemNode { func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame - let attributes = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) - if let url = attributes[TextNode.UrlAttribute] as? String { - return .url(url) - } else if let peerId = attributes[TextNode.TelegramPeerMentionAttribute] as? NSNumber { - return .peerMention(PeerId(peerId.int64Value)) - } else if let peerName = attributes[TextNode.TelegramPeerTextMentionAttribute] as? String { - return .textMention(peerName) - } else if let botCommand = attributes[TextNode.TelegramBotCommandAttribute] as? String { - return .botCommand(botCommand) - } else if let hashtag = attributes[TextNode.TelegramHashtagAttribute] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) + if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let url = attributes[TextNode.UrlAttribute] as? String { + return .url(url) + } else if let peerMention = attributes[TextNode.TelegramPeerMentionAttribute] as? TelegramPeerMention { + return .peerMention(peerMention.peerId, peerMention.mention) + } else if let peerName = attributes[TextNode.TelegramPeerTextMentionAttribute] as? String { + return .textMention(peerName) + } else if let botCommand = attributes[TextNode.TelegramBotCommandAttribute] as? String { + return .botCommand(botCommand) + } else if let hashtag = attributes[TextNode.TelegramHashtagAttribute] as? TelegramHashtag { + return .hashtag(hashtag.peerName, hashtag.hashtag) + } else { + return .none + } } else { return .none } @@ -187,7 +205,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { if let controllerInteraction = self.controllerInteraction { controllerInteraction.openUrl(url) } - case let .peerMention(peerId): + case let .peerMention(peerId, _): foundTapAction = true if let controllerInteraction = self.controllerInteraction { controllerInteraction.openPeer(peerId, .chat(textInputState: nil), nil) diff --git a/TelegramUI/ChatBotStartInputPanelNode.swift b/TelegramUI/ChatBotStartInputPanelNode.swift index 4b3edf774a..0f7ca1080b 100644 --- a/TelegramUI/ChatBotStartInputPanelNode.swift +++ b/TelegramUI/ChatBotStartInputPanelNode.swift @@ -11,11 +11,11 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { private var statusDisposable: Disposable? - private var presentationInterfaceState = ChatPresentationInterfaceState() + private var presentationInterfaceState: ChatPresentationInterfaceState? override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { - if let interfaceInteraction = self.interfaceInteraction { + if let _ = self.interfaceInteraction { if self.statusDisposable == nil { if let startingBot = self.interfaceInteraction?.statuses?.startingBot { self.statusDisposable = (startingBot |> deliverOnMainQueue).start(next: { [weak self] value in @@ -37,7 +37,13 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { } } - override init() { + private var theme: PresentationTheme + private var strings: PresentationStrings + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + self.button = HighlightableButtonNode() self.activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) self.activityIndicator.isHidden = true @@ -47,7 +53,7 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { self.addSubnode(self.button) self.view.addSubview(self.activityIndicator) - self.button.setAttributedTitle(NSAttributedString(string: "Start", font: Font.regular(17.0), textColor: UIColor(0x007ee5)), for: []) + self.button.setAttributedTitle(NSAttributedString(string: strings.Bot_Start, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) } @@ -55,6 +61,15 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { self.statusDisposable?.dispose() } + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if self.theme !== theme || self.strings !== strings { + self.theme = theme + self.strings = strings + + self.button.setAttributedTitle(NSAttributedString(string: strings.Bot_Start, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) + } + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.bounds.contains(point) { return self.button.view @@ -64,11 +79,11 @@ final class ChatBotStartInputPanelNode: ChatInputPanelNode { } @objc func buttonPressed() { - guard let account = self.account, let peer = self.presentationInterfaceState.peer else { + guard let account = self.account, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.peer else { return } - self.interfaceInteraction?.sendBotStart(self.presentationInterfaceState.botStartPayload) + self.interfaceInteraction?.sendBotStart(presentationInterfaceState.botStartPayload) } override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { diff --git a/TelegramUI/ChatButtonKeyboardInputNode.swift b/TelegramUI/ChatButtonKeyboardInputNode.swift index a47e27a062..9221e036e7 100644 --- a/TelegramUI/ChatButtonKeyboardInputNode.swift +++ b/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -7,27 +7,24 @@ import SwiftSignalKit private let defaultPortraitPanelHeight: CGFloat = UIScreenScale.isEqual(to: 3.0) ? 271.0 : 258.0 -private func generateButtonBackgroundImage(color: UIColor) -> UIImage? { - let radius: CGFloat = 5.0 - let shadowSize: CGFloat = 1.0 - return generateImage(CGSize(width: radius * 2.0, height: radius * 2.0 + shadowSize), contextGenerator: { size, context in - context.setFillColor(UIColor(0xc3c7c9).cgColor) - context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: radius * 2.0, height: radius * 2.0)) - context.setFillColor(color.cgColor) - context.fillEllipse(in: CGRect(x: 0.0, y: shadowSize, width: radius * 2.0, height: radius * 2.0)) - })?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)) -} -private let buttonBackgroundImage = generateButtonBackgroundImage(color: .white) -private let buttonHighlightedBackgroundImage = generateButtonBackgroundImage(color: UIColor(0xa8b3c0)) - private final class ChatButtonKeyboardInputButtonNode: ASButtonNode { var button: ReplyMarkupButton? - override init() { + private var theme: PresentationTheme? + + init(theme: PresentationTheme) { super.init() - self.setBackgroundImage(buttonBackgroundImage, for: []) - self.setBackgroundImage(buttonHighlightedBackgroundImage, for: [.highlighted]) + self.updateTheme(theme: theme) + } + + func updateTheme(theme: PresentationTheme) { + if theme !== self.theme { + self.theme = theme + + self.setBackgroundImage(PresentationResourcesChat.chatInputButtonPanelButtonImage(theme), for: []) + self.setBackgroundImage(PresentationResourcesChat.chatInputButtonPanelButtonHighlightedImage(theme), for: [.highlighted]) + } } } @@ -41,6 +38,8 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { private var buttonNodes: [ChatButtonKeyboardInputButtonNode] = [] private var message: Message? + private var theme: PresentationTheme? + init(account: Account, controllerInteraction: ChatControllerInteraction) { self.account = account self.controllerInteraction = controllerInteraction @@ -49,11 +48,9 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = UIColor(0xBEC2C6) super.init() - self.backgroundColor = UIColor(0xE8EBF0) self.addSubnode(self.scrollNode) self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.canCancelContentTouches = true @@ -68,6 +65,13 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))) + if self.theme !== interfaceState.theme { + self.theme = interfaceState.theme + + self.separatorNode.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelSerapatorColor + self.backgroundColor = interfaceState.theme.chat.inputButtonPanel.panelBackgroundColor + } + var validatedMarkup: ReplyMarkupMessageAttribute? if let message = interfaceState.keyboardButtonsMessage { for attribute in message.attributes { @@ -91,7 +95,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { var panelHeight = self.heightForWidth(width: width) - var rowsHeight = verticalInset + CGFloat(markup.rows.count) * buttonHeight + CGFloat(max(0, markup.rows.count - 1)) * rowSpacing + verticalInset + let rowsHeight = verticalInset + CGFloat(markup.rows.count) * buttonHeight + CGFloat(max(0, markup.rows.count - 1)) * rowSpacing + verticalInset if !markup.flags.contains(.fit) && rowsHeight < panelHeight { buttonHeight = floor((panelHeight - verticalInset * 2.0 - CGFloat(max(0, markup.rows.count - 1)) * rowSpacing) / CGFloat(markup.rows.count)) } @@ -106,8 +110,9 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { let buttonNode: ChatButtonKeyboardInputButtonNode if buttonIndex < self.buttonNodes.count { buttonNode = self.buttonNodes[buttonIndex] + buttonNode.updateTheme(theme: interfaceState.theme) } else { - buttonNode = ChatButtonKeyboardInputButtonNode() + buttonNode = ChatButtonKeyboardInputButtonNode(theme: interfaceState.theme) buttonNode.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: [.touchUpInside]) self.scrollNode.addSubnode(buttonNode) self.buttonNodes.append(buttonNode) @@ -115,7 +120,7 @@ final class ChatButtonKeyboardInputNode: ChatInputNode { buttonIndex += 1 if buttonNode.button != button { buttonNode.button = button - buttonNode.setTitle(button.title, with: Font.regular(16.0), with: .black, for: []) + buttonNode.setTitle(button.title, with: Font.regular(16.0), with: interfaceState.theme.chat.inputButtonPanel.buttonTextColor, for: []) } buttonNode.frame = CGRect(origin: CGPoint(x: sideInset + CGFloat(columnIndex) * (buttonWidth + columnSpacing), y: verticalOffset), size: CGSize(width: buttonWidth, height: buttonHeight)) columnIndex += 1 diff --git a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift index 9e485d71b5..66ec3da1fc 100644 --- a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift +++ b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -12,16 +12,16 @@ private enum SubscriberAction { case unmuteNotifications } -private func titleAndColorForAction(_ action: SubscriberAction) -> (String, UIColor) { +private func titleAndColorForAction(_ action: SubscriberAction, theme: PresentationTheme, strings: PresentationStrings) -> (String, UIColor) { switch action { case .join: - return ("Join", UIColor(0x007ee5)) + return (strings.Channel_JoinChannel, theme.chat.inputPanel.panelControlAccentColor) case .kicked: - return ("Join", UIColor.gray) + return (strings.Channel_JoinChannel, theme.chat.inputPanel.panelControlDisabledColor) case .muteNotifications: - return ("Mute", UIColor(0x007ee5)) + return (strings.Conversation_Mute, theme.chat.inputPanel.panelControlAccentColor) case .unmuteNotifications: - return ("Unmute", UIColor(0x007ee5)) + return (strings.Conversation_Unmute, theme.chat.inputPanel.panelControlAccentColor) } } @@ -53,7 +53,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { private let actionDisposable = MetaDisposable() - private var presentationInterfaceState = ChatPresentationInterfaceState() + private var presentationInterfaceState: ChatPresentationInterfaceState? private var notificationSettingsDisposable = MetaDisposable() @@ -86,7 +86,7 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } @objc func buttonPressed() { - guard let account = self.account, let action = self.action, let peer = self.presentationInterfaceState.peer else { + guard let account = self.account, let action = self.action, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.peer else { return } @@ -106,12 +106,12 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { case .kicked: break case .muteNotifications: - if let account = self.account, let peer = self.presentationInterfaceState.peer { + if let account = self.account, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.peer { let muteState: PeerMuteState = .muted(until: Int32.max) self.actionDisposable.set(changePeerNotificationSettings(account: account, peerId: peer.id, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) } case .unmuteNotifications: - if let account = self.account, let peer = self.presentationInterfaceState.peer { + if let account = self.account, let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.peer { let muteState: PeerMuteState = .unmuted self.actionDisposable.set(changePeerNotificationSettings(account: account, peerId: peer.id, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start()) } @@ -125,10 +125,10 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState - if let peer = interfaceState.peer, previousState.peer == nil || !peer.isEqual(previousState.peer!) { + if let peer = interfaceState.peer, previousState?.peer == nil || !peer.isEqual(previousState!.peer!) || previousState?.theme !== interfaceState.theme || previousState?.strings !== interfaceState.strings { if let action = actionForPeer(peer: peer, muteState: self.muteState) { self.action = action - let (title, color) = titleAndColorForAction(action) + let (title, color) = titleAndColorForAction(action, theme: interfaceState.theme, strings: interfaceState.strings) self.button.setTitle(title, for: []) self.button.setTitleColor(color, for: [.normal]) self.button.setTitleColor(color.withAlphaComponent(0.5), for: [.highlighted]) @@ -146,20 +146,20 @@ final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } } |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] muteState in - if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.peer { + if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState, let peer = presentationInterfaceState.peer { strongSelf.muteState = muteState let action = actionForPeer(peer: peer, muteState: muteState) if let layoutData = strongSelf.layoutData, action != strongSelf.action { strongSelf.action = action if let action = action { - let (title, color) = titleAndColorForAction(action) + let (title, color) = titleAndColorForAction(action, theme: presentationInterfaceState.theme, strings: presentationInterfaceState.strings) strongSelf.button.setTitle(title, for: []) strongSelf.button.setTitleColor(color, for: [.normal]) strongSelf.button.setTitleColor(color.withAlphaComponent(0.5), for: [.highlighted]) strongSelf.button.sizeToFit() } - let _ = strongSelf.updateLayout(width: layoutData, transition: .immediate, interfaceState: strongSelf.presentationInterfaceState) + let _ = strongSelf.updateLayout(width: layoutData, transition: .immediate, interfaceState: presentationInterfaceState) } } })) diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 2a8bc667ab..b80e06b2b2 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -5,6 +5,7 @@ import SwiftSignalKit import Display import AsyncDisplayKit import TelegramCore +import SafariServices public class ChatController: TelegramController { private var containerLayout = ContainerViewLayout() @@ -23,7 +24,7 @@ public class ChatController: TelegramController { private var didSetPeerReady = false private let peerView = Promise() - private var presentationInterfaceState = ChatPresentationInterfaceState() + private var presentationInterfaceState: ChatPresentationInterfaceState private var chatTitleView: ChatTitleView? private var leftNavigationButton: ChatNavigationButton? @@ -49,6 +50,8 @@ public class ChatController: TelegramController { private let editingMessage = ValuePromise(false, ignoreRepeated: true) private let startingBot = ValuePromise(false, ignoreRepeated: true) private let unblockingPeer = ValuePromise(false, ignoreRepeated: true) + private let searching = ValuePromise(false, ignoreRepeated: true) + private let loadingMessage = ValuePromise(false, ignoreRepeated: true) private let botCallbackAlertMessage = Promise(nil) private var botCallbackAlertMessageDisposable: Disposable? @@ -76,23 +79,30 @@ public class ChatController: TelegramController { private let typingActivityPromise = Promise() private var typingActivityDisposable: Disposable? + private var searchDisposable: MetaDisposable? + private var historyNavigationStack = ChatHistoryNavigationStack() let canReadHistory = ValuePromise(true, ignoreRepeated: true) + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + public init(account: Account, peerId: PeerId, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil) { self.account = account self.peerId = peerId self.messageId = messageId self.botStart = botStart - /*if #available(iOSApplicationExtension 10.0, *) { - kdebug_signpost(1, 0, 0, 0, 0) - }*/ + self.presentationData = (account.applicationContext as! TelegramApplicationContext).currentPresentationData.with { $0 } + + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings) super.init(account: account) - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.ready.set(.never()) @@ -132,14 +142,15 @@ public class ChatController: TelegramController { controller.sendSticker = { file in self?.controllerInteraction?.sendSticker(file) } + strongSelf.chatDisplayNode.dismissInput() strongSelf.present(controller, in: .window) } break } } - } else if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice { + } else if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice || file.isInstantVideo { if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - let player = ManagedAudioPlaylistPlayer(audioSessionManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) + let player = ManagedAudioPlaylistPlayer(audioSessionManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, overlayMediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.overlayMediaManager, mediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) applicationContext.mediaManager.setPlaylistPlayer(player) player.control(.navigation(.next)) } @@ -214,20 +225,7 @@ public class ChatController: TelegramController { } }, openPeerMention: { [weak self] name in if let strongSelf = self { - let disposable: MetaDisposable - if let resolvePeerByNameDisposable = strongSelf.resolvePeerByNameDisposable { - disposable = resolvePeerByNameDisposable - } else { - disposable = MetaDisposable() - strongSelf.resolvePeerByNameDisposable = disposable - } - disposable.set((resolvePeerByName(account: strongSelf.account, name: name, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in - if let strongSelf = self { - if let peerId = peerId { - (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: nil)) - } - } - })) + strongSelf.openPeerMention(name) } }, openMessageContextMenu: { [weak self] id, node, frame in if let strongSelf = self, strongSelf.isNodeLoaded { @@ -429,7 +427,7 @@ public class ChatController: TelegramController { var shareAction: (([PeerId]) -> Void)? let shareController = ShareController(account: strongSelf.account, shareAction: { peerIds in shareAction?(peerIds) - }, defaultAction: ShareControllerAction(title: "Copy Link", action: { + }, defaultAction: ShareControllerAction(title: strongSelf.presentationData.strings.ShareMenu_CopyShareLink, action: { copyLink?() })) strongSelf.present(shareController, in: .window) @@ -454,11 +452,123 @@ public class ChatController: TelegramController { } }, presentController: { [weak self] controller, arguments in self?.present(controller, in: .window, with: arguments) + }, callPeer: { [weak self] peerId in + if let strongSelf = self { + let callResult = strongSelf.account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: false) + if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { + if currentPeerId == peerId { + strongSelf.account.telegramApplicationContext.navigateToCurrentCall?() + } else { + let presentationData = strongSelf.presentationData + let _ = (account.postbox.modify { modifier -> (Peer?, Peer?) in + return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId)) + } |> deliverOnMainQueue).start(next: { peer, current in + if let strongSelf = self, let peer = peer, let current = current { + strongSelf.present(standardTextAlertController(title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true) + })]), in: .window) + } + }) + } + } + } + }, longTap: { [weak self] action in + if let strongSelf = self { + switch action { + case let .url(url): + let actionSheet = ActionSheetController() + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: url), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.openUrl(url) + } + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Web_CopyLink, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = url + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let link = URL(string: url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, in: .window) + case let .mention(mention): + let actionSheet = ActionSheetController() + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: mention), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.openPeerMention(mention) + } + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = mention + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, in: .window) + case let .command(command): + let actionSheet = ActionSheetController() + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: command), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: command, attributes: [], media: nil, replyToMessageId: nil)]).start() + } + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = command + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, in: .window) + case let .hashtag(hashtag): + let actionSheet = ActionSheetController() + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: hashtag), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + let searchController = HashtagSearchController(account: strongSelf.account, peerName: nil, query: hashtag) + (strongSelf.navigationController as? NavigationController)?.pushViewController(searchController) + } + }), + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = hashtag + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, in: .window) + } + } }) self.controllerInteraction = controllerInteraction - self.chatTitleView = ChatTitleView(frame: CGRect()) + self.chatTitleView = ChatTitleView(theme: self.presentationData.theme, strings: self.presentationData.strings) self.navigationItem.titleView = self.chatTitleView self.chatTitleView?.pressed = { [weak self] in if let strongSelf = self { @@ -597,6 +707,20 @@ public class ChatController: TelegramController { strongSelf.account.updateLocalInputActivity(peerId: strongSelf.peerId, activity: .typingText, isPresent: value) } }) + + 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.themeAndStringsUpdated() + } + } + }) } required public init(coder aDecoder: NSCoder) { @@ -628,6 +752,8 @@ public class ChatController: TelegramController { self.recentlyUsedInlineBotsDisposable?.dispose() self.unpinMessageDisposable?.dispose() self.typingActivityDisposable?.dispose() + self.presentationDataDisposable?.dispose() + self.searchDisposable?.dispose() } var chatDisplayNode: ChatControllerNode { @@ -636,8 +762,14 @@ public class ChatController: TelegramController { } } + private func themeAndStringsUpdated() { + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + } + override public func loadDisplayNode() { - self.displayNode = ChatControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!) + self.displayNode = ChatControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, navigationBar: self.navigationBar!) let initialData = self.chatDisplayNode.historyNode.initialData |> take(1) @@ -837,7 +969,7 @@ public class ChatController: TelegramController { if let strongSelf = self { var mappedTransition: (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?)? - strongSelf.chatDisplayNode.containerLayoutUpdated(strongSelf.containerLayout, navigationBarHeight: strongSelf.navigationBar.frame.maxY, transition: .animated(duration: 0.4, curve: .spring), listViewTransaction: { updateSizeAndInsets in + strongSelf.chatDisplayNode.containerLayoutUpdated(strongSelf.containerLayout, navigationBarHeight: strongSelf.navigationHeight, transition: .animated(duration: 0.4, curve: .spring), listViewTransaction: { updateSizeAndInsets in var options = transition.options let _ = options.insert(.Synchronous) let _ = options.insert(.LowLatency) @@ -892,7 +1024,7 @@ public class ChatController: TelegramController { let legacyController = LegacyController(legacyController: navigationController, presentation: .custom) var presentOverlayController: ((UIViewController) -> (() -> Void))? - let controller = legacyAttachmentMenu(parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, presentOverlayController: { controller in + let controller = legacyAttachmentMenu(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, parentController: legacyController, recentlyUsedInlineBots: strongSelf.recentlyUsedInlineBotsValue, presentOverlayController: { controller in if let presentOverlayController = presentOverlayController { return presentOverlayController(controller) } else { @@ -913,7 +1045,7 @@ public class ChatController: TelegramController { }, openContacts: { if let strongSelf = self { - let contactsController = ContactSelectionController(account: strongSelf.account, title: "Select Contact") + let contactsController = ContactSelectionController(account: strongSelf.account, title: { $0.DialogList_SelectContact }) strongSelf.present(contactsController, in: .window, with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) strongSelf.controllerNavigationDisposable.set((contactsController.result |> deliverOnMainQueue).start(next: { peerId in if let strongSelf = self, let peerId = peerId { @@ -1040,11 +1172,11 @@ public class ChatController: TelegramController { if options.contains(.globally) { let globalTitle: String if isChannel { - globalTitle = "Delete" + globalTitle = strongSelf.presentationData.strings.Common_Delete } else if let personalPeerName = personalPeerName { - globalTitle = "Delete for me and \(personalPeerName)" + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0 } else { - globalTitle = "Delete for everyone" + globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone } items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -1055,7 +1187,7 @@ public class ChatController: TelegramController { })) } if options.contains(.locally) { - items.append(ActionSheetButtonItem(title: "Delete for me", color: .destructive, action: { [weak actionSheet] in + items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) @@ -1064,7 +1196,7 @@ public class ChatController: TelegramController { })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -1142,14 +1274,149 @@ public class ChatController: TelegramController { } }, beginMessageSearch: { [weak self] in if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + return current.updatedTitlePanelContext { + if let index = $0.index(where: { + switch $0 { + case .chatInfo: + return true + default: + return false + } + }) { + var updatedContexts = $0 + updatedContexts.remove(at: index) + return updatedContexts + } else { + return $0 + } + }.updatedSearch(current.search == nil ? ChatSearchData() : current.search) + }) + } + }, dismissMessageSearch: { [weak self] in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + return current.updatedSearch(nil) + }) + } + }, updateMessageSearch: { [weak self] query in + if let strongSelf = self { + var begin = false + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + if let data = current.search, data.query != query { + begin = true + return current.updatedSearch(data.withUpdatedQuery(query)) + } else { + return current + } + }) + if begin { + if query.isEmpty { + strongSelf.searching.set(false) + strongSelf.searchDisposable?.set(nil) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + if let data = current.search { + return current.updatedSearch(data.withUpdatedResultsState(nil)) + } else { + return current + } + }) + } else { + strongSelf.searching.set(true) + let searchDisposable: MetaDisposable + if let current = strongSelf.searchDisposable { + searchDisposable = current + } else { + searchDisposable = MetaDisposable() + strongSelf.searchDisposable = searchDisposable + } + searchDisposable.set((searchMessages(account: strongSelf.account, peerId: strongSelf.peerId, query: query) |> deliverOnMainQueue).start(next: { results in + if let strongSelf = self { + var navigateId: MessageId? + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + if let data = current.search { + let messageIds = results.map({ $0.id }).sorted() + var currentId = messageIds.last + if let previousResultId = data.resultsState?.currentId { + for id in messageIds { + if id >= previousResultId { + currentId = id + break + } + } + } + navigateId = currentId + return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIds: messageIds, currentId: currentId))) + } else { + return current + } + }) + if let navigateId = navigateId { + strongSelf.navigateToMessage(from: nil, to: navigateId) + } + } + }, completed: { + if let strongSelf = self { + strongSelf.searching.set(false) + } + })) + } + } + } + }, navigateMessageSearch: { [weak self] action in + if let strongSelf = self { + var navigateId: MessageId? + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + if let data = current.search, let resultsState = data.resultsState { + if let currentId = resultsState.currentId, let index = resultsState.messageIds.index(of: currentId) { + var updatedIndex: Int? + switch action { + case .earlier: + if index != 0 { + updatedIndex = index - 1 + } + case .later: + if index != resultsState.messageIds.count - 1 { + updatedIndex = index + 1 + } + } + if let updatedIndex = updatedIndex { + navigateId = resultsState.messageIds[updatedIndex] + return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIds: resultsState.messageIds, currentId: resultsState.messageIds[updatedIndex]))) + } + } + } + return current + }) + if let navigateId = navigateId { + strongSelf.navigateToMessage(from: nil, to: navigateId) + } + } + }, openCalendarSearch: { [weak self] in + if let strongSelf = self { + strongSelf.chatDisplayNode.dismissInput() + let controller = ChatDateSelectionSheet(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, completion: { timestamp in + if let strongSelf = self { + strongSelf.loadingMessage.set(true) + strongSelf.messageIndexDisposable.set((searchMessageIdByTimestamp(account: strongSelf.account, peerId: strongSelf.peerId, timestamp: timestamp) |> deliverOnMainQueue).start(next: { messageId in + if let strongSelf = self { + strongSelf.loadingMessage.set(false) + if let messageId = messageId { + strongSelf.navigateToMessage(from: nil, to: messageId) + } + } + })) + } + }) + strongSelf.present(controller, in: .window) } }, navigateToMessage: { [weak self] messageId in self?.navigateToMessage(from: nil, to: messageId) }, openPeerInfo: { [weak self] in self?.navigationButtonAction(.openChatInfo) }, togglePeerNotifications: { - + }, sendContextResult: { [weak self] results, result in self?.enqueueChatContextResult(results, result) }, sendBotCommand: { [weak self] botPeer, command in @@ -1194,7 +1461,7 @@ public class ChatController: TelegramController { strongSelf.chatDisplayNode.dismissInput() if let peer = strongSelf.presentationInterfaceState.peer as? TelegramSecretChat { - let controller = ChatSecretAutoremoveTimerActionSheetController(currentValue: peer.messageAutoremoveTimeout == nil ? 0 : peer.messageAutoremoveTimeout!, applyValue: { value in + let controller = ChatSecretAutoremoveTimerActionSheetController(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, currentValue: peer.messageAutoremoveTimeout == nil ? 0 : peer.messageAutoremoveTimeout!, applyValue: { value in if let strongSelf = self { let _ = setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.account, peerId: strongSelf.peerId, timeout: value == 0 ? nil : value).start() } @@ -1219,31 +1486,30 @@ public class ChatController: TelegramController { if let strongSelf = self { if let peer = strongSelf.presentationInterfaceState.peer { if let channel = peer as? TelegramChannel { - switch channel.role { - case .creator, .moderator, .editor: - let pinAction: (Bool) -> Void = { notify in - if let strongSelf = self { - let disposable: MetaDisposable - if let current = strongSelf.unpinMessageDisposable { - disposable = current - } else { - disposable = MetaDisposable() - strongSelf.unpinMessageDisposable = disposable - } - disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: strongSelf.peerId, update: .pin(id: messageId, silent: !notify)).start()) + if channel.hasAdminRights([.canPinMessages]) { + let pinAction: (Bool) -> Void = { notify in + if let strongSelf = self { + let disposable: MetaDisposable + if let current = strongSelf.unpinMessageDisposable { + disposable = current + } else { + disposable = MetaDisposable() + strongSelf.unpinMessageDisposable = disposable } + disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: strongSelf.peerId, update: .pin(id: messageId, silent: !notify)).start()) } - strongSelf.present(standardTextAlertController(title: nil, text: "Pin this message and notify all members of the group?", actions: [TextAlertAction(type: .genericAction, title: "Only Pin", action: { - pinAction(false) - }), TextAlertAction(type: .defaultAction, title: "Yes", action: { - pinAction(true) - })]), in: .window) - case .member: - if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessageId { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedPinnedMessageId(pinnedMessageId) }) }) - }) - } + } + strongSelf.present(standardTextAlertController(title: nil, text: strongSelf.presentationData.strings.Conversation_PinMessageAlertGroup, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Conversation_PinMessageAlert_OnlyPin, action: { + pinAction(false) + }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Yes, action: { + pinAction(true) + })]), in: .window) + } else { + if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessageId { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedPinnedMessageId(pinnedMessageId) }) }) + }) + } } } } @@ -1252,26 +1518,25 @@ public class ChatController: TelegramController { if let strongSelf = self { if let peer = strongSelf.presentationInterfaceState.peer { if let channel = peer as? TelegramChannel { - switch channel.role { - case .creator, .moderator, .editor: - strongSelf.present(standardTextAlertController(title: nil, text: "Would you like to unpin this Message?", actions: [TextAlertAction(type: .genericAction, title: "No", action: {}), TextAlertAction(type: .genericAction, title: "Yes", action: { - if let strongSelf = self { - let disposable: MetaDisposable - if let current = strongSelf.unpinMessageDisposable { - disposable = current - } else { - disposable = MetaDisposable() - strongSelf.unpinMessageDisposable = disposable - } - disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: strongSelf.peerId, update: .clear).start()) + if channel.hasAdminRights([.canPinMessages]) { + strongSelf.present(standardTextAlertController(title: nil, text: strongSelf.presentationData.strings.Conversation_UnpinMessageAlert, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_No, action: {}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Yes, action: { + if let strongSelf = self { + let disposable: MetaDisposable + if let current = strongSelf.unpinMessageDisposable { + disposable = current + } else { + disposable = MetaDisposable() + strongSelf.unpinMessageDisposable = disposable } - })]), in: .window) - case .member: - if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessageId { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { - return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedPinnedMessageId(pinnedMessageId) }) }) - }) + disposable.set(requestUpdatePinnedMessage(account: strongSelf.account, peerId: strongSelf.peerId, update: .clear).start()) } + })]), in: .window) + } else { + if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessageId { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedPinnedMessageId(pinnedMessageId) }) }) + }) + } } } } @@ -1282,7 +1547,7 @@ public class ChatController: TelegramController { self?.dismissReportPeer() }, deleteChat: { [weak self] in self?.deleteChat(reportChatSpam: false) - }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get())) + }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get())) self.chatUnreadCountDisposable = (self.account.postbox.unreadMessageCountsView(items: [.peer(self.peerId)]) |> deliverOnMainQueue).start(next: { [weak self] items in if let strongSelf = self { @@ -1355,11 +1620,11 @@ public class ChatController: TelegramController { super.viewDidAppear(animated) self.chatDisplayNode.historyNode.preloadPages = true - self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest((self.account.applicationContext as! TelegramApplicationContext).applicationInForeground, self.canReadHistory.get()) |> map { a, b in + self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest((self.account.applicationContext as! TelegramApplicationContext).applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in return a && b }) - self.chatDisplayNode.loadInputPanels() + self.chatDisplayNode.loadInputPanels(theme: self.presentationInterfaceState.theme, strings: self.presentationInterfaceState.strings) self.recentlyUsedInlineBotsDisposable = (recentlyUsedInlineBots(postbox: self.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] peers in self?.recentlyUsedInlineBotsValue = peers @@ -1565,7 +1830,7 @@ public class ChatController: TelegramController { case .clearHistory: let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Delete All Messages", color: .destructive, action: { [weak self, weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ClearAll, color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) @@ -1573,7 +1838,7 @@ public class ChatController: TelegramController { } }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -1696,7 +1961,7 @@ public class ChatController: TelegramController { waveformBuffer = MemoryBuffer(data: waveform) } - enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: nil)]).start() + let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.compressedData.count, attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)]), replyToMessageId: nil)]).start() strongSelf.audioRecorderFeedback?.success() strongSelf.audioRecorderFeedback = nil @@ -1727,12 +1992,37 @@ public class ChatController: TelegramController { } if let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(toId) { + self.loadingMessage.set(false) + self.messageIndexDisposable.set(nil) self.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: MessageIndex(message)) } else { - self.messageIndexDisposable.set((self.account.postbox.messageIndexAtId(toId) |> deliverOnMainQueue).start(next: { [weak self] index in + self.loadingMessage.set(true) + let historyView = chatHistoryViewForLocation(.InitialSearch(messageId: toId, count: 50), account: self.account, peerId: self.peerId, fixedCombinedReadState: nil, tagMask: nil, additionalData: []) + let signal = historyView + |> mapToSignal { historyView -> Signal in + switch historyView { + case .Loading: + return .complete() + case let .HistoryView(view, _, _, _): + for entry in view.entries { + if case let .MessageEntry(message, _, _, _) = entry { + if message.id == toId { + return .single(MessageIndex(message)) + } + } + } + return .single(nil) + } + } + |> take(1) + self.messageIndexDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] index in if let strongSelf = self, let index = index { strongSelf.chatDisplayNode.historyNode.scrollToMessage(from: fromIndex, to: index) } + }, completed: { [weak self] in + if let strongSelf = self { + strongSelf.loadingMessage.set(false) + } })) } } @@ -1854,6 +2144,23 @@ public class ChatController: TelegramController { } } + private func openPeerMention(_ name: String) { + let disposable: MetaDisposable + if let resolvePeerByNameDisposable = self.resolvePeerByNameDisposable { + disposable = resolvePeerByNameDisposable + } else { + disposable = MetaDisposable() + self.resolvePeerByNameDisposable = disposable + } + disposable.set((resolvePeerByName(account: self.account, name: name, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in + if let strongSelf = self { + if let peerId = peerId { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: nil)) + } + } + })) + } + private func unblockPeer() { let unblockingPeer = self.unblockingPeer unblockingPeer.set(true) @@ -1868,15 +2175,11 @@ public class ChatController: TelegramController { if let peer = self.presentationInterfaceState.peer { let title: String if let _ = peer as? TelegramGroup { - title = "Report spam and leave group" + title = self.presentationData.strings.Conversation_ReportSpamAndLeave } else if let peer = peer as? TelegramChannel { - if case .group = peer.info { - title = "Report spam and leave group" - } else { - title = "Report spam and leave channel" - } + title = self.presentationData.strings.Conversation_ReportSpamAndLeave } else { - title = "Report spam and delete chat" + title = self.presentationData.strings.Conversation_ReportSpamAndLeave } let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ @@ -1886,11 +2189,11 @@ public class ChatController: TelegramController { strongSelf.deleteChat(reportChatSpam: true) } }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) self.present(actionSheet, in: .window) } } @@ -1933,7 +2236,7 @@ public class ChatController: TelegramController { switch result { case let .externalUrl(url): if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - applicationContext.openUrl(url) + applicationContext.applicationBindings.openUrl(url) } case let .peer(peerId): strongSelf.openPeer(peerId: peerId, navigation: .chat(textInputState: nil), fromMessageId: nil) @@ -1965,7 +2268,7 @@ public class ChatController: TelegramController { return data.with { [weak self] data -> [UIPreviewActionItem] in var items: [UIPreviewActionItem] = [] - if let data = data { + if let data = data, let strongSelf = self { if let _ = data.peer as? TelegramUser { items.append(UIPreviewAction(title: "👍", style: .default, handler: { _, _ in if let strongSelf = self { @@ -1976,7 +2279,7 @@ public class ChatController: TelegramController { if let notificationSettings = data.notificationSettings as? TelegramPeerNotificationSettings { if case .unmuted = notificationSettings.muteState { - let muteItem = UIPreviewAction(title: "Mute", style: .default, handler: { _, _ in + let muteItem = UIPreviewAction(title: strongSelf.presentationData.strings.Conversation_Mute, style: .default, handler: { _, _ in if let strongSelf = self { let muteState: PeerMuteState = .muted(until: Int32.max) let _ = changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start() @@ -1984,7 +2287,7 @@ public class ChatController: TelegramController { }) items.append(muteItem) } else { - let unmuteItem = UIPreviewAction(title: "Unmute", style: .default, handler: { _, _ in + let unmuteItem = UIPreviewAction(title: strongSelf.presentationData.strings.Conversation_Unmute, style: .default, handler: { _, _ in if let strongSelf = self { let muteState: PeerMuteState = .unmuted let _ = changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start() diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 3cff0798ad..e2fa201eec 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -28,6 +28,13 @@ struct ChatInterfaceHighlightedState: Equatable { } } +public enum ChatControllerInteractionLongTapAction { + case url(String) + case mention(String) + case command(String) + case hashtag(String) +} + public final class ChatControllerInteraction { let openMessage: (MessageId) -> Void let openSecretMessagePreview: (MessageId) -> Void @@ -54,8 +61,10 @@ public final class ChatControllerInteraction { let updateInputState: ((ChatTextInputState) -> ChatTextInputState) -> Void let openMessageShareMenu: (MessageId) -> Void let presentController: (ViewController, Any?) -> Void + let callPeer: (PeerId) -> Void + let longTap: (ChatControllerInteractionLongTapAction) -> Void - public init(openMessage: @escaping (MessageId) -> Void, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, MessageId?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessageSelection: @escaping (MessageId) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (MessageId) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void) { + public init(openMessage: @escaping (MessageId) -> Void, openSecretMessagePreview: @escaping (MessageId) -> Void, closeSecretMessagePreview: @escaping () -> Void, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, MessageId?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessageSelection: @escaping (MessageId) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, sendGif: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (MessageId) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction) -> Void) { self.openMessage = openMessage self.openSecretMessagePreview = openSecretMessagePreview self.closeSecretMessagePreview = closeSecretMessagePreview @@ -78,5 +87,7 @@ public final class ChatControllerInteraction { self.updateInputState = updateInputState self.openMessageShareMenu = openMessageShareMenu self.presentController = presentController + self.callPeer = callPeer + self.longTap = longTap } } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 667d0a1fc5..c4e25efe3c 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -5,7 +5,7 @@ import SwiftSignalKit import Display import TelegramCore -private let backgroundImage = UIImage(bundleImageName: "Chat/Wallpapers/Builtin0") +private var backgroundImageForWallpaper: (TelegramWallpaper, UIImage)? private func shouldRequestLayoutOnPresentationInterfaceStateTransition(_ lhs: ChatPresentationInterfaceState, _ rhs: ChatPresentationInterfaceState) -> Bool { @@ -17,9 +17,13 @@ class ChatControllerNode: ASDisplayNode { let peerId: PeerId let controllerInteraction: ChatControllerInteraction + let navigationBar: NavigationBar + let backgroundNode: ASDisplayNode let historyNode: ChatHistoryListNode + private var searchNavigationNode: ChatSearchNavigationContentNode? + private let inputPanelBackgroundNode: ASDisplayNode private let inputPanelBackgroundSeparatorNode: ASDisplayNode @@ -39,7 +43,7 @@ class ChatControllerNode: ASDisplayNode { private var ignoreUpdateHeight = false - var chatPresentationInterfaceState = ChatPresentationInterfaceState() + var chatPresentationInterfaceState: ChatPresentationInterfaceState var requestUpdateChatInterfaceState: (Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _ in } var displayAttachmentMenu: () -> Void = { } @@ -55,10 +59,12 @@ class ChatControllerNode: ASDisplayNode { private var scheduledLayoutTransitionRequestId: Int = 0 private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? - init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction) { + init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, navigationBar: NavigationBar) { self.account = account self.peerId = peerId self.controllerInteraction = controllerInteraction + self.chatPresentationInterfaceState = chatPresentationInterfaceState + self.navigationBar = navigationBar self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -72,22 +78,52 @@ class ChatControllerNode: ASDisplayNode { self.historyNode = ChatHistoryListNode(account: account, peerId: peerId, tagMask: nil, messageId: messageId, controllerInteraction: controllerInteraction) self.inputPanelBackgroundNode = ASDisplayNode() - self.inputPanelBackgroundNode.backgroundColor = UIColor(0xF5F6F8) + self.inputPanelBackgroundNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor self.inputPanelBackgroundNode.isLayerBacked = true self.inputPanelBackgroundSeparatorNode = ASDisplayNode() - self.inputPanelBackgroundSeparatorNode.backgroundColor = UIColor(0xC9CDD1) + self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelStrokeColor self.inputPanelBackgroundSeparatorNode.isLayerBacked = true - self.navigateToLatestButton = ChatHistoryNavigationButtonNode() + self.navigateToLatestButton = ChatHistoryNavigationButtonNode(theme: self.chatPresentationInterfaceState.theme) self.navigateToLatestButton.alpha = 0.0 super.init(viewBlock: { return UITracingLayerView() }, didLoad: nil) - self.backgroundColor = UIColor(0xdee3e9) + self.backgroundColor = UIColor(rgb: 0xdee3e9) + + assert(Queue.mainQueue().isCurrent()) + + var backgroundImage: UIImage? + let wallpaper = chatPresentationInterfaceState.chatWallpaper + if wallpaper == backgroundImageForWallpaper?.0 { + backgroundImage = backgroundImageForWallpaper?.1 + } else { + switch wallpaper { + case .builtin: + backgroundImage = UIImage(bundleImageName: "Chat/Wallpapers/Builtin0")?.precomposed() + case let .color(color): + backgroundImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.setFillColor(UIColor(rgb: UInt32(bitPattern: color)).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + }) + case let .image(representations): + if let largest = largestImageRepresentation(representations) { + if let path = account.postbox.mediaBox.completedResourcePath(largest.resource) { + backgroundImage = UIImage(contentsOfFile: path)?.precomposed() + } + } + } + if let backgroundImage = backgroundImage { + backgroundImageForWallpaper = (wallpaper, backgroundImage) + } + } + self.backgroundNode.contents = backgroundImage?.cgImage + + self.addSubnode(self.backgroundNode) self.addSubnode(self.historyNode) @@ -188,6 +224,28 @@ class ChatControllerNode: ASDisplayNode { } self.containerLayoutAndNavigationBarHeight = (layout, navigationBarHeight) + let transitionIsAnimated: Bool + if case .immediate = transition { + transitionIsAnimated = false + } else { + transitionIsAnimated = true + } + + if let search = self.chatPresentationInterfaceState.search, let interfaceInteraction = self.interfaceInteraction { + var activate = false + if self.searchNavigationNode == nil { + activate = true + self.searchNavigationNode = ChatSearchNavigationContentNode(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, interaction: interfaceInteraction) + } + self.navigationBar.setContentNode(self.searchNavigationNode, animated: transitionIsAnimated) + if activate { + self.searchNavigationNode?.activate() + } + } else if let searchNavigationNode = self.searchNavigationNode { + self.searchNavigationNode = nil + self.navigationBar.setContentNode(nil, animated: transitionIsAnimated) + } + var dismissedTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode? var immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance = false if let titleAccessoryPanelNode = titlePanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.titleAccessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { @@ -549,6 +607,8 @@ class ChatControllerNode: ASDisplayNode { let updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState self.chatPresentationInterfaceState = chatPresentationInterfaceState + self.navigateToLatestButton.updateTheme(theme: chatPresentationInterfaceState.theme) + let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || chatPresentationInterfaceState.interfaceState.editMessage != nil var extendedSearchLayout = false if let inputQueryResult = chatPresentationInterfaceState.inputQueryResult { @@ -628,6 +688,7 @@ class ChatControllerNode: ASDisplayNode { return (.none, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) }) } + self.searchNavigationNode?.deactivate() } private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) { @@ -645,9 +706,9 @@ class ChatControllerNode: ASDisplayNode { self.setNeedsLayout() } - func loadInputPanels() { + func loadInputPanels(theme: PresentationTheme, strings: PresentationStrings) { if self.inputMediaNode == nil { - let inputNode = ChatMediaInputNode(account: self.account, controllerInteraction: self.controllerInteraction) + let inputNode = ChatMediaInputNode(account: self.account, controllerInteraction: self.controllerInteraction, theme: theme, strings: strings) inputNode.interfaceInteraction = interfaceInteraction self.inputMediaNode = inputNode let _ = inputNode.updateLayout(width: self.bounds.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) diff --git a/TelegramUI/ChatDateSelectionSheet.swift b/TelegramUI/ChatDateSelectionSheet.swift new file mode 100644 index 0000000000..64db5c7c19 --- /dev/null +++ b/TelegramUI/ChatDateSelectionSheet.swift @@ -0,0 +1,114 @@ +import Foundation +import Display +import AsyncDisplayKit +import UIKit +import SwiftSignalKit +import Photos + +final class ChatDateSelectionSheet: ActionSheetController { + private let theme: PresentationTheme + private let strings: PresentationStrings + + private let _ready = Promise() + override var ready: Promise { + return self._ready + } + + init(theme: PresentationTheme, strings: PresentationStrings, completion: @escaping (Int32) -> Void) { + self.theme = theme + self.strings = strings + + super.init() + + self._ready.set(.single(true)) + + var updatedValue: Int32? + self.setItemGroups([ + ActionSheetItemGroup(items: [ + ChatDateSelectorItem(theme: theme, strings: strings, valueChanged: { value in + updatedValue = value + }), + ActionSheetButtonItem(title: strings.Common_Search, action: { [weak self] in + self?.dismissAnimated() + if let updatedValue = updatedValue { + completion(updatedValue) + } + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in + self?.dismissAnimated() + }), + ]) + ]) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class ChatDateSelectorItem: ActionSheetItem { + let theme: PresentationTheme + let strings: PresentationStrings + + let valueChanged: (Int32) -> Void + + init(theme: PresentationTheme, strings: PresentationStrings, valueChanged: @escaping (Int32) -> Void) { + self.theme = theme + self.strings = strings + self.valueChanged = valueChanged + } + + func node() -> ActionSheetItemNode { + return ChatDateSelectorItemNode(theme: self.theme, strings: self.strings, valueChanged: self.valueChanged) + } + + func updateNode(_ node: ActionSheetItemNode) { + } +} + +private final class ChatDateSelectorItemNode: ActionSheetItemNode { + private let theme: PresentationTheme + private let strings: PresentationStrings + + private let pickerView: UIDatePicker + + private let valueChanged: (Int32) -> Void + + private var currentValue: Int32 { + return Int32(self.pickerView.date.timeIntervalSince1970) + } + + init(theme: PresentationTheme, strings: PresentationStrings, valueChanged: @escaping (Int32) -> Void) { + self.theme = theme + self.strings = strings + self.valueChanged = valueChanged + + self.pickerView = UIDatePicker() + self.pickerView.datePickerMode = .date + self.pickerView.locale = Locale(identifier: strings.languageCode) + + self.pickerView.maximumDate = Date(timeIntervalSinceNow: 2.0) + self.pickerView.minimumDate = Date(timeIntervalSinceNow: 1376438400.0) + + super.init() + + self.view.addSubview(self.pickerView) + self.pickerView.addTarget(self, action: #selector(self.pickerChanged), for: .valueChanged) + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 157.0) + } + + override func layout() { + super.layout() + + self.pickerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.bounds.size.width, height: 180.0)) + } + + @objc func pickerChanged() { + self.valueChanged(self.currentValue) + } +} diff --git a/TelegramUI/ChatDocumentGalleryItem.swift b/TelegramUI/ChatDocumentGalleryItem.swift index a012b72e0f..a99efe9640 100644 --- a/TelegramUI/ChatDocumentGalleryItem.swift +++ b/TelegramUI/ChatDocumentGalleryItem.swift @@ -7,17 +7,21 @@ import TelegramCore class ChatDocumentGalleryItem: GalleryItem { let account: Account + let theme: PresentationTheme + let strings: PresentationStrings let message: Message let location: MessageHistoryEntryLocation? - init(account: Account, message: Message, location: MessageHistoryEntryLocation?) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, message: Message, location: MessageHistoryEntryLocation?) { self.account = account + self.theme = theme + self.strings = strings self.message = message self.location = location } func node() -> GalleryItemNode { - let node = ChatDocumentGalleryItemNode(account: self.account) + let node = ChatDocumentGalleryItemNode(account: self.account, theme: self.theme, strings: self.strings) for media in self.message.media { if let file = media as? TelegramMediaFile { @@ -61,7 +65,7 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { private let footerContentNode: ChatItemGalleryFooterContentNode - init(account: Account) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { if #available(iOS 9.0, *) { let webView = WKWebView() self.webView = webView @@ -70,7 +74,7 @@ class ChatDocumentGalleryItemNode: GalleryItemNode { webView.scalesPageToFit = true self.webView = webView } - self.footerContentNode = ChatItemGalleryFooterContentNode(account: account) + self.footerContentNode = ChatItemGalleryFooterContentNode(account: account, theme: theme, strings: strings) super.init() diff --git a/TelegramUI/ChatEmptyItem.swift b/TelegramUI/ChatEmptyItem.swift index 2013b4b2e2..c7e16f9858 100644 --- a/TelegramUI/ChatEmptyItem.swift +++ b/TelegramUI/ChatEmptyItem.swift @@ -6,9 +6,16 @@ import Postbox import TelegramCore private let messageFont = Font.medium(14.0) -private let iconImage = UIImage(bundleImageName: "Chat/EmptyChatIcon")?.precomposed() final class ChatEmptyItem: ListViewItem { + fileprivate let theme: PresentationTheme + fileprivate let strings: PresentationStrings + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + } + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { let node = ChatEmptyItemNode() @@ -50,8 +57,6 @@ final class ChatEmptyItem: ListViewItem { } } -private let backgroundImage = generateStretchableFilledCircleImage(radius: 14.0, color: UIColor(0x748391, 0.45)) - final class ChatEmptyItemNode: ListViewItemNode { var controllerInteraction: ChatControllerInteraction? @@ -60,15 +65,15 @@ final class ChatEmptyItemNode: ListViewItemNode { let iconNode: ASImageNode let textNode: TextNode + private var theme: PresentationTheme? + init() { self.offsetContainer = ASDisplayNode() self.backgroundNode = ASImageNode() self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = backgroundImage self.iconNode = ASImageNode() - self.iconNode.image = iconImage self.textNode = TextNode() super.init(layerBacked: false, dynamicBounce: true, rotated: true) @@ -84,9 +89,20 @@ final class ChatEmptyItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ChatEmptyItem, _ width: CGFloat) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) + let currentTheme = self.theme return { [weak self] item, width in - let attributedText = NSAttributedString(string: "No messages\nhere yet...", font: messageFont, textColor: .white, paragraphAlignment: .center) + var updatedBackgroundImage: UIImage? + var updatedIconImage: UIImage? + let iconImage = PresentationResourcesChat.chatEmptyItemIconImage(item.theme) + + if currentTheme !== item.theme { + updatedBackgroundImage = PresentationResourcesChat.chatEmptyItemBackgroundImage(item.theme) + updatedIconImage = iconImage + } + + let attributedText = NSAttributedString(string: item.strings.Conversation_EmptyPlaceholder, font: messageFont, textColor: item.theme.chat.serviceMessage.serviceMessagePrimaryTextColor, paragraphAlignment: .center) + let horizontalEdgeInset: CGFloat = 10.0 let horizontalContentInset: CGFloat = 12.0 let verticalItemInset: CGFloat = 10.0 @@ -109,6 +125,16 @@ final class ChatEmptyItemNode: ListViewItemNode { let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: imageSize.height + imageSpacing + textLayout.size.height + verticalItemInset * 2.0 + verticalContentInset * 2.0 + 4.0), insets: UIEdgeInsets()) return (itemLayout, { _ in if let strongSelf = self { + strongSelf.theme = item.theme + + if let updatedBackgroundImage = updatedBackgroundImage { + strongSelf.backgroundNode.image = updatedBackgroundImage + } + + if let updatedIconImage = updatedIconImage { + strongSelf.iconNode.image = updatedIconImage + } + let _ = textApply() strongSelf.offsetContainer.frame = CGRect(origin: CGPoint(), size: itemLayout.contentSize) strongSelf.backgroundNode.frame = backgroundFrame diff --git a/TelegramUI/ChatHistoryEntriesForView.swift b/TelegramUI/ChatHistoryEntriesForView.swift index a0b00e77c6..e174acdba7 100644 --- a/TelegramUI/ChatHistoryEntriesForView.swift +++ b/TelegramUI/ChatHistoryEntriesForView.swift @@ -2,22 +2,30 @@ import Foundation import Postbox import TelegramCore -func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: Bool, includeChatInfoEntry: Bool) -> [ChatHistoryEntry] { +func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: Bool, includeEmptyEntry: Bool, includeChatInfoEntry: Bool, theme: PresentationTheme, strings: PresentationStrings) -> [ChatHistoryEntry] { var entries: [ChatHistoryEntry] = [] for entry in view.entries { switch entry { case let .HoleEntry(hole, _): - entries.append(.HoleEntry(hole)) + entries.append(.HoleEntry(hole, theme, strings)) case let .MessageEntry(message, read, _, monthLocation): - entries.append(.MessageEntry(message, read, monthLocation)) + var isClearHistory = false + if !message.media.isEmpty { + if let action = message.media[0] as? TelegramMediaAction, case .historyCleared = action.action { + isClearHistory = true + } + } + if !isClearHistory { + entries.append(.MessageEntry(message, theme, strings, read, monthLocation)) + } } } if let maxReadIndex = view.maxReadIndex, includeUnreadEntry { var inserted = false var i = 0 - let unreadEntry: ChatHistoryEntry = .UnreadEntry(maxReadIndex) + let unreadEntry: ChatHistoryEntry = .UnreadEntry(maxReadIndex, theme, strings) for entry in entries { if entry > unreadEntry { entries.insert(unreadEntry, at: i) @@ -42,9 +50,9 @@ func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: B } } if let cachedPeerData = cachedPeerData as? CachedUserData, let botInfo = cachedPeerData.botInfo, !botInfo.description.isEmpty { - entries.insert(.ChatInfoEntry(botInfo.description), at: 0) - } else if view.entries.isEmpty { - entries.insert(.EmptyChatInfoEntry, at: 0) + entries.insert(.ChatInfoEntry(botInfo.description, theme, strings), at: 0) + } else if view.entries.isEmpty && includeEmptyEntry { + entries.insert(.EmptyChatInfoEntry(theme, strings), at: 0) } } } diff --git a/TelegramUI/ChatHistoryEntry.swift b/TelegramUI/ChatHistoryEntry.swift index 8a6443def3..558725f642 100644 --- a/TelegramUI/ChatHistoryEntry.swift +++ b/TelegramUI/ChatHistoryEntry.swift @@ -2,17 +2,17 @@ import Postbox import TelegramCore enum ChatHistoryEntry: Identifiable, Comparable { - case HoleEntry(MessageHistoryHole) - case MessageEntry(Message, Bool, MessageHistoryEntryMonthLocation?) - case UnreadEntry(MessageIndex) - case ChatInfoEntry(String) - case EmptyChatInfoEntry + case HoleEntry(MessageHistoryHole, PresentationTheme, PresentationStrings) + case MessageEntry(Message, PresentationTheme, PresentationStrings, Bool, MessageHistoryEntryMonthLocation?) + case UnreadEntry(MessageIndex, PresentationTheme, PresentationStrings) + case ChatInfoEntry(String, PresentationTheme, PresentationStrings) + case EmptyChatInfoEntry(PresentationTheme, PresentationStrings) var stableId: UInt64 { switch self { - case let .HoleEntry(hole): + case let .HoleEntry(hole, _, _): return UInt64(hole.stableId) | ((UInt64(1) << 40)) - case let .MessageEntry(message, _, _): + case let .MessageEntry(message, _, _, _, _): return UInt64(message.stableId) | ((UInt64(2) << 40)) case .UnreadEntry: return UInt64(3) << 40 @@ -25,11 +25,11 @@ enum ChatHistoryEntry: Identifiable, Comparable { var index: MessageIndex { switch self { - case let .HoleEntry(hole): + case let .HoleEntry(hole, _, _): return hole.maxIndex - case let .MessageEntry(message, _, _): + case let .MessageEntry(message, _, _, _, _): return MessageIndex(message) - case let .UnreadEntry(index): + case let .UnreadEntry(index, _, _): return index case .ChatInfoEntry: return MessageIndex.absoluteLowerBound() @@ -41,16 +41,21 @@ enum ChatHistoryEntry: Identifiable, Comparable { func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { switch lhs { - case let .HoleEntry(lhsHole): - switch rhs { - case let .HoleEntry(rhsHole) where lhsHole == rhsHole: - return true - default: - return false + case let .HoleEntry(lhsHole, lhsTheme, lhsStrings): + if case let .HoleEntry(rhsHole, rhsTheme, rhsStrings) = rhs, lhsHole == rhsHole, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + return true + } else { + return false } - case let .MessageEntry(lhsMessage, lhsRead, _): + case let .MessageEntry(lhsMessage, lhsTheme, lhsStrings, lhsRead, _): switch rhs { - case let .MessageEntry(rhsMessage, rhsRead, _) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead: + case let .MessageEntry(rhsMessage, rhsTheme, rhsStrings, rhsRead, _) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead: + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if lhsMessage.stableVersion != rhsMessage.stableVersion { return false } @@ -78,21 +83,20 @@ func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { default: return false } - case let .UnreadEntry(lhsIndex): - switch rhs { - case let .UnreadEntry(rhsIndex) where lhsIndex == rhsIndex: - return true - default: - return false - } - case let .ChatInfoEntry(text): - if case .ChatInfoEntry(text) = rhs { + case let .UnreadEntry(lhsIndex, lhsTheme, lhsStrings): + if case let .UnreadEntry(rhsIndex, rhsTheme, rhsStrings) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings { return true } else { return false } - case .EmptyChatInfoEntry: - if case .EmptyChatInfoEntry = rhs { + case let .ChatInfoEntry(lhsText, lhsTheme, lhsStrings): + if case let .ChatInfoEntry(rhsText, rhsTheme, rhsStrings) = rhs, lhsText == rhsText, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + return true + } else { + return false + } + case let .EmptyChatInfoEntry(lhsTheme, lhsStrings): + if case let .EmptyChatInfoEntry(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings { return true } else { return false diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index 2aa08fe469..456c040dd7 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -15,11 +15,11 @@ struct ChatHistoryGridViewTransition { let stationaryItems: GridNodeStationaryItems } -private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionInsertEntry]) -> [GridNodeInsertItem] { +private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionInsertEntry], theme: PresentationTheme, strings: PresentationStrings) -> [GridNodeInsertItem] { return entries.map { entry -> GridNodeInsertItem in switch entry.entry { - case let .MessageEntry(message, _, _): - return GridNodeInsertItem(index: entry.index, item: GridMessageItem(account: account, message: message, controllerInteraction: controllerInteraction), previousIndex: entry.previousIndex) + case let .MessageEntry(message, _, _, _, _): + return GridNodeInsertItem(index: entry.index, item: GridMessageItem(theme: theme, strings: strings, account: account, message: message, controllerInteraction: controllerInteraction), previousIndex: entry.previousIndex) case .HoleEntry: return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) case .UnreadEntry: @@ -32,11 +32,11 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt } } -private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [GridNodeUpdateItem] { +private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionUpdateEntry], theme: PresentationTheme, strings: PresentationStrings) -> [GridNodeUpdateItem] { return entries.map { entry -> GridNodeUpdateItem in switch entry.entry { - case let .MessageEntry(message, _, _): - return GridNodeUpdateItem(index: entry.index, item: GridMessageItem(account: account, message: message, controllerInteraction: controllerInteraction)) + case let .MessageEntry(message, _, _, _, _): + return GridNodeUpdateItem(index: entry.index, item: GridMessageItem(theme: theme, strings: strings, account: account, message: message, controllerInteraction: controllerInteraction)) case .HoleEntry: return GridNodeUpdateItem(index: entry.index, item: GridHoleItem()) case .UnreadEntry: @@ -49,7 +49,7 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt } } -private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, transition: ChatHistoryViewTransition, from: ChatHistoryView?) -> ChatHistoryGridViewTransition { +private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, transition: ChatHistoryViewTransition, from: ChatHistoryView?, theme: PresentationTheme, strings: PresentationStrings) -> ChatHistoryGridViewTransition { var mappedScrollToItem: GridNodeScrollToItem? if let scrollToItem = transition.scrollToItem { let mappedPosition: GridNodeScrollToItemPosition @@ -120,7 +120,7 @@ private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerI var topOffsetWithinMonth: Int = 0 if let lastEntry = transition.historyView.filteredEntries.last { switch lastEntry { - case let .MessageEntry(_, _, monthLocation): + case let .MessageEntry(_, _, _, _, monthLocation): if let monthLocation = monthLocation { topOffsetWithinMonth = Int(monthLocation.indexInMonth) } @@ -129,7 +129,7 @@ private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerI } } - return ChatHistoryGridViewTransition(historyView: transition.historyView, topOffsetWithinMonth: topOffsetWithinMonth, deleteItems: transition.deleteItems.map { $0.index }, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.updateEntries), scrollToItem: mappedScrollToItem, stationaryItems: stationaryItems) + return ChatHistoryGridViewTransition(historyView: transition.historyView, topOffsetWithinMonth: topOffsetWithinMonth, deleteItems: transition.deleteItems.map { $0.index }, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.insertEntries, theme: theme, strings: strings), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.updateEntries, theme: theme, strings: strings), scrollToItem: mappedScrollToItem, stationaryItems: stationaryItems) } private func itemSizeForContainerLayout(size: CGSize) -> CGSize { @@ -171,14 +171,21 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() + private var presentationData: PresentationData + private let themeAndStringsPromise = Promise<(PresentationTheme, PresentationStrings)>() + public init(account: Account, peerId: PeerId, messageId: MessageId?, tagMask: MessageTags?, controllerInteraction: ChatControllerInteraction) { self.account = account self.peerId = peerId self.messageId = messageId self.tagMask = tagMask + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + super.init() + self.themeAndStringsPromise.set(.single((self.presentationData.theme, self.presentationData.strings))) + self.floatingSections = true //self.preloadPages = false @@ -193,7 +200,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { let previousView = Atomic(value: nil) - let historyViewTransition = historyViewUpdate |> mapToQueue { [weak self] update -> Signal in + let historyViewTransition = combineLatest(historyViewUpdate, self.themeAndStringsPromise.get()) |> mapToQueue { [weak self] update, themeAndStrings -> Signal in switch update { case .Loading: Queue.mainQueue().async { [weak self] in @@ -226,10 +233,10 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } } - let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false, includeChatInfoEntry: false)) + let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false, includeEmptyEntry: false, includeChatInfoEntry: false, theme: themeAndStrings.0, strings: themeAndStrings.1)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous, theme: themeAndStrings.0, strings: themeAndStrings.1) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -309,7 +316,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { if let historyView = self.historyView { - for case let .MessageEntry(message, _, _) in historyView.filteredEntries where message.id == id { + for case let .MessageEntry(message, _, _, _, _) in historyView.filteredEntries where message.id == id { return message } } @@ -372,7 +379,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { var updateLayout: GridNodeUpdateLayout? if let updateSizeAndInsets = updateSizeAndInsets { - updateLayout = GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, type: .fixed(itemSize: CGSize(width: 200.0, height: 200.0))), transition: .immediate) + updateLayout = GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, type: .fixed(itemSize: CGSize(width: 200.0, height: 200.0), lineSpacing: 0.0)), transition: .immediate) } self.transaction(GridNodeTransaction(deleteItems: mappedTransition.deleteItems, insertItems: mappedTransition.insertItems, updateItems: mappedTransition.updateItems, scrollToItem: mappedTransition.scrollToItem, updateLayout: updateLayout, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: mappedTransition.topOffsetWithinMonth), completion: completion) @@ -383,7 +390,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) { - self.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, type: .fixed(itemSize: itemSizeForContainerLayout(size: updateSizeAndInsets.size))), transition: .immediate), stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, type: .fixed(itemSize: itemSizeForContainerLayout(size: updateSizeAndInsets.size), lineSpacing: 0.0)), transition: .immediate), stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in }) if !self.dequeuedInitialTransitionOnLayout { self.dequeuedInitialTransitionOnLayout = true diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index b001d10820..68c8daa9c9 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -88,7 +88,7 @@ struct ChatHistoryListViewTransition { private func maxMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> (incoming: MessageIndex?, overall: MessageIndex?) { var overall: MessageIndex? for i in (indexRange.0 ... indexRange.1).reversed() { - if case let .MessageEntry(message, _, _) = entries[i] { + if case let .MessageEntry(message, _, _, _, _) = entries[i] { if overall == nil { overall = MessageIndex(message) } @@ -103,30 +103,30 @@ private func maxMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { - case let .MessageEntry(message, read, _): + case let .MessageEntry(message, theme, strings, read, _): let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message, read: read) + item = ChatMessageItem(theme: theme, strings: strings, account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message, read: read) case .list: - item = ListMessageItem(account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message) + item = ListMessageItem(theme: theme, account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message) } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) - case .HoleEntry: + case let .HoleEntry(_, theme, strings): let item: ListViewItem switch mode { case .bubbles: - item = ChatHoleItem(index: entry.entry.index) + item = ChatHoleItem(index: entry.entry.index, theme: theme, strings: strings) case .list: item = ListMessageHoleItem() } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) - case .UnreadEntry: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index), directionHint: entry.directionHint) - case let .ChatInfoEntry(text): - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(text: text, controllerInteraction: controllerInteraction), directionHint: entry.directionHint) - case .EmptyChatInfoEntry: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatEmptyItem(), directionHint: entry.directionHint) + case let .UnreadEntry(_, theme, strings): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, theme: theme, strings: strings), directionHint: entry.directionHint) + case let .ChatInfoEntry(text, theme, strings): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(text: text, controllerInteraction: controllerInteraction, theme: theme, strings: strings), directionHint: entry.directionHint) + case let .EmptyChatInfoEntry(theme, strings): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatEmptyItem(theme: theme, strings: strings), directionHint: entry.directionHint) } } } @@ -134,30 +134,30 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { - case let .MessageEntry(message, read, _): + case let .MessageEntry(message, theme, strings, read, _): let item: ListViewItem switch mode { case .bubbles: - item = ChatMessageItem(account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message, read: read) + item = ChatMessageItem(theme: theme, strings: strings, account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message, read: read) case .list: - item = ListMessageItem(account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message) + item = ListMessageItem(theme: theme, account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message) } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) - case .HoleEntry: + case let .HoleEntry(_, theme, strings): let item: ListViewItem switch mode { case .bubbles: - item = ChatHoleItem(index: entry.entry.index) + item = ChatHoleItem(index: entry.entry.index, theme: theme, strings: strings) case .list: item = ListMessageHoleItem() } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) - case .UnreadEntry: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index), directionHint: entry.directionHint) - case let .ChatInfoEntry(text): - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(text: text, controllerInteraction: controllerInteraction), directionHint: entry.directionHint) - case .EmptyChatInfoEntry: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatEmptyItem(), directionHint: entry.directionHint) + case let .UnreadEntry(_, theme, strings): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, theme: theme, strings: strings), directionHint: entry.directionHint) + case let .ChatInfoEntry(text, theme, strings): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(text: text, controllerInteraction: controllerInteraction, theme: theme, strings: strings), directionHint: entry.directionHint) + case let .EmptyChatInfoEntry(theme, strings): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatEmptyItem(theme: theme, strings: strings), directionHint: entry.directionHint) } } } @@ -229,6 +229,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var scrolledToIndex: ((MessageIndex) -> Void)? + private var currentPresentationData: PresentationData + private var themeAndStrings: Promise<(PresentationTheme, PresentationStrings)> + private var presentationDataDisposable: Disposable? + public init(account: Account, peerId: PeerId, tagMask: MessageTags?, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode = .bubbles) { self.account = account self.peerId = peerId @@ -236,6 +240,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.controllerInteraction = controllerInteraction self.mode = mode + self.currentPresentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.themeAndStrings = Promise((self.currentPresentationData.theme, self.currentPresentationData.strings)) + super.init() //self.stackFromBottom = true @@ -277,7 +285,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let previousView = Atomic(value: nil) - let historyViewTransition = historyViewUpdate |> mapToQueue { [weak self] update -> Signal in + let historyViewTransition = combineLatest(historyViewUpdate, self.themeAndStrings.get()) |> mapToQueue { [weak self] update, themeAndStrings -> Signal in let initialData: ChatHistoryCombinedInitialData? switch update { case let .Loading(data): @@ -321,7 +329,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles, includeChatInfoEntry: true)) + let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles, includeEmptyEntry: mode == .bubbles, includeChatInfoEntry: true, theme: themeAndStrings.0, strings: themeAndStrings.1)) let previous = previousView.swap(processedView) return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) @@ -374,7 +382,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var messageIdsWithViewCount: [MessageId] = [] for i in (indexRange.0 ... indexRange.1) { - if case let .MessageEntry(message, _, _) = historyView.filteredEntries[i] { + if case let .MessageEntry(message, _, _, _, _) = historyView.filteredEntries[i] { inner: for attribute in message.attributes { if attribute is ViewCountMessageAttribute { messageIdsWithViewCount.append(message.id) @@ -410,6 +418,25 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } } + + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.currentPresentationData.theme + let previousStrings = strongSelf.currentPresentationData.strings + + strongSelf.currentPresentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.forEachItemHeaderNode { itemHeaderNode in + if let dateNode = itemHeaderNode as? ChatMessageDateHeaderNode { + dateNode.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) + } + } + strongSelf.themeAndStrings.set(.single((presentationData.theme, presentationData.strings))) + } + } + }) } deinit { @@ -435,7 +462,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var index = 0 for entry in historyView.filteredEntries.reversed() { if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { - if case let .MessageEntry(message, _, _) = entry { + if case let .MessageEntry(message, _, _, _, _) = entry { return message } } @@ -443,7 +470,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - for case let .MessageEntry(message, _, _) in historyView.filteredEntries { + for case let .MessageEntry(message, _, _, _, _) in historyView.filteredEntries { return message } } @@ -452,7 +479,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { if let historyView = self.historyView { - for case let .MessageEntry(message, _, _) in historyView.filteredEntries where message.id == id { + for case let .MessageEntry(message, _, _, _, _) in historyView.filteredEntries where message.id == id { return message } } diff --git a/TelegramUI/ChatHistoryNavigationButtonNode.swift b/TelegramUI/ChatHistoryNavigationButtonNode.swift index 1b3caaa354..b1204de4bd 100644 --- a/TelegramUI/ChatHistoryNavigationButtonNode.swift +++ b/TelegramUI/ChatHistoryNavigationButtonNode.swift @@ -2,27 +2,6 @@ import Foundation import AsyncDisplayKit import Display -private func generateBackgroundImage() -> UIImage? { - return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: size.width - 1.0, height: size.height - 1.0))) - context.setLineWidth(0.5) - context.setStrokeColor(UIColor(0x000000, 0.15).cgColor) - context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.25, y: 0.25), size: CGSize(width: size.width - 0.5, height: size.height - 0.5))) - context.setStrokeColor(UIColor(0x88888D).cgColor) - context.setLineWidth(1.5) - - let position = CGPoint(x: 9.0 - 0.5, y: 23.0) - context.move(to: CGPoint(x: position.x + 1.0, y: position.y - 1.0)) - context.addLine(to: CGPoint(x: position.x + 10.0, y: position.y - 10.0)) - context.addLine(to: CGPoint(x: position.x + 19.0, y: position.y - 1.0)) - context.strokePath() - }) -} - -private let backgroundImage = generateBackgroundImage() -private let badgeImage = generateStretchableFilledCircleImage(diameter: 18.0, color: UIColor(0x007ee5), backgroundColor: nil) private let badgeFont = Font.regular(13.0) class ChatHistoryNavigationButtonNode: ASControlNode { @@ -40,17 +19,21 @@ class ChatHistoryNavigationButtonNode: ASControlNode { } } - override init() { + private var theme: PresentationTheme + + init(theme: PresentationTheme) { + self.theme = theme + self.imageNode = ASImageNode() self.imageNode.displayWithoutProcessing = true - self.imageNode.image = backgroundImage + self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) self.imageNode.isLayerBacked = true self.badgeBackgroundNode = ASImageNode() self.badgeBackgroundNode.isLayerBacked = true self.badgeBackgroundNode.displayWithoutProcessing = true self.badgeBackgroundNode.displaysAsynchronously = false - self.badgeBackgroundNode.image = badgeImage + self.badgeBackgroundNode.image = PresentationResourcesChat.chatHistoryNavigationButtonBadgeImage(theme) self.badgeTextNode = ASTextNode() self.badgeTextNode.maximumNumberOfLines = 1 @@ -70,6 +53,19 @@ class ChatHistoryNavigationButtonNode: ASControlNode { self.addTarget(self, action: #selector(onTap), forControlEvents: .touchUpInside) } + func updateTheme(theme: PresentationTheme) { + if self.theme !== theme { + self.theme = theme + + self.imageNode.image = PresentationResourcesChat.chatHistoryNavigationButtonImage(theme) + self.badgeBackgroundNode.image = PresentationResourcesChat.chatHistoryNavigationButtonBadgeImage(theme) + + if let string = self.badgeTextNode.attributedText?.string { + self.badgeTextNode.attributedText = NSAttributedString(string: string, font: badgeFont, textColor: theme.chat.historyNavigation.badgeTextColor) + } + } + } + @objc func onTap() { if let tapped = self.tapped { tapped() @@ -78,7 +74,7 @@ class ChatHistoryNavigationButtonNode: ASControlNode { private func layoutBadge() { if !self.badge.isEmpty { - self.badgeTextNode.attributedText = NSAttributedString(string: self.badge, font: badgeFont, textColor: .white) + self.badgeTextNode.attributedText = NSAttributedString(string: self.badge, font: badgeFont, textColor: self.theme.chat.historyNavigation.badgeTextColor) self.badgeBackgroundNode.isHidden = false self.badgeTextNode.isHidden = false diff --git a/TelegramUI/ChatHoleItem.swift b/TelegramUI/ChatHoleItem.swift index 19c596f579..0b698419f5 100644 --- a/TelegramUI/ChatHoleItem.swift +++ b/TelegramUI/ChatHoleItem.swift @@ -5,23 +5,19 @@ import AsyncDisplayKit import Display import SwiftSignalKit -private func backgroundImage(color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context -> Void in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0x748391, 0.45).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - })?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8) -} - private let titleFont = UIFont.systemFont(ofSize: 13.0) class ChatHoleItem: ListViewItem { let index: MessageIndex + let theme: PresentationTheme + let strings: PresentationStrings let header: ChatMessageDateHeader - init(index: MessageIndex) { + init(index: MessageIndex, theme: PresentationTheme, strings: PresentationStrings) { self.index = index - self.header = ChatMessageDateHeader(timestamp: index.timestamp) + self.theme = theme + self.strings = strings + self.header = ChatMessageDateHeader(timestamp: index.timestamp, theme: theme, strings: strings) } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { @@ -58,12 +54,11 @@ class ChatHoleItemNode: ListViewItemNode { super.init(layerBacked: true) - self.backgroundNode.image = backgroundImage(color: UIColor(0x007ee5)) self.addSubnode(self.backgroundNode) self.addSubnode(self.labelNode) - self.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0.0, 0.0, 1.0) + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) } override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { @@ -79,8 +74,14 @@ class ChatHoleItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ChatHoleItem, _ width: CGFloat, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants + let currentItem = self.item return { item, width, dateAtBottom in - let (size, apply) = labelLayout(NSAttributedString(string: "Loading", font: titleFont, textColor: UIColor.white), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + var updatedBackground: UIImage? + if item.theme !== currentItem?.theme { + updatedBackground = PresentationResourcesChat.chatServiceBubbleFillImage(item.theme) + } + + let (size, apply) = labelLayout(NSAttributedString(string: item.strings.Channel_NotificationLoading, font: titleFont, textColor: item.theme.chat.serviceMessage.serviceMessagePrimaryTextColor), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0) @@ -88,6 +89,10 @@ class ChatHoleItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + if let updatedBackground = updatedBackground { + strongSelf.backgroundNode.image = updatedBackground + } + let _ = apply() strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize) diff --git a/TelegramUI/ChatImageGalleryItem.swift b/TelegramUI/ChatImageGalleryItem.swift index 157fdaba91..5d54e5ab69 100644 --- a/TelegramUI/ChatImageGalleryItem.swift +++ b/TelegramUI/ChatImageGalleryItem.swift @@ -7,17 +7,21 @@ import TelegramCore class ChatImageGalleryItem: GalleryItem { let account: Account + let theme: PresentationTheme + let strings: PresentationStrings let message: Message let location: MessageHistoryEntryLocation? - init(account: Account, message: Message, location: MessageHistoryEntryLocation?) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, message: Message, location: MessageHistoryEntryLocation?) { self.account = account + self.theme = theme + self.strings = strings self.message = message self.location = location } func node() -> GalleryItemNode { - let node = ChatImageGalleryItemNode(account: self.account) + let node = ChatImageGalleryItemNode(account: self.account, theme: self.theme, strings: self.strings) for media in self.message.media { if let image = media as? TelegramMediaImage { @@ -68,11 +72,11 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private var fetchDisposable = MetaDisposable() - init(account: Account) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { self.account = account self.imageNode = TransformImageNode() - self.footerContentNode = ChatItemGalleryFooterContentNode(account: account) + self.footerContentNode = ChatItemGalleryFooterContentNode(account: account, theme: theme, strings: strings) super.init() @@ -122,7 +126,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.imageNode.alphaTransitionOnFirstUpdate = false let displaySize = largestSize.dividedByScreenScale() self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.setSignal(account: account, signal: chatMessageImageFile(account: account, file: file, progressive: true), dispatchOnDisplayLink: false) + self.imageNode.setSignal(account: account, signal: chatMessageImageFile(account: account, file: file, progressive: false), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize, self.imageNode) } else { self._ready.set(.single(Void())) diff --git a/TelegramUI/ChatInfoTitlePanelNode.swift b/TelegramUI/ChatInfoTitlePanelNode.swift index f36bd11e75..00502c748c 100644 --- a/TelegramUI/ChatInfoTitlePanelNode.swift +++ b/TelegramUI/ChatInfoTitlePanelNode.swift @@ -10,16 +10,16 @@ private enum ChatInfoTitleButton { case mute case unmute - var title: String { + func title(_ strings: PresentationStrings) -> String { switch self { case .search: - return "Search" + return strings.Common_Search case .info: - return "Info" + return strings.Conversation_Info case .mute: - return "Mute" + return strings.Conversation_Mute case .unmute: - return "Unmute" + return strings.Conversation_Unmute } } } @@ -33,25 +33,31 @@ private func peerButtons(_ peer: Peer) -> [ChatInfoTitleButton] { } final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { - private let separatorNode: ASDisplayNode + private var theme: PresentationTheme? + private let separatorNode: ASDisplayNode private var buttons: [(ChatInfoTitleButton, UIButton)] = [] override init() { self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) self.separatorNode.isLayerBacked = true super.init() - self.backgroundColor = UIColor(0xF5F6F8) - self.addSubnode(self.separatorNode) } override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + let themeUpdated = self.theme !== interfaceState.theme + self.theme = interfaceState.theme + let panelHeight: CGFloat = 44.0 + if themeUpdated { + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor + self.backgroundColor = interfaceState.theme.rootController.navigationBar.backgroundColor + } + let updatedButtons: [ChatInfoTitleButton] if let peer = interfaceState.peer { updatedButtons = peerButtons(peer) @@ -71,17 +77,17 @@ final class ChatInfoTitlePanelNode: ChatTitleAccessoryPanelNode { } } - if buttonsUpdated { + if buttonsUpdated || themeUpdated { for (_, view) in self.buttons { view.removeFromSuperview() } self.buttons.removeAll() for button in updatedButtons { let view = UIButton() - view.setTitle(button.title, for: []) + view.setTitle(button.title(interfaceState.strings), for: []) view.titleLabel?.font = Font.regular(17.0) - view.setTitleColor(UIColor(0x007ee5), for: []) - view.setTitleColor(UIColor(0x007ee5).withAlphaComponent(0.7), for: [.highlighted]) + view.setTitleColor(interfaceState.theme.rootController.navigationBar.accentTextColor, for: []) + view.setTitleColor(interfaceState.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.7), for: [.highlighted]) view.addTarget(self, action: #selector(self.buttonPressed(_:)), for: [.touchUpInside]) self.view.addSubview(view) self.buttons.append((button, view)) diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index c854050dbc..bd20737127 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -159,7 +159,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if case let .contextRequest(addressName, query) = inputQuery, query.isEmpty { let string = NSMutableAttributedString() string.append(NSAttributedString(string: "@" + addressName, font: Font.regular(17.0), textColor: UIColor.clear)) - string.append(NSAttributedString(string: " " + inlinePlaceholder, font: Font.regular(17.0), textColor: UIColor(0xC8C8CE))) + string.append(NSAttributedString(string: " " + inlinePlaceholder, font: Font.regular(17.0), textColor: UIColor(rgb: 0xC8C8CE))) contextPlaceholder = string } } diff --git a/TelegramUI/ChatInterfaceInputNodes.swift b/TelegramUI/ChatInterfaceInputNodes.swift index dab4ce43ad..26e15e9e58 100644 --- a/TelegramUI/ChatInterfaceInputNodes.swift +++ b/TelegramUI/ChatInterfaceInputNodes.swift @@ -10,7 +10,7 @@ func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: } else if let inputMediaNode = inputMediaNode { return inputMediaNode } else { - let inputNode = ChatMediaInputNode(account: account, controllerInteraction: controllerInteraction) + let inputNode = ChatMediaInputNode(account: account, controllerInteraction: controllerInteraction, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) inputNode.interfaceInteraction = interfaceInteraction return inputNode } diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index bb1c938fc6..eee12a0546 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -53,8 +53,8 @@ public struct ChatTextInputState: Coding, Equatable { } public init(decoder: Decoder) { - self.inputText = decoder.decodeStringForKey("t") - self.selectionRange = Int(decoder.decodeInt32ForKey("s0")) ..< Int(decoder.decodeInt32ForKey("s1")) + self.inputText = decoder.decodeStringForKey("t", orElse: "") + self.selectionRange = Int(decoder.decodeInt32ForKey("s0", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("s1", orElse: 0)) } public func encode(_ encoder: Encoder) { @@ -74,7 +74,7 @@ struct ChatEditMessageState: Coding, Equatable { } init(decoder: Decoder) { - self.messageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("mp")), namespace: decoder.decodeInt32ForKey("mn"), id: decoder.decodeInt32ForKey("mi")) + self.messageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("mp", orElse: 0)), namespace: decoder.decodeInt32ForKey("mn", orElse: 0), id: decoder.decodeInt32ForKey("mi", orElse: 0)) if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState { self.inputState = inputState } else { @@ -108,8 +108,8 @@ final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceState { } init(decoder: Decoder) { - self.timestamp = decoder.decodeInt32ForKey("d") - self.text = decoder.decodeStringForKey("t") + self.timestamp = decoder.decodeInt32ForKey("d", orElse: 0) + self.text = decoder.decodeStringForKey("t", orElse: "") } func encode(_ encoder: Encoder) { @@ -148,19 +148,19 @@ struct ChatInterfaceMessageActionsState: Coding, Equatable { } init(decoder: Decoder) { - if let closedMessageIdPeerId = (decoder.decodeInt64ForKey("cb.p") as Int64?), let closedMessageIdNamespace = (decoder.decodeInt32ForKey("cb.n") as Int32?), let closedMessageIdId = (decoder.decodeInt32ForKey("cb.i") as Int32?) { + if let closedMessageIdPeerId = decoder.decodeOptionalInt64ForKey("cb.p"), let closedMessageIdNamespace = decoder.decodeOptionalInt32ForKey("cb.n"), let closedMessageIdId = decoder.decodeOptionalInt32ForKey("cb.i") { self.closedButtonKeyboardMessageId = MessageId(peerId: PeerId(closedMessageIdPeerId), namespace: closedMessageIdNamespace, id: closedMessageIdId) } else { self.closedButtonKeyboardMessageId = nil } - if let processedMessageIdPeerId = (decoder.decodeInt64ForKey("pb.p") as Int64?), let processedMessageIdNamespace = (decoder.decodeInt32ForKey("pb.n") as Int32?), let processedMessageIdId = (decoder.decodeInt32ForKey("pb.i") as Int32?) { + if let processedMessageIdPeerId = decoder.decodeOptionalInt64ForKey("pb.p"), let processedMessageIdNamespace = decoder.decodeOptionalInt32ForKey("pb.n"), let processedMessageIdId = decoder.decodeOptionalInt32ForKey("pb.i") { self.processedSetupReplyMessageId = MessageId(peerId: PeerId(processedMessageIdPeerId), namespace: processedMessageIdNamespace, id: processedMessageIdId) } else { self.processedSetupReplyMessageId = nil } - if let closedPinnedMessageIdPeerId = (decoder.decodeInt64ForKey("cp.p") as Int64?), let closedPinnedMessageIdNamespace = (decoder.decodeInt32ForKey("cp.n") as Int32?), let closedPinnedMessageIdId = (decoder.decodeInt32ForKey("cp.i") as Int32?) { + if let closedPinnedMessageIdPeerId = decoder.decodeOptionalInt64ForKey("cp.p"), let closedPinnedMessageIdNamespace = decoder.decodeOptionalInt32ForKey("cp.n"), let closedPinnedMessageIdId = decoder.decodeOptionalInt32ForKey("cp.i") { self.closedPinnedMessageId = MessageId(peerId: PeerId(closedPinnedMessageIdPeerId), namespace: closedPinnedMessageIdNamespace, id: closedPinnedMessageIdId) } else { self.closedPinnedMessageId = nil @@ -277,20 +277,20 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { } init(decoder: Decoder) { - self.timestamp = decoder.decodeInt32ForKey("ts") + self.timestamp = decoder.decodeInt32ForKey("ts", orElse: 0) if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState { self.composeInputState = inputState } else { self.composeInputState = ChatTextInputState() } - if let composeDisableUrlPreview = decoder.decodeStringForKey("dup") as String? { + if let composeDisableUrlPreview = decoder.decodeOptionalStringForKey("dup") { self.composeDisableUrlPreview = composeDisableUrlPreview } else { self.composeDisableUrlPreview = nil } - let replyMessageIdPeerId: Int64? = decoder.decodeInt64ForKey("r.p") - let replyMessageIdNamespace: Int32? = decoder.decodeInt32ForKey("r.n") - let replyMessageIdId: Int32? = decoder.decodeInt32ForKey("r.i") + let replyMessageIdPeerId: Int64? = decoder.decodeOptionalInt64ForKey("r.p") + let replyMessageIdNamespace: Int32? = decoder.decodeOptionalInt32ForKey("r.n") + let replyMessageIdId: Int32? = decoder.decodeOptionalInt32ForKey("r.i") if let replyMessageIdPeerId = replyMessageIdPeerId, let replyMessageIdNamespace = replyMessageIdNamespace, let replyMessageIdId = replyMessageIdId { self.replyMessageId = MessageId(peerId: PeerId(replyMessageIdPeerId), namespace: replyMessageIdNamespace, id: replyMessageIdId) } else { diff --git a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift index d5d442355c..6edd7fced1 100644 --- a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift +++ b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift @@ -10,27 +10,30 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS if let editMessage = chatPresentationInterfaceState.interfaceState.editMessage { if let editPanelNode = currentPanel as? EditAccessoryPanelNode, editPanelNode.messageId == editMessage.messageId { editPanelNode.interfaceInteraction = interfaceInteraction + editPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return editPanelNode } else { - let panelNode = EditAccessoryPanelNode(account: account, messageId: editMessage.messageId) + let panelNode = EditAccessoryPanelNode(account: account, messageId: editMessage.messageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panelNode.interfaceInteraction = interfaceInteraction return panelNode } } else if let forwardMessageIds = chatPresentationInterfaceState.interfaceState.forwardMessageIds { if let forwardPanelNode = currentPanel as? ForwardAccessoryPanelNode, forwardPanelNode.messageIds == forwardMessageIds { forwardPanelNode.interfaceInteraction = interfaceInteraction + forwardPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return forwardPanelNode } else { - let panelNode = ForwardAccessoryPanelNode(account: account, messageIds: forwardMessageIds) + let panelNode = ForwardAccessoryPanelNode(account: account, messageIds: forwardMessageIds, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panelNode.interfaceInteraction = interfaceInteraction return panelNode } } else if let replyMessageId = chatPresentationInterfaceState.interfaceState.replyMessageId { if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageId { replyPanelNode.interfaceInteraction = interfaceInteraction + replyPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return replyPanelNode } else { - let panelNode = ReplyAccessoryPanelNode(account: account, messageId: replyMessageId) + let panelNode = ReplyAccessoryPanelNode(account: account, messageId: replyMessageId, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panelNode.interfaceInteraction = interfaceInteraction return panelNode } @@ -38,9 +41,10 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode, previewPanelNode.webpage.id == urlPreview.1.id { previewPanelNode.interfaceInteraction = interfaceInteraction previewPanelNode.replaceWebpage(urlPreview.1) + previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return previewPanelNode } else { - let panelNode = WebpagePreviewAccessoryPanelNode(account: account, webpage: urlPreview.1) + let panelNode = WebpagePreviewAccessoryPanelNode(account: account, webpage: urlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panelNode.interfaceInteraction = interfaceInteraction return panelNode } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index c23eccf212..c841bcf2f0 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -17,20 +17,10 @@ func contextMenuForChatPresentationIntefaceState(_ chatPresentationInterfaceStat if let channel = peer as? TelegramChannel { switch channel.info { case .broadcast: - switch channel.role { - case .creator, .editor, .moderator: - canReply = true - case .member: - canReply = false - } + canReply = channel.hasAdminRights([.canPostMessages]) case .group: canReply = true - switch channel.role { - case .creator, .editor, .moderator: - canPin = true - case .member: - canPin = false - } + canPin = channel.hasAdminRights([.canPinMessages]) } } else { canReply = true @@ -133,13 +123,8 @@ func chatDeleteMessagesOptions(account: Account, messageIds: Set) -> if !message.flags.contains(.Incoming) { options.insert(.globally) } else { - switch channel.role { - case .creator: - options.insert(.globally) - case .moderator, .editor: - options.insert(.globally) - case .member: - break + if channel.hasAdminRights([.canDeleteMessages]) { + options.insert(.globally) } } optionsMap[message.id] = options diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift index 48e6cc66cf..627186d809 100644 --- a/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -3,13 +3,26 @@ import AsyncDisplayKit import TelegramCore func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputPanelNode? { + if let _ = chatPresentationInterfaceState.search { + if let currentPanel = currentPanel as? ChatSearchInputPanelNode { + currentPanel.interfaceInteraction = interfaceInteraction + return currentPanel + } else { + let panel = ChatSearchInputPanelNode(theme: chatPresentationInterfaceState.theme) + panel.account = account + panel.interfaceInteraction = interfaceInteraction + return panel + } + } + if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState { if let currentPanel = currentPanel as? ChatMessageSelectionInputPanelNode { currentPanel.selectedMessageCount = selectionState.selectedIds.count currentPanel.interfaceInteraction = interfaceInteraction + currentPanel.updateTheme(theme: chatPresentationInterfaceState.theme) return currentPanel } else { - let panel = ChatMessageSelectionInputPanelNode() + let panel = ChatMessageSelectionInputPanelNode(theme: chatPresentationInterfaceState.theme) panel.account = account panel.selectedMessageCount = selectionState.selectedIds.count panel.interfaceInteraction = interfaceInteraction @@ -19,9 +32,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if chatPresentationInterfaceState.peerIsBlocked { if let currentPanel = currentPanel as? ChatUnblockInputPanelNode { currentPanel.interfaceInteraction = interfaceInteraction + currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return currentPanel } else { - let panel = ChatUnblockInputPanelNode() + let panel = ChatUnblockInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.account = account panel.interfaceInteraction = interfaceInteraction return panel @@ -68,17 +82,14 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState } switch channel.info { case .broadcast: - switch channel.role { - case .creator, .editor, .moderator: - break - case .member: - if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { - return currentPanel - } else { - let panel = ChatChannelSubscriberInputPanelNode() - panel.account = account - return panel - } + if !channel.hasAdminRights([.canPostMessages]) { + if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { + return currentPanel + } else { + let panel = ChatChannelSubscriberInputPanelNode() + panel.account = account + return panel + } } case .group: switch channel.participationStatus { @@ -121,9 +132,10 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if displayBotStartPanel { if let currentPanel = currentPanel as? ChatBotStartInputPanelNode { + currentPanel.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) return currentPanel } else { - let panel = ChatBotStartInputPanelNode() + let panel = ChatBotStartInputPanelNode(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings) panel.account = account panel.interfaceInteraction = interfaceInteraction return panel diff --git a/TelegramUI/ChatItemGalleryFooterContentNode.swift b/TelegramUI/ChatItemGalleryFooterContentNode.swift index 555d58eb78..80dae8bdd0 100644 --- a/TelegramUI/ChatItemGalleryFooterContentNode.swift +++ b/TelegramUI/ChatItemGalleryFooterContentNode.swift @@ -15,6 +15,8 @@ private let dateFont = Font.regular(14.0) final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { private let account: Account + private var theme: PresentationTheme + private var strings: PresentationStrings private let deleteButton: UIButton private let actionButton: UIButton @@ -30,8 +32,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { private let messageContextDisposable = MetaDisposable() - init(account: Account) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { self.account = account + self.theme = theme + self.strings = strings self.deleteButton = UIButton() self.actionButton = UIButton() @@ -72,12 +76,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { canDelete = true } else if let channel = peer as? TelegramChannel { if message.flags.contains(.Incoming) { - switch channel.role { - case .creator, .moderator, .editor: - canDelete = true - case .member: - canDelete = false - } + canDelete = channel.hasAdminRights(.canDeleteMessages) } else { canDelete = true } @@ -96,7 +95,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode { authorNameText = peer.displayTitle } - let dateText = humanReadableStringForTimestamp(timestamp: message.timestamp) + let dateText = humanReadableStringForTimestamp(strings: self.strings, timestamp: message.timestamp) if self.currentMessageText != message.text || canDelete != !self.deleteButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText { self.currentMessageText = message.text diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 99d3d185f3..0931d72411 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -4,12 +4,6 @@ import SwiftSignalKit import Display import TelegramCore -private let composeButtonImage = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0x007ee5).cgColor) - try? drawSvgPath(context, path: "M0,4 L15,4 L14,5 L1,5 L1,22 L18,22 L18,9 L19,8 L19,23 L0,23 L0,4 Z M18.5944456,1.70209754 L19.5995507,2.70718758 L10.0510517,12.255543 L9.54849908,13.7631781 L11.0561568,13.2606331 L20.6046559,3.71227763 L21.6097611,4.71736767 L11.5587094,14.7682681 L7.53828874,15.7733582 L9.04594649,11.250453 L18.5944456,1.70209754 Z M19.0969982,1.19955251 L20.0773504,0.21921503 C20.3690844,-0.0725145755 20.8398084,-0.0729335627 21.1298838,0.217137419 L23.0947435,2.18196761 C23.3833646,2.47058439 23.3838887,2.94326675 23.0926659,3.23448517 L22.1123136,4.21482265 L19.0969982,1.19955251 Z ") -}) - public class ChatListController: TelegramController, UIViewControllerPreviewingDelegate { private let account: Account @@ -29,23 +23,30 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD private let passcodeDisposable = MetaDisposable() + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + public override init(account: Account) { self.account = account - self.titleView = NetworkStatusTitleView() + self.presentationData = (account.telegramApplicationContext.currentPresentationData.with { $0 }) + + self.titleView = NetworkStatusTitleView(theme: self.presentationData.theme) super.init(account: account) - self.navigationBar.item = nil + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style - self.titleView.title = NetworkStatusTitle(text: "Chats", activity: false) + self.navigationBar?.item = nil + + self.titleView.title = NetworkStatusTitle(text: self.presentationData.strings.DialogList_Title, activity: false) self.navigationItem.titleView = self.titleView - self.tabBarItem.title = "Chats" + self.tabBarItem.title = self.presentationData.strings.DialogList_Title self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconChats") self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconChatsSelected") - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Edit", style: .plain, target: self, action: #selector(self.editPressed)) - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: composeButtonImage, style: .plain, target: self, action: #selector(self.composePressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) self.scrollToTop = { [weak self] in if let strongSelf = self { @@ -57,13 +58,13 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD if let strongSelf = self { switch state { case .waitingForNetwork: - strongSelf.titleView.title = NetworkStatusTitle(text: "Waiting For Network...", activity: true) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true) case .connecting: - strongSelf.titleView.title = NetworkStatusTitle(text: "Connecting...", activity: true) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Connecting, activity: true) case .updating: - strongSelf.titleView.title = NetworkStatusTitle(text: "Updating...", activity: true) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true) case .online: - strongSelf.titleView.title = NetworkStatusTitle(text: "Chats", activity: false) + strongSelf.titleView.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.DialogList_Title, activity: false) } } }) @@ -108,6 +109,20 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD }).start() } } + + 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() + } + } + }) } required public init(coder aDecoder: NSCoder) { @@ -119,10 +134,25 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD self.titleDisposable?.dispose() self.badgeDisposable?.dispose() self.passcodeDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.tabBarItem.title = self.presentationData.strings.DialogList_Title + self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) + + self.titleView.theme = self.presentationData.theme + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + if self.isNodeLoaded { + self.chatListDisplayNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + } } override public func loadDisplayNode() { - self.displayNode = ChatListControllerNode(account: self.account) + self.displayNode = ChatListControllerNode(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings) self.chatListDisplayNode.navigationBar = self.navigationBar @@ -138,7 +168,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD if let strongSelf = self { let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Delete", color: .destructive, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { @@ -146,7 +176,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in + ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) @@ -219,14 +249,14 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } @objc func editPressed() { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) self.chatListDisplayNode.chatListNode.updateState { state in return state.withUpdatedEditing(true) } } @objc func donePressed() { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Edit", style: .plain, target: self, action: #selector(self.editPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) self.chatListDisplayNode.chatListNode.updateState { state in return state.withUpdatedEditing(false).withUpdatedPeerIdWithRevealedOptions(nil) } @@ -265,7 +295,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD let chatController = ChatController(account: self.account, peerId: peerId) chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) + chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) return chatController } else if let messageId = action as? MessageId { if #available(iOSApplicationExtension 9.0, *) { @@ -276,7 +306,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD let chatController = ChatController(account: self.account, peerId: messageId.peerId, messageId: messageId) chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) + chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) return chatController } } @@ -299,7 +329,7 @@ public class ChatListController: TelegramController, UIViewControllerPreviewingD } let chatController = ChatController(account: self.account, peerId: item.peer.peerId) chatController.canReadHistory.set(false) - chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) + chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) return chatController } else { return nil diff --git a/TelegramUI/ChatListControllerNode.swift b/TelegramUI/ChatListControllerNode.swift index fda61c6550..f301be381c 100644 --- a/TelegramUI/ChatListControllerNode.swift +++ b/TelegramUI/ChatListControllerNode.swift @@ -18,9 +18,13 @@ class ChatListControllerNode: ASDisplayNode { var requestOpenPeerFromSearch: ((Peer) -> Void)? var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)? - init(account: Account) { + var themeAndStrings: (PresentationTheme, PresentationStrings) + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { self.account = account - self.chatListNode = ChatListNode(account: account, mode: .chatList) + self.chatListNode = ChatListNode(account: account, mode: .chatList, theme: theme, strings: strings) + + self.themeAndStrings = (theme, strings) super.init(viewBlock: { return UITracingLayerView() @@ -29,6 +33,12 @@ class ChatListControllerNode: ASDisplayNode { self.addSubnode(self.chatListNode) } + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.themeAndStrings = (theme, strings) + self.chatListNode.updateThemeAndStrings(theme: theme, strings: strings) + self.searchDisplayController?.updateThemeAndStrings(theme: theme, strings: strings) + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) @@ -86,7 +96,7 @@ class ChatListControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(contentNode: ChatListSearchContainerNode(account: self.account, openPeer: { [weak self] peer in + self.searchDisplayController = SearchDisplayController(theme: self.themeAndStrings.0, strings: self.themeAndStrings.1, contentNode: ChatListSearchContainerNode(account: self.account, openPeer: { [weak self] peer in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peer) } diff --git a/TelegramUI/ChatListEmptyItem.swift b/TelegramUI/ChatListEmptyItem.swift deleted file mode 100644 index 14c7f1f60c..0000000000 --- a/TelegramUI/ChatListEmptyItem.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Foundation -import UIKit -import AsyncDisplayKit -import Postbox -import Display -import SwiftSignalKit - -class ChatListEmptyItem: ListViewItem { - let selectable: Bool = false - - init() { - } - - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { - async { - let node = ChatListEmptyItemNode() - node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) - node.updateItemPosition(first: previousItem == nil, last: nextItem == nil) - completion(node, { - return (nil, {}) - }) - } - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { - assert(node is ChatListEmptyItemNode) - if let node = node as? ChatListEmptyItemNode { - Queue.mainQueue().async { - node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) - node.updateItemPosition(first: previousItem == nil, last: nextItem == nil) - - completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), {}) - } - } - } -} - -private let separatorHeight = 1.0 / UIScreen.main.scale - -class ChatListEmptyItemNode: ListViewItemNode { - let separatorNode: ASDisplayNode - - required init() { - self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) - self.separatorNode.isLayerBacked = true - - super.init(layerBacked: false, dynamicBounce: false) - - self.addSubnode(self.separatorNode) - } - - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 68.0 - separatorHeight), size: CGSize(width: width, height: separatorHeight)) - - self.contentSize = CGSize(width: width, height: 68.0) - } - - func updateItemPosition(first: Bool, last: Bool) { - self.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) - } -} diff --git a/TelegramUI/ChatListHoleItem.swift b/TelegramUI/ChatListHoleItem.swift index 9dd1bc4e87..e1bd218aa3 100644 --- a/TelegramUI/ChatListHoleItem.swift +++ b/TelegramUI/ChatListHoleItem.swift @@ -57,7 +57,7 @@ class ChatListHoleItemNode: ListViewItemNode { required init() { self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) + self.separatorNode.backgroundColor = UIColor(rgb: 0xc8c7cc) self.separatorNode.isLayerBacked = true self.labelNode = TextNode() @@ -78,11 +78,18 @@ class ChatListHoleItemNode: ListViewItemNode { let labelNodeLayout = TextNode.asyncLayout(self.labelNode) return { width, first, last in - let (labelLayout, labelApply) = labelNodeLayout(NSAttributedString(string: "Loading", font: titleFont, textColor: UIColor(0xc8c7cc)), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = labelNodeLayout(NSAttributedString(string: "", font: titleFont, textColor: UIColor(rgb: 0xc8c7cc)), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: false) let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 68.0), insets: insets) + let separatorInset: CGFloat + if last { + separatorInset = 0.0 + } else { + separatorInset = 80.0 + } + return (layout, { [weak self] in if let strongSelf = self { strongSelf.relativePosition = (first, last) @@ -91,7 +98,7 @@ class ChatListHoleItemNode: ListViewItemNode { strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: floor((width - labelLayout.size.width) / 2.0), y: floor((layout.contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 80.0, y: 68.0 - separatorHeight), size: CGSize(width: width - 78.0, height: separatorHeight)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: separatorInset, y: 68.0 - separatorHeight), size: CGSize(width: width - separatorInset, height: separatorHeight)) strongSelf.contentSize = layout.contentSize strongSelf.insets = layout.insets diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 8d320348d4..ec2eb5b916 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -7,6 +7,8 @@ import SwiftSignalKit import TelegramCore class ChatListItem: ListViewItem { + let theme: PresentationTheme + let strings: PresentationStrings let account: Account let index: ChatListIndex let message: Message? @@ -22,7 +24,9 @@ class ChatListItem: ListViewItem { let header: ListViewItemHeader? - init(account: Account, index: ChatListIndex, message: Message?, peer: RenderedPeer, combinedReadState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedState: PeerChatListEmbeddedInterfaceState?, editing: Bool, hasActiveRevealControls: Bool, header: ListViewItemHeader?, interaction: ChatListNodeInteraction) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, index: ChatListIndex, message: Message?, peer: RenderedPeer, combinedReadState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedState: PeerChatListEmbeddedInterfaceState?, editing: Bool, hasActiveRevealControls: Bool, header: ListViewItemHeader?, interaction: ChatListNodeInteraction) { + self.theme = theme + self.strings = strings self.account = account self.index = index self.message = message @@ -121,8 +125,6 @@ private let textFont = Font.regular(15.0) private let dateFont = Font.regular(14.0) private let badgeFont = Font.regular(14.0) -private let titleSecretColor = UIColor(0x00a629) - private let pinIcon = UIImage(bundleImageName: "Chat List/RevealActionPinIcon")?.precomposed() private let unpinIcon = UIImage(bundleImageName: "Chat List/RevealActionUnpinIcon")?.precomposed() private let muteIcon = UIImage(bundleImageName: "Chat List/RevealActionMuteIcon")?.precomposed() @@ -137,76 +139,28 @@ private enum RevealOptionKey: Int32 { case delete } -private let pinOption = ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: "Pin", icon: pinIcon, color: UIColor(0xbcbcc3)) -private let unpinOption = ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: "Unpin", icon: unpinIcon, color: UIColor(0xbcbcc3)) -private let muteOption = ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: "Mute", icon: muteIcon, color: UIColor(0xaaaab3)) -private let unmuteOption = ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: "Unmute", icon: unmuteIcon, color: UIColor(0xaaaab3)) -private let deleteOption = ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: "Delete", icon: deleteIcon, color: UIColor(0xff3824)) +private let itemHeight: CGFloat = 76.0 -private let itemHeight: CGFloat = 76.0 //68.0 - -private func revealOptions(isPinned: Bool, isMuted: Bool) -> [ItemListRevealOption] { +private func revealOptions(strings: PresentationStrings, isPinned: Bool, isMuted: Bool) -> [ItemListRevealOption] { var options: [ItemListRevealOption] = [] if isPinned { - options.append(unpinOption) + options.append(ItemListRevealOption(key: RevealOptionKey.unpin.rawValue, title: strings.DialogList_Unpin, icon: unpinIcon, color: UIColor(rgb: 0xbcbcc3))) } else { - options.append(pinOption) + options.append(ItemListRevealOption(key: RevealOptionKey.pin.rawValue, title: strings.DialogList_Pin, icon: pinIcon, color: UIColor(rgb: 0xbcbcc3))) } if isMuted { - options.append(unmuteOption) + options.append(ItemListRevealOption(key: RevealOptionKey.unmute.rawValue, title: strings.Conversation_Unmute, icon: unmuteIcon, color: UIColor(rgb: 0xaaaab3))) } else { - options.append(muteOption) + options.append(ItemListRevealOption(key: RevealOptionKey.mute.rawValue, title: strings.Conversation_Mute, icon: muteIcon, color: UIColor(rgb: 0xaaaab3))) } - options.append(deleteOption) + options.append(ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: strings.Common_Delete, icon: deleteIcon, color: UIColor(rgb: 0xff3824))) return options } -private func generateStatusCheckImage(single: Bool) -> UIImage? { - return generateImage(CGSize(width: single ? 13.0 : 18.0, height: 13.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0 + 1.0, y: -size.height / 2.0 + 1.0) - - //CGContextSetFillColorWithColor(context, UIColor.lightGrayColor().CGColor) - //CGContextFillRect(context, CGRect(origin: CGPoint(), size: size)) - - context.scaleBy(x: 0.5, y: 0.5) - context.setStrokeColor(UIColor(0x19C700).cgColor) - context.setLineWidth(2.8) - if single { - let _ = try? drawSvgPath(context, path: "M0,12 L6.75230742,19.080349 L22.4821014,0.277229071 ") - } else { - let _ = try? drawSvgPath(context, path: "M0,12 L6.75230742,19.080349 L22.4821014,0.277229071 ") - let _ = try? drawSvgPath(context, path: "M13.4492402,16.500967 L15.7523074,18.8031199 L31.4821014,0 ") - } - context.strokePath() - }) -} - -private func generateBadgeBackgroundImage(active: Bool) -> UIImage? { - return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - if active { - context.setFillColor(UIColor(0x007ee5).cgColor) - } else { - context.setFillColor(UIColor(0xadb3bb).cgColor) - } - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10) -} - -private let statusSingleCheckImage = generateStatusCheckImage(single: true) -private let statusDoubleCheckImage = generateStatusCheckImage(single: false) -private let activeBadgeBackgroundImage = generateBadgeBackgroundImage(active: true) -private let inactiveBadgeBackgroundImage = generateBadgeBackgroundImage(active: false) private let peerMutedIcon = UIImage(bundleImageName: "Chat List/PeerMutedIcon")?.precomposed() private let separatorHeight = 1.0 / UIScreen.main.scale -private let pinnedBackgroundColor = UIColor(0xf7f7f7) - private let avatarFont: UIFont = UIFont(name: "ArialRoundedMTBold", size: 24.0)! class ChatListItemNode: ItemListRevealOptionsItemNode { @@ -242,13 +196,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.displaysAsynchronously = false - self.backgroundNode.backgroundColor = .white self.avatarNode = AvatarNode(font: avatarFont) self.avatarNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true self.titleNode = TextNode() @@ -287,10 +239,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { self.mutedIconNode.displayWithoutProcessing = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) self.separatorNode.isLayerBacked = true - super.init(layerBacked: false, dynamicBounce: false, rotated: false) + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.addSubnode(self.backgroundNode) self.addSubnode(self.separatorNode) @@ -355,36 +306,17 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let strongSelf = self { if completed { strongSelf.highlightedBackgroundNode.removeFromSupernode() - /*if let item = strongSelf.layoutParams?.0, item.index.pinningIndex != nil { - strongSelf.contentNode.backgroundColor = pinnedBackgroundColor - } else { - strongSelf.contentNode.backgroundColor = UIColor.white - } - - if !strongSelf.contentNode.isOpaque { - strongSelf.contentNode.isOpaque = true - strongSelf.contentNode.recursivelyEnsureDisplaySynchronously(true) - }*/ } } }) self.highlightedBackgroundNode.alpha = 0.0 } else { self.highlightedBackgroundNode.removeFromSupernode() - - /*if let item = self.layoutParams?.0, item.index.pinningIndex != nil { - self.contentNode.backgroundColor = pinnedBackgroundColor - } else { - self.contentNode.backgroundColor = UIColor.white - } - self.contentNode.isOpaque = true - self.contentNode.displaysAsynchronously = true*/ } } } } - func asyncLayout() -> (_ item: ChatListItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) { let dateLayout = TextNode.asyncLayout(self.dateNode) let textLayout = TextNode.asyncLayout(self.textNode) @@ -393,6 +325,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let badgeTextLayout = TextNode.asyncLayout(self.badgeTextNode) let editableControlLayout = ItemListEditableControlNode.asyncLayout(self.editableControlNode) + let currentItem = self.layoutParams?.0 + return { item, width, first, last, firstWithHeader, nextIsPinned in let account = item.account let message = item.message @@ -400,6 +334,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let notificationSettings = item.notificationSettings let embeddedState = item.embeddedState + let theme = item.theme.chatList + + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + var authorAttributedString: NSAttributedString? var textAttributedString: NSAttributedString? var dateAttributedString: NSAttributedString? @@ -437,17 +379,78 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { for media in message.media { switch media { case _ as TelegramMediaImage: - messageText = "Photo" + if message.text.isEmpty { + messageText = item.strings.Message_Photo + } case let fileMedia as TelegramMediaFile: - if fileMedia.isSticker { - messageText = "Sticker" - } else { - messageText = "File" + if message.text.isEmpty { + if let fileName = fileMedia.fileName { + messageText = fileName + } else { + messageText = item.strings.Message_File + } + inner: for attribute in fileMedia.attributes { + switch attribute { + case .Animated: + messageText = item.strings.Message_Animation + break inner + case let .Audio(isVoice, _, title, performer, _): + if isVoice { + messageText = item.strings.Message_Audio + break inner + } else { + let descriptionString: String + if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { + descriptionString = title + " — " + performer + } else if let title = title, !title.isEmpty { + descriptionString = title + } else if let performer = performer, !performer.isEmpty { + descriptionString = performer + } else if let fileName = fileMedia.fileName { + descriptionString = fileName + } else { + descriptionString = item.strings.Message_Audio + } + messageText = descriptionString + break inner + } + case let .Sticker(displayText, _): + if displayText.isEmpty { + messageText = item.strings.Message_Sticker + break inner + } else { + messageText = displayText + " " + item.strings.Message_Sticker + break inner + } + case let .Video(_, _, flags): + if flags.contains(.instantRoundVideo) { + messageText = item.strings.Message_VideoMessage + } else { + messageText = item.strings.Message_Video + } + break inner + default: + break + } + } } case _ as TelegramMediaMap: - messageText = "Map" + messageText = item.strings.Message_Location case _ as TelegramMediaContact: - messageText = "Contact" + messageText = item.strings.Message_Contact + case let action as TelegramMediaAction: + switch action.action { + case .phoneCall: + if message.effectivelyIncoming { + messageText = item.strings.Notification_CallIncoming + } else { + messageText = item.strings.Notification_CallOutgoing + } + default: + if let text = serviceMessageString(theme: item.theme, strings: item.strings, message: message, accountPeerId: item.account.peerId) { + messageText = text.string + } + } default: break } @@ -460,29 +463,24 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let attributedText: NSAttributedString if let embeddedState = embeddedState as? ChatEmbeddedInterfaceState { - authorAttributedString = NSAttributedString(string: "Draft", font: textFont, textColor: UIColor(0xdd4b39)) + authorAttributedString = NSAttributedString(string: item.strings.DialogList_Draft, font: textFont, textColor: theme.messageDraftTextColor) - attributedText = NSAttributedString(string: embeddedState.text, font: textFont, textColor: UIColor(0x8e8e93)) + attributedText = NSAttributedString(string: embeddedState.text, font: textFont, textColor: theme.messageTextColor) } else if let message = message, let author = message.author as? TelegramUser, let peer = peer, !(peer is TelegramUser) { if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: UIColor(0x8e8e93)) + attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor) } else { - let peerText: String = author.id == account.peerId ? "You" : author.displayTitle + let peerText: String = author.id == account.peerId ? item.strings.DialogList_You : author.displayTitle - authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: .black) - attributedText = NSAttributedString(string: messageText, font: textFont, textColor: UIColor(0x8e8e93)) - - /*let mutableAttributedText = NSMutableAttributedString(string: peerText.appending(messageText as String), attributes: [kCTFontAttributeName as String: textFont]) - mutableAttributedText.addAttribute(kCTForegroundColorAttributeName as String, value: UIColor.black.cgColor, range: NSMakeRange(0, peerText.length)) - mutableAttributedText.addAttribute(kCTForegroundColorAttributeName as String, value: UIColor(0x8e8e93).cgColor, range: NSMakeRange(peerText.length, messageText.length)) - attributedText = mutableAttributedText*/ + authorAttributedString = NSAttributedString(string: peerText, font: textFont, textColor: theme.authorNameColor) + attributedText = NSAttributedString(string: messageText, font: textFont, textColor: theme.messageTextColor) } } else { - attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: UIColor(0x8e8e93)) + attributedText = NSAttributedString(string: messageText as String, font: textFont, textColor: theme.messageTextColor) } if let displayTitle = peer?.displayTitle { - titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat ? titleSecretColor : UIColor.black) + titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat ? theme.secretTitleColor : theme.titleColor) } textAttributedString = attributedText @@ -492,16 +490,16 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { localtime_r(&t, &timeinfo) let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - let dateText = stringForRelativeTimestamp(item.index.messageIndex.timestamp, relativeTo: timestamp) + let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.index.messageIndex.timestamp, relativeTo: timestamp) - dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: UIColor(0x8e8e93)) + dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: theme.dateTextColor) if let message = message, message.author?.id == account.peerId { if !message.flags.isSending { if let combinedReadState = combinedReadState, combinedReadState.isOutgoingMessageIndexRead(MessageIndex(message)) { - statusImage = statusDoubleCheckImage + statusImage = PresentationResourcesChatList.doubleCheckImage(item.theme) } else { - statusImage = statusSingleCheckImage + statusImage = PresentationResourcesChatList.singleCheckImage(item.theme) } } } @@ -509,16 +507,20 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let combinedReadState = combinedReadState { let unreadCount = combinedReadState.count if unreadCount != 0 { + let badgeTextColor: UIColor if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings { if case .unmuted = notificationSettings.muteState { - currentBadgeBackgroundImage = activeBadgeBackgroundImage + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme) + badgeTextColor = theme.unreadBadgeActiveTextColor } else { - currentBadgeBackgroundImage = inactiveBadgeBackgroundImage + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.theme) + badgeTextColor = theme.unreadBadgeInactiveTextColor } } else { - currentBadgeBackgroundImage = activeBadgeBackgroundImage + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.theme) + badgeTextColor = theme.unreadBadgeActiveTextColor } - badgeAttributedString = NSAttributedString(string: "\(unreadCount)", font: badgeFont, textColor: UIColor.white) + badgeAttributedString = NSAttributedString(string: "\(unreadCount)", font: badgeFont, textColor: badgeTextColor) } } @@ -559,12 +561,17 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader) let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: itemHeight), insets: insets) - let peerRevealOptions = revealOptions(isPinned: item.index.pinningIndex != nil, isMuted: currentMutedIconImage != nil) + let peerRevealOptions = revealOptions(strings: item.strings, isPinned: item.index.pinningIndex != nil, isMuted: currentMutedIconImage != nil) return (layout, { [weak self] animated in if let strongSelf = self { strongSelf.layoutParams = (item, first, last, firstWithHeader, nextIsPinned) + if let _ = updatedTheme { + strongSelf.separatorNode.backgroundColor = item.theme.chatList.itemSeparatorColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.chatList.itemHighlightedBackgroundColor + } + let revealOffset = strongSelf.revealOffset let transition: ContainedViewLayoutTransition @@ -677,35 +684,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) if item.index.pinningIndex != nil { - strongSelf.backgroundNode.backgroundColor = pinnedBackgroundColor - /*if strongSelf.contentNode.backgroundColor == nil || !strongSelf.contentNode.backgroundColor!.isEqual(pinnedBackgroundColor) { - strongSelf.contentNode.backgroundColor = pinnedBackgroundColor - updateContentNode = true - }*/ + strongSelf.backgroundNode.backgroundColor = theme.pinnedItemBackgroundColor } else { - strongSelf.backgroundNode.backgroundColor = UIColor.white - /*if strongSelf.contentNode.backgroundColor == nil || !strongSelf.contentNode.backgroundColor!.isEqual(UIColor.white) { - strongSelf.contentNode.backgroundColor = UIColor.white - updateContentNode = true - }*/ + strongSelf.backgroundNode.backgroundColor = theme.itemBackgroundColor } let topNegativeInset: CGFloat = 0.0 strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset)) - /*if crossfadeContent && animated { - if let contents = strongSelf.titleNode.contents { - let tempNode = ASDisplayNode() - tempNode.isLayerBacked = true - tempNode.contents = contents - tempNode.frame = previousContentNodeFrame - strongSelf.insertSubnode(tempNode, aboveSubnode: strongSelf.contentNode) - transition.updateFrame(node: tempNode, frame: strongSelf.contentNode.frame) - transition.updateAlpha(node: tempNode, alpha: 0.0, completion: { [weak tempNode] _ in - tempNode?.removeFromSupernode() - }) - } - }*/ - strongSelf.setRevealOptions(peerRevealOptions) strongSelf.setRevealOptionsOpened(item.hasActiveRevealControls, animated: animated) } diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index 272660dce8..fa4b9694d8 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -41,18 +41,30 @@ final class ChatListNodeInteraction { } struct ChatListNodeState: Equatable { + let theme: PresentationTheme + let strings: PresentationStrings let editing: Bool let peerIdWithRevealedOptions: PeerId? + func withUpdatedPresentationData(theme: PresentationTheme, strings: PresentationStrings) -> ChatListNodeState { + return ChatListNodeState(theme: theme, strings: strings, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions) + } + func withUpdatedEditing(_ editing: Bool) -> ChatListNodeState { - return ChatListNodeState(editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions) + return ChatListNodeState(theme: self.theme, strings: self.strings, editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions) } func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChatListNodeState { - return ChatListNodeState(editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions) + return ChatListNodeState(theme: self.theme, strings: self.strings, editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions) } static func ==(lhs: ChatListNodeState, rhs: ChatListNodeState) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } if lhs.editing != rhs.editing { return false } @@ -66,14 +78,14 @@ struct ChatListNodeState: Equatable { private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { - case .SearchEntry: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: { + case let .SearchEntry(theme, text): + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: text, activate: { nodeInteraction.activateSearch() }), directionHint: entry.directionHint) - case let .PeerEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, editing, hasActiveRevealControls): + case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, editing, hasActiveRevealControls): switch mode { case .chatList: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(theme: theme, strings: strings, account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) case .peers: var peer: Peer? var chatPeer: Peer? @@ -81,16 +93,14 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode peer = messageMainPeer(message) chatPeer = message.peers[message.id.peerId] } - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, index: nil, header: nil, action: { _ in + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } }), directionHint: entry.directionHint) } - case .HoleEntry: + case let .HoleEntry(theme): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint) - case .Nothing: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyItem(), directionHint: entry.directionHint) } } } @@ -98,14 +108,14 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { - case .SearchEntry: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: { + case let .SearchEntry(theme, text): + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: text, activate: { nodeInteraction.activateSearch() }), directionHint: entry.directionHint) - case let .PeerEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer, editing, hasActiveRevealControls): + case let .PeerEntry(index, theme, strings, message, combinedReadState, notificationSettings, embeddedState, peer, editing, hasActiveRevealControls): switch mode { case .chatList: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(theme: theme, strings: strings, account: account, index: index, message: message, peer: peer, combinedReadState: combinedReadState, notificationSettings: notificationSettings, embeddedState: embeddedState, editing: editing, hasActiveRevealControls: hasActiveRevealControls, header: nil, interaction: nodeInteraction), directionHint: entry.directionHint) case .peers: var peer: Peer? var chatPeer: Peer? @@ -113,16 +123,14 @@ private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNode peer = messageMainPeer(message) chatPeer = message.peers[message.id.peerId] } - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, index: nil, header: nil, action: { _ in + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: chatPeer, status: .none, selection: .none, index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) } }), directionHint: entry.directionHint) } - case .HoleEntry: + case let .HoleEntry(theme): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint) - case .Nothing: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyItem(), directionHint: entry.directionHint) } } } @@ -156,16 +164,21 @@ final class ChatListNode: ListView { private var dequeuedInitialTransitionOnLayout = false private var enqueuedTransition: (ChatListNodeListViewTransition, () -> Void)? - private var currentState = ChatListNodeState(editing: false, peerIdWithRevealedOptions: nil) - private let statePromise = ValuePromise(ChatListNodeState(editing: false, peerIdWithRevealedOptions: nil), ignoreRepeated: true) + private var currentState: ChatListNodeState + private let statePromise: ValuePromise private var currentLocation: ChatListNodeLocation? private let chatListLocation = ValuePromise() private let chatListDisposable = MetaDisposable() - init(account: Account, mode: ChatListNodeMode) { + init(account: Account, mode: ChatListNodeMode, theme: PresentationTheme, strings: PresentationStrings) { + self.currentState = ChatListNodeState(theme: theme, strings: strings, editing: false, peerIdWithRevealedOptions: nil) + self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) + super.init() + self.backgroundColor = theme.chatList.backgroundColor + let nodeInteraction = ChatListNodeInteraction(activateSearch: { [weak self] in if let strongSelf = self, let activateSearch = strongSelf.activateSearch { activateSearch() @@ -282,6 +295,16 @@ final class ChatListNode: ListView { self.chatListDisposable.dispose() } + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if theme !== self.currentState.theme || strings !== self.currentState.strings { + self.backgroundColor = theme.chatList.backgroundColor + + self.updateState { + return $0.withUpdatedPresentationData(theme: theme, strings: strings) + } + } + } + func updateState(_ f: (ChatListNodeState) -> ChatListNodeState) { let state = f(self.currentState) if state != self.currentState { @@ -325,16 +348,6 @@ final class ChatListNode: ListView { if let strongSelf = self { strongSelf.chatListView = transition.chatListView - /*if let range = visibleRange.loadedRange { - strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.firstIndex].index) - - if let visible = visibleRange.visibleRange { - if let messageId = maxIncomingMessageIdForEntries(transition.historyView.filteredEntries, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visible.firstIndex)) { - strongSelf.updateMaxVisibleReadIncomingMessageId(messageId) - } - } - }*/ - if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) diff --git a/TelegramUI/ChatListNodeEntries.swift b/TelegramUI/ChatListNodeEntries.swift index 41759d0975..fa620822a7 100644 --- a/TelegramUI/ChatListNodeEntries.swift +++ b/TelegramUI/ChatListNodeEntries.swift @@ -20,12 +20,12 @@ enum ChatListNodeEntryId: Hashable, CustomStringConvertible { var description: String { switch self { - case .Search: - return "search" - case let .Hole(value): - return "hole(\(value))" - case let .PeerId(value): - return "peerId(\(value))" + case .Search: + return "search" + case let .Hole(value): + return "hole(\(value))" + case let .PeerId(value): + return "peerId(\(value))" } } @@ -61,21 +61,18 @@ enum ChatListNodeEntryId: Hashable, CustomStringConvertible { } enum ChatListNodeEntry: Comparable, Identifiable { - case SearchEntry - case PeerEntry(index: ChatListIndex, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, editing: Bool, hasActiveRevealControls: Bool) - case HoleEntry(ChatListHole) - case Nothing(ChatListIndex) + case SearchEntry(theme: PresentationTheme, text: String) + case PeerEntry(index: ChatListIndex, theme: PresentationTheme, strings: PresentationStrings, message: Message?, readState: CombinedPeerReadState?, notificationSettings: PeerNotificationSettings?, embeddedInterfaceState: PeerChatListEmbeddedInterfaceState?, peer: RenderedPeer, editing: Bool, hasActiveRevealControls: Bool) + case HoleEntry(ChatListHole, theme: PresentationTheme) var index: ChatListIndex { switch self { case .SearchEntry: return ChatListIndex.absoluteUpperBound - case let .PeerEntry(index, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _): return index - case let .HoleEntry(hole): + case let .HoleEntry(hole, _): return ChatListIndex(pinningIndex: nil, messageIndex: hole.index) - case let .Nothing(index): - return index } } @@ -83,12 +80,10 @@ enum ChatListNodeEntry: Comparable, Identifiable { switch self { case .SearchEntry: return .Search - case let .PeerEntry(index, _, _, _, _, _, _, _): + case let .PeerEntry(index, _, _, _, _, _, _, _, _, _): return .PeerId(index.messageIndex.id.peerId.toInt64()) - case let .HoleEntry(hole): + case let .HoleEntry(hole, _): return .Hole(Int64(hole.index.id.id)) - case let .Nothing(index): - return .PeerId(index.messageIndex.id.peerId.toInt64()) } } @@ -98,19 +93,24 @@ enum ChatListNodeEntry: Comparable, Identifiable { static func ==(lhs: ChatListNodeEntry, rhs: ChatListNodeEntry) -> Bool { switch lhs { - case .SearchEntry: - switch rhs { - case .SearchEntry: - return true - default: - return false + case let .SearchEntry(lhsTheme, lhsText): + if case let .SearchEntry(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false } - case let .PeerEntry(lhsIndex, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsEditing, lhsHasRevealControls): + case let .PeerEntry(lhsIndex, lhsTheme, lhsStrings, lhsMessage, lhsUnreadCount, lhsNotificationSettings, lhsEmbeddedState, lhsPeer, lhsEditing, lhsHasRevealControls): switch rhs { - case let .PeerEntry(rhsIndex, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsEditing, rhsHasRevealControls): + case let .PeerEntry(rhsIndex, rhsTheme, rhsStrings, rhsMessage, rhsUnreadCount, rhsNotificationSettings, rhsEmbeddedState, rhsPeer, rhsEditing, rhsHasRevealControls): if lhsIndex != rhsIndex { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if lhsMessage?.stableVersion != rhsMessage?.stableVersion { return false } @@ -141,25 +141,17 @@ enum ChatListNodeEntry: Comparable, Identifiable { return false } return true - default: - break - } - case let .HoleEntry(lhsHole): - switch rhs { - case let .HoleEntry(rhsHole): - return lhsHole == rhsHole default: return false } - case let .Nothing(lhsIndex): + case let .HoleEntry(lhsHole, lhsTheme): switch rhs { - case let .Nothing(rhsIndex): - return lhsIndex == rhsIndex + case let .HoleEntry(rhsHole, rhsTheme): + return lhsHole == rhsHole && lhsTheme === rhsTheme default: return false } } - return false } } @@ -168,13 +160,13 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState) for entry in view.entries { switch entry { case let .MessageEntry(index, message, combinedReadState, notificationSettings, embeddedState, peer): - result.append(.PeerEntry(index: index, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions)) + result.append(.PeerEntry(index: index, theme: state.theme, strings: state.strings, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions)) case let .HoleEntry(hole): - result.append(.HoleEntry(hole)) + result.append(.HoleEntry(hole, theme: state.theme)) } } if view.laterIndex == nil { - result.append(.SearchEntry) + result.append(.SearchEntry(theme: state.theme, text: state.strings.ChatSearch_SearchPlaceholder)) } return result } diff --git a/TelegramUI/ChatListRecentPeersListItem.swift b/TelegramUI/ChatListRecentPeersListItem.swift index bf7586512f..ccea2d2f63 100644 --- a/TelegramUI/ChatListRecentPeersListItem.swift +++ b/TelegramUI/ChatListRecentPeersListItem.swift @@ -7,13 +7,17 @@ import SwiftSignalKit import TelegramCore class ChatListRecentPeersListItem: ListViewItem { + let theme: PresentationTheme + let strings: PresentationStrings let account: Account let peers: [Peer] let peerSelected: (Peer) -> Void let header: ListViewItemHeader? - init(account: Account, peers: [Peer], peerSelected: @escaping (Peer) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peers: [Peer], peerSelected: @escaping (Peer) -> Void) { + self.theme = theme + self.strings = strings self.account = account self.peers = peers self.peerSelected = peerSelected @@ -60,11 +64,9 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { required init() { self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = .white self.backgroundNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) self.separatorNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) @@ -84,19 +86,32 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { } func asyncLayout() -> (_ item: ChatListRecentPeersListItem, _ width: CGFloat, _ last: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, () -> Void)) { + let currentItem = self.item + return { [weak self] item, width, last in let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 130.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) return (nodeLayout, { [weak self] in + var updatedTheme: PresentationTheme? + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + return (nil, { if let strongSelf = self { strongSelf.item = item + if let _ = updatedTheme { + strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + } + let peersNode: ChatListSearchRecentPeersNode if let currentPeersNode = strongSelf.peersNode { peersNode = currentPeersNode + peersNode.updateThemeAndStrings(theme: item.theme, strings: item.strings) } else { - peersNode = ChatListSearchRecentPeersNode(account: item.account, peerSelected: { peer in + peersNode = ChatListSearchRecentPeersNode(account: item.account, theme: item.theme, strings: item.strings, peerSelected: { peer in self?.item?.peerSelected(peer) }) strongSelf.peersNode = peersNode diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index d0930a86af..442830592b 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -37,22 +37,22 @@ enum ChatListRecentEntryStableId: Hashable { } enum ChatListRecentEntry: Comparable, Identifiable { - case topPeers([Peer]) - case peer(index: Int, peer: Peer) + case topPeers([Peer], PresentationTheme, PresentationStrings) + case peer(index: Int, peer: Peer, PresentationTheme, PresentationStrings) var stableId: ChatListRecentEntryStableId { switch self { case .topPeers: return .topPeers - case let .peer(_, peer): + case let .peer(_, peer, _, _): return .peerId(peer.id) } } static func ==(lhs: ChatListRecentEntry, rhs: ChatListRecentEntry) -> Bool { switch lhs { - case let .topPeers(lhsPeers): - if case let .topPeers(rhsPeers) = rhs { + case let .topPeers(lhsPeers, lhsTheme, lhsStrings): + if case let .topPeers(rhsPeers, rhsTheme, rhsStrings) = rhs { if lhsPeers.count != rhsPeers.count { return false } @@ -61,12 +61,18 @@ enum ChatListRecentEntry: Comparable, Identifiable { return false } } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } return true } else { return false } - case let .peer(lhsIndex, lhsPeer): - if case let .peer(rhsIndex, rhsPeer) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex { + case let .peer(lhsIndex, lhsPeer, lhsTheme, lhsStrings): + if case let .peer(rhsIndex, rhsPeer, rhsTheme, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings { return true } else { return false @@ -78,11 +84,11 @@ enum ChatListRecentEntry: Comparable, Identifiable { switch lhs { case .topPeers: return true - case let .peer(lhsIndex, _): + case let .peer(lhsIndex, _, _, _): switch rhs { case .topPeers: return false - case let .peer(rhsIndex, _): + case let .peer(rhsIndex, _, _, _): return lhsIndex <= rhsIndex } } @@ -90,12 +96,12 @@ enum ChatListRecentEntry: Comparable, Identifiable { func item(account: Account, peerSelected: @escaping (Peer) -> Void) -> ListViewItem { switch self { - case let .topPeers(peers): - return ChatListRecentPeersListItem(account: account, peers: peers, peerSelected: { peer in + case let .topPeers(peers, theme, strings): + return ChatListRecentPeersListItem(theme: theme, strings: strings, account: account, peers: peers, peerSelected: { peer in peerSelected(peer) }) - case let .peer(_, peer): - return ContactsPeerItem(account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .recentPeers), action: { _ in + case let .peer(_, peer, theme, strings): + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .recentPeers, theme: theme, strings: strings), action: { _ in peerSelected(peer) }) } @@ -145,43 +151,49 @@ enum ChatListSearchEntryStableId: Hashable { enum ChatListSearchEntry: Comparable, Identifiable { - case localPeer(Peer, Int) - case globalPeer(Peer, Int) - case message(Message) + case localPeer(Peer, Int, PresentationTheme, PresentationStrings) + case globalPeer(Peer, Int, PresentationTheme, PresentationStrings) + case message(Message, PresentationTheme, PresentationStrings) var stableId: ChatListSearchEntryStableId { switch self { - case let .localPeer(peer, _): + case let .localPeer(peer, _, _, _): return .localPeerId(peer.id) - case let .globalPeer(peer, _): + case let .globalPeer(peer, _, _, _): return .globalPeerId(peer.id) - case let .message(message): + case let .message(message, _, _): return .messageId(message.id) } } static func ==(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool { switch lhs { - case let .localPeer(lhsPeer, lhsIndex): - if case let .localPeer(rhsPeer, rhsIndex) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex { + case let .localPeer(lhsPeer, lhsIndex, lhsTheme, lhsStrings): + if case let .localPeer(rhsPeer, rhsIndex, rhsTheme, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings { return true } else { return false } - case let .globalPeer(lhsPeer, lhsIndex): - if case let .globalPeer(rhsPeer, rhsIndex) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex { + case let .globalPeer(lhsPeer, lhsIndex, lhsTheme, lhsStrings): + if case let .globalPeer(rhsPeer, rhsIndex, rhsTheme, rhsStrings) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings { return true } else { return false } - case let .message(lhsMessage): - if case let .message(rhsMessage) = rhs { + case let .message(lhsMessage, lhsTheme, lhsStrings): + if case let .message(rhsMessage, rhsTheme, rhsStrings) = rhs { if lhsMessage.id != rhsMessage.id { return false } if lhsMessage.stableVersion != rhsMessage.stableVersion { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } return true } else { return false @@ -191,23 +203,23 @@ enum ChatListSearchEntry: Comparable, Identifiable { static func <(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool { switch lhs { - case let .localPeer(_, lhsIndex): - if case let .localPeer(_, rhsIndex) = rhs { + case let .localPeer(_, lhsIndex, _, _): + if case let .localPeer(_, rhsIndex, _, _) = rhs { return lhsIndex <= rhsIndex } else { return true } - case let .globalPeer(_, lhsIndex): + case let .globalPeer(_, lhsIndex, _, _): switch rhs { case .localPeer: return false - case let .globalPeer(_, rhsIndex): + case let .globalPeer(_, rhsIndex, _, _): return lhsIndex <= rhsIndex case .message: return true } - case let .message(lhsMessage): - if case let .message(rhsMessage) = rhs { + case let .message(lhsMessage, _, _): + if case let .message(rhsMessage, _, _) = rhs { return MessageIndex(lhsMessage) < MessageIndex(rhsMessage) } else { return false @@ -217,16 +229,16 @@ enum ChatListSearchEntry: Comparable, Identifiable { func item(account: Account, enableHeaders: Bool, interaction: ChatListNodeInteraction) -> ListViewItem { switch self { - case let .localPeer(peer, _): - return ContactsPeerItem(account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .localPeers), action: { _ in + case let .localPeer(peer, _, theme, strings): + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .localPeers, theme: theme, strings: strings), action: { _ in interaction.peerSelected(peer) }) - case let .globalPeer(peer, _): - return ContactsPeerItem(account: account, peer: peer, chatPeer: peer, status: .addressName, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .globalPeers), action: { _ in + case let .globalPeer(peer, _, theme, strings): + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .addressName, selection: .none, index: nil, header: ChatListSearchItemHeader(type: .globalPeers, theme: theme, strings: strings), action: { _ in interaction.peerSelected(peer) }) - case let .message(message): - return ChatListItem(account: account, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(message)), message: message, peer: RenderedPeer(message: message), combinedReadState: nil, notificationSettings: nil, embeddedState: nil, editing: false, hasActiveRevealControls: false, header: enableHeaders ? ChatListSearchItemHeader(type: .messages) : nil, interaction: interaction) + case let .message(message, theme, strings): + return ChatListItem(theme: theme, strings: strings, account: account, index: ChatListIndex(pinningIndex: nil, messageIndex: MessageIndex(message)), message: message, peer: RenderedPeer(message: message), combinedReadState: nil, notificationSettings: nil, embeddedState: nil, editing: false, hasActiveRevealControls: false, header: enableHeaders ? ChatListSearchItemHeader(type: .messages, theme: theme, strings: strings) : nil, interaction: interaction) } } } @@ -281,10 +293,18 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { private let searchQuery = Promise() private let searchDisposable = MetaDisposable() + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + init(account: Account, openPeer: @escaping (Peer) -> Void, openMessage: @escaping (Peer, MessageId) -> Void) { self.account = account self.openMessage = openMessage + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) + //self.recentPeersNode = ChatListSearchRecentPeersNode(account: account, peerSelected: openPeer) self.recentListNode = ListView() @@ -292,7 +312,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { super.init() - self.backgroundColor = UIColor.white + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor //self.addSubnode(self.recentPeersNode) self.addSubnode(self.recentListNode) @@ -300,41 +320,38 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { self.listNode.isHidden = true + let themeAndStringsPromise = self.themeAndStringsPromise let foundItems = searchQuery.get() |> mapToSignal { query -> Signal<[ChatListSearchEntry]?, NoError> in if let query = query, !query.isEmpty { let foundLocalPeers = account.postbox.searchPeers(query: query.lowercased()) - |> map { peers -> [ChatListSearchEntry] in + let foundRemotePeers: Signal<[Peer], NoError> = .single([]) |> then(searchPeers(account: account, query: query) + |> delay(0.2, queue: Queue.concurrentDefaultQueue())) + let foundRemoteMessages: Signal<[Message], NoError> = .single([]) |> then(searchMessages(account: account, peerId: nil, query: query) + |> delay(0.2, queue: Queue.concurrentDefaultQueue())) + + return combineLatest(foundLocalPeers, foundRemotePeers, foundRemoteMessages, themeAndStringsPromise.get()) + |> map { foundLocalPeers, foundRemotePeers, foundRemoteMessages, themeAndStrings -> [ChatListSearchEntry]? in var entries: [ChatListSearchEntry] = [] var index = 0 - for peer in peers { - entries.append(.localPeer(peer, index)) + for peer in foundLocalPeers { + entries.append(.localPeer(peer, index, themeAndStrings.0, themeAndStrings.1)) index += 1 } - return entries - } - - let foundRemotePeers: Signal<[ChatListSearchEntry], NoError> = .single([]) |> then(searchPeers(account: account, query: query) - |> delay(0.2, queue: Queue.concurrentDefaultQueue()) - |> map { peers -> [ChatListSearchEntry] in - var entries: [ChatListSearchEntry] = [] - var index = 0 - for peer in peers { - entries.append(.globalPeer(peer, index)) + + index = 0 + for peer in foundRemotePeers { + entries.append(.globalPeer(peer, index, themeAndStrings.0, themeAndStrings.1)) index += 1 } + + index = 0 + for message in foundRemoteMessages { + entries.append(.message(message, themeAndStrings.0, themeAndStrings.1)) + index += 1 + } + return entries - }) - - let foundRemoteMessages: Signal<[ChatListSearchEntry], NoError> = .single([]) |> then(searchMessages(account: account, peerId: nil, query: query) - |> delay(0.2, queue: Queue.concurrentDefaultQueue()) - |> map { messages -> [ChatListSearchEntry] in - return messages.map({ .message($0) }) - }) - - return combineLatest(foundLocalPeers, foundRemotePeers, foundRemoteMessages) - |> map { localPeers, remotePeers, remoteMessages -> [ChatListSearchEntry]? in - return localPeers + remotePeers + remoteMessages } } else { return .single(nil) @@ -360,12 +377,12 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { }) let previousRecentItems = Atomic<[ChatListRecentEntry]?>(value: nil) - let recentItemsTransition = recentlySearchedPeers(postbox: account.postbox) - |> mapToSignal { [weak self] peers -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in + let recentItemsTransition = combineLatest(recentlySearchedPeers(postbox: account.postbox), themeAndStringsPromise.get()) + |> mapToSignal { [weak self] peers, themeAndStrings -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in var entries: [ChatListRecentEntry] = [] - entries.append(.topPeers([])) + entries.append(.topPeers([], themeAndStrings.0, themeAndStrings.1)) for i in 0 ..< peers.count { - entries.append(.peer(index: i, peer: peers[i])) + entries.append(.peer(index: i, peer: peers[i], themeAndStrings.0, themeAndStrings.1)) } let previousEntries = previousRecentItems.swap(entries) @@ -392,11 +409,30 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) + + 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(theme: presentationData.theme, strings: presentationData.strings) + } + } + }) } deinit { self.recentDisposable.dispose() self.searchDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.backgroundColor = theme.chatList.backgroundColor } override func searchTextUpdated(text: String) { diff --git a/TelegramUI/ChatListSearchItem.swift b/TelegramUI/ChatListSearchItem.swift index d487d56696..57dd7b6e41 100644 --- a/TelegramUI/ChatListSearchItem.swift +++ b/TelegramUI/ChatListSearchItem.swift @@ -6,17 +6,16 @@ import Display import SwiftSignalKit private let searchBarFont = Font.regular(15.0) -private let pinnedBackgroundColor = UIColor(0xf7f7f7) -private let regularSearchBackgroundColor = UIColor(0xededed) -private let pinnedSearchBackgroundColor = UIColor(0xe5e5e5) class ChatListSearchItem: ListViewItem { let selectable: Bool = false + let theme: PresentationTheme private let placeholder: String private let activate: () -> Void - init(placeholder: String, activate: @escaping () -> Void) { + init(theme: PresentationTheme, placeholder: String, activate: @escaping () -> Void) { + self.theme = theme self.placeholder = placeholder self.activate = activate } @@ -31,7 +30,7 @@ class ChatListSearchItem: ListViewItem { if let nextItem = nextItem as? ChatListItem, nextItem.index.pinningIndex != nil { nextIsPinned = true } - let (layout, apply) = makeLayout(width, nextIsPinned) + let (layout, apply) = makeLayout(self, width, nextIsPinned) node.contentSize = layout.contentSize node.insets = layout.insets @@ -54,7 +53,7 @@ class ChatListSearchItem: ListViewItem { if let nextItem = nextItem as? ChatListItem, nextItem.index.pinningIndex != nil { nextIsPinned = true } - let (nodeLayout, apply) = layout(width, nextIsPinned) + let (nodeLayout, apply) = layout(self, width, nextIsPinned) Queue.mainQueue().async { completion(nodeLayout, { apply(animation.isAnimated) @@ -90,18 +89,18 @@ class ChatListSearchItemNode: ListViewItemNode { if let nextItem = nextItem as? ChatListItem, nextItem.index.pinningIndex != nil { nextIsPinned = true } - let (layout, apply) = makeLayout(width, nextIsPinned) + let (layout, apply) = makeLayout(item as! ChatListSearchItem, width, nextIsPinned) apply(false) self.contentSize = layout.contentSize self.insets = layout.insets } - func asyncLayout() -> (_ width: CGFloat, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) { + func asyncLayout() -> (_ item: ChatListSearchItem, _ width: CGFloat, _ nextIsPinned: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) { let searchBarNodeLayout = self.searchBarNode.asyncLayout() let placeholder = self.placeholder - return { width, nextIsPinned in - let searchBarApply = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "Search", font: searchBarFont, textColor: UIColor(0x8e8e93)), CGSize(width: width - 16.0, height: CGFloat.greatestFiniteMagnitude), nextIsPinned ? pinnedSearchBackgroundColor : regularSearchBackgroundColor) + return { item, width, nextIsPinned in + let searchBarApply = searchBarNodeLayout(NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93)), CGSize(width: width - 16.0, height: CGFloat.greatestFiniteMagnitude), nextIsPinned ? item.theme.chatList.pinnedSearchBarColor : item.theme.chatList.regularSearchBarColor, nextIsPinned ? item.theme.chatList.itemBackgroundColor : item.theme.chatList.pinnedItemBackgroundColor) let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 44.0 + 4.0), insets: UIEdgeInsets()) @@ -119,7 +118,7 @@ class ChatListSearchItemNode: ListViewItemNode { strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: width - 16.0, height: 28.0)) - transition.updateBackgroundColor(node: strongSelf, color: nextIsPinned ? pinnedBackgroundColor : UIColor.white) + transition.updateBackgroundColor(node: strongSelf, color: nextIsPinned ? item.theme.chatList.pinnedItemBackgroundColor : item.theme.chatList.itemBackgroundColor) } }) } diff --git a/TelegramUI/ChatListSearchItemHeader.swift b/TelegramUI/ChatListSearchItemHeader.swift index 9f09fd6b78..a07a75872c 100644 --- a/TelegramUI/ChatListSearchItemHeader.swift +++ b/TelegramUI/ChatListSearchItemHeader.swift @@ -12,40 +12,48 @@ final class ChatListSearchItemHeader: ListViewItemHeader { let id: Int64 let type: ChatListSearchItemHeaderType let stickDirection: ListViewItemHeaderStickDirection = .top + let theme: PresentationTheme + let strings: PresentationStrings let height: CGFloat = 29.0 - init(type: ChatListSearchItemHeaderType) { + init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings) { self.type = type self.id = Int64(self.type.rawValue) + self.theme = theme + self.strings = strings } func node() -> ListViewItemHeaderNode { - return ChatListSearchItemHeaderNode(type: self.type) + return ChatListSearchItemHeaderNode(type: self.type, theme: self.theme, strings: self.strings) } } final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode { private let type: ChatListSearchItemHeaderType + private var theme: PresentationTheme + private var strings: PresentationStrings private let sectionHeaderNode: ListSectionHeaderNode - init(type: ChatListSearchItemHeaderType) { + init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings) { self.type = type + self.theme = theme + self.strings = strings - self.sectionHeaderNode = ListSectionHeaderNode() + self.sectionHeaderNode = ListSectionHeaderNode(theme: theme) super.init() switch type { case .localPeers: - self.sectionHeaderNode.title = "CHATS AND CONTACTS" + self.sectionHeaderNode.title = strings.DialogList_SearchSectionDialogs.uppercased() case .globalPeers: - self.sectionHeaderNode.title = "GLOBAL SEARCH" + self.sectionHeaderNode.title = strings.DialogList_SearchSectionGlobal.uppercased() case .messages: - self.sectionHeaderNode.title = "MESSAGES" + self.sectionHeaderNode.title = strings.DialogList_SearchSectionMessages.uppercased() case .recentPeers: - self.sectionHeaderNode.title = "RECENT" + self.sectionHeaderNode.title = strings.DialogList_SearchSectionRecent.uppercased() } self.addSubnode(self.sectionHeaderNode) diff --git a/TelegramUI/ChatListSearchRecentPeersNode.swift b/TelegramUI/ChatListSearchRecentPeersNode.swift index 21986b4712..40d7dda1c9 100644 --- a/TelegramUI/ChatListSearchRecentPeersNode.swift +++ b/TelegramUI/ChatListSearchRecentPeersNode.swift @@ -6,17 +6,22 @@ import Postbox import TelegramCore final class ChatListSearchRecentPeersNode: ASDisplayNode { + private var theme: PresentationTheme + private var strings: PresentationStrings private let sectionHeaderNode: ListSectionHeaderNode private let listView: ListView private let disposable = MetaDisposable() - init(account: Account, peerSelected: @escaping (Peer) -> Void) { - self.sectionHeaderNode = ListSectionHeaderNode() - self.sectionHeaderNode.title = "PEOPLE" + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peerSelected: @escaping (Peer) -> Void) { + self.theme = theme + self.strings = strings + + self.sectionHeaderNode = ListSectionHeaderNode(theme: theme) + self.sectionHeaderNode.title = strings.DialogList_RecentTitlePeople self.listView = ListView() - self.listView.transform = CATransform3DMakeRotation(-CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) super.init() @@ -27,7 +32,7 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { if let strongSelf = self { var items: [ListViewItem] = [] for peer in peers { - items.append(HorizontalPeerItem(account: account, peer: peer, action: peerSelected)) + items.append(HorizontalPeerItem(theme: strongSelf.theme, strings: strongSelf.strings, account: account, peer: peer, action: peerSelected)) } strongSelf.listView.transaction(deleteIndices: [], insertIndicesAndItems: (0 ..< items.count).map({ ListViewInsertItem(index: $0, previousIndex: nil, item: items[$0], directionHint: .Down) }), updateIndicesAndItems: [], options: [], updateOpaqueState: nil) } @@ -38,6 +43,16 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { disposable.dispose() } + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if self.theme !== theme || self.strings !== strings { + self.theme = theme + self.strings = strings + + self.sectionHeaderNode.title = strings.DialogList_RecentTitlePeople + self.sectionHeaderNode.updateTheme(theme: theme) + } + } + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: constrainedSize.width, height: 120.0) } diff --git a/TelegramUI/ChatListTitleLockView.swift b/TelegramUI/ChatListTitleLockView.swift index eb8ff0a17f..28a5f182cb 100644 --- a/TelegramUI/ChatListTitleLockView.swift +++ b/TelegramUI/ChatListTitleLockView.swift @@ -1,11 +1,6 @@ import UIKit import Display -private let topLockedImage = generateTintedImage(image: UIImage(bundleImageName: "Chat List/LockLockedTop"), color: UIColor(0x007ee5)) -private let topUnlockedImage = UIImage(bundleImageName: "Chat List/LockUnlockedTop")?.preloaded() -private let bottomLockedImage = generateTintedImage(image: UIImage(bundleImageName: "Chat List/LockLockedBottom"), color: UIColor(0x007ee5)) -private let bottomUnlockedImage = UIImage(bundleImageName: "Chat List/LockUnlockedBottom")?.preloaded() - final class ChatListTitleLockView: UIView { private let topView: UIImageView private let bottomView: UIImageView @@ -26,7 +21,7 @@ final class ChatListTitleLockView: UIView { fatalError("init(coder:) has not been implemented") } - func setIsLocked(_ isLocked: Bool, animated: Bool) { + func setIsLocked( _ isLocked: Bool, theme: PresentationTheme, animated: Bool) { self.isLocked = isLocked if animated { let topViewCopy = UIImageView(image: self.topView.image) @@ -37,8 +32,8 @@ final class ChatListTitleLockView: UIView { bottomViewCopy.frame = self.bottomView.frame self.addSubview(bottomViewCopy) - self.topView.image = self.isLocked ? topLockedImage : topUnlockedImage - self.bottomView.image = self.isLocked ? bottomLockedImage : bottomUnlockedImage + self.topView.image = self.isLocked ? PresentationResourcesChatList.lockTopLockedImage(theme) : PresentationResourcesChatList.lockTopUnlockedImage(theme) + self.bottomView.image = self.isLocked ? PresentationResourcesChatList.lockBottomLockedImage(theme) : PresentationResourcesChatList.lockBottomUnlockedImage(theme) self.topView.alpha = 0.5 self.bottomView.alpha = 0.5 @@ -64,8 +59,8 @@ final class ChatListTitleLockView: UIView { bottomViewCopy.removeFromSuperview() }) } else { - self.topView.image = self.isLocked ? topLockedImage : topUnlockedImage - self.bottomView.image = self.isLocked ? bottomLockedImage : bottomUnlockedImage + self.topView.image = self.isLocked ? PresentationResourcesChatList.lockTopLockedImage(theme) : PresentationResourcesChatList.lockTopUnlockedImage(theme) + self.bottomView.image = self.isLocked ? PresentationResourcesChatList.lockBottomLockedImage(theme) : PresentationResourcesChatList.lockBottomUnlockedImage(theme) self.layoutItems() } } diff --git a/TelegramUI/ChatMediaActionSheetRollItem.swift b/TelegramUI/ChatMediaActionSheetRollItem.swift index 5c414de571..b1a1fb91e1 100644 --- a/TelegramUI/ChatMediaActionSheetRollItem.swift +++ b/TelegramUI/ChatMediaActionSheetRollItem.swift @@ -39,7 +39,7 @@ private final class ChatMediaActionSheetRollItemNode: ActionSheetItemNode, PHPho self.label = UILabel() self.label.backgroundColor = nil self.label.isOpaque = false - self.label.textColor = UIColor(0x007ee5) + self.label.textColor = UIColor(rgb: 0x007ee5) self.label.text = "Photo or Video" self.label.font = Font.regular(20.0) self.label.sizeToFit() diff --git a/TelegramUI/ChatMediaInputGifPane.swift b/TelegramUI/ChatMediaInputGifPane.swift index 61952faf9a..ab8a4f2c19 100644 --- a/TelegramUI/ChatMediaInputGifPane.swift +++ b/TelegramUI/ChatMediaInputGifPane.swift @@ -31,7 +31,7 @@ final class ChatMediaInputGifPane: ASDisplayNode, UIScrollViewDelegate { } self.disposable.set((gifs |> deliverOnMainQueue).start(next: { [weak self] gifs in if let strongSelf = self { - strongSelf.multiplexedNode.files = gifs + strongSelf.multiplexedNode.files = [gifs.first!] } })) diff --git a/TelegramUI/ChatMediaInputGridEntries.swift b/TelegramUI/ChatMediaInputGridEntries.swift index 6dfb0b0e6d..138bbb1c9b 100644 --- a/TelegramUI/ChatMediaInputGridEntries.swift +++ b/TelegramUI/ChatMediaInputGridEntries.swift @@ -20,13 +20,14 @@ struct ChatMediaInputGridEntry: Comparable, Identifiable { let index: ItemCollectionViewEntryIndex let stickerItem: StickerPackItem let stickerPackInfo: StickerPackCollectionInfo? + let theme: PresentationTheme var stableId: ChatMediaInputGridEntryStableId { return ChatMediaInputGridEntryStableId(collectionId: self.index.collectionId, itemId: self.stickerItem.index.id) } static func ==(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool { - return lhs.index == rhs.index && lhs.stickerItem == rhs.stickerItem + return lhs.index == rhs.index && lhs.stickerItem == rhs.stickerItem && lhs.theme === rhs.theme } static func <(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool { @@ -34,6 +35,6 @@ struct ChatMediaInputGridEntry: Comparable, Identifiable { } func item(account: Account, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> GridItem { - return ChatMediaInputStickerGridItem(account: account, collectionId: self.index.collectionId, stickerPackInfo: self.stickerPackInfo, index: self.index, stickerItem: self.stickerItem, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, selected: { }) + return ChatMediaInputStickerGridItem(account: account, collectionId: self.index.collectionId, stickerPackInfo: self.stickerPackInfo, index: self.index, stickerItem: self.stickerItem, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, theme: self.theme, selected: { }) } } diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index b7e86edd3c..e914d70a68 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -81,23 +81,23 @@ private func preparedChatMediaInputGridEntryTransition(account: Account, from fr return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem) } -private func chatMediaInputPanelEntries(view: ItemCollectionsView, recentStickers: OrderedItemListView?) -> [ChatMediaInputPanelEntry] { +private func chatMediaInputPanelEntries(view: ItemCollectionsView, recentStickers: OrderedItemListView?, theme: PresentationTheme) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] - entries.append(.recentGifs) + entries.append(.recentGifs(theme)) if let recentStickers = recentStickers, !recentStickers.items.isEmpty { - entries.append(.recentPacks) + entries.append(.recentPacks(theme)) } var index = 0 for (_, info, item) in view.collectionInfos { if let info = info as? StickerPackCollectionInfo { - entries.append(.stickerPack(index: index, info: info, topItem: item as? StickerPackItem)) + entries.append(.stickerPack(index: index, info: info, topItem: item as? StickerPackItem, theme: theme)) index += 1 } } return entries } -private func chatMediaInputGridEntries(view: ItemCollectionsView, recentStickers: OrderedItemListView?) -> [ChatMediaInputGridEntry] { +private func chatMediaInputGridEntries(view: ItemCollectionsView, recentStickers: OrderedItemListView?, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] { var entries: [ChatMediaInputGridEntry] = [] var stickerPackInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:] @@ -113,14 +113,14 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, recentStickers if let item = recentStickers.items[i].contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile, let mediaId = item.media.id { let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id) let stickerItem = StickerPackItem(index: index, file: file, indexKeys: []) - entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo)) + entries.append(ChatMediaInputGridEntry(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, theme: theme)) } } } for entry in view.entries { if let item = entry.item as? StickerPackItem { - entries.append(ChatMediaInputGridEntry(index: entry.index, stickerItem: item, stickerPackInfo: stickerPackInfos[entry.index.collectionId])) + entries.append(ChatMediaInputGridEntry(index: entry.index, stickerItem: item, stickerPackInfo: stickerPackInfos[entry.index.collectionId], theme: theme)) } } return entries @@ -228,16 +228,24 @@ final class ChatMediaInputNode: ChatInputNode { private var validLayout: (CGFloat, ChatPresentationInterfaceState)? private var paneArrangement: ChatMediaInputPaneArrangement - init(account: Account, controllerInteraction: ChatControllerInteraction) { + private var theme: PresentationTheme + private var strings: PresentationStrings + private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + + init(account: Account, controllerInteraction: ChatControllerInteraction, theme: PresentationTheme, strings: PresentationStrings) { self.account = account self.controllerInteraction = controllerInteraction + self.theme = theme + self.strings = strings + + self.themeAndStringsPromise = Promise((theme, strings)) self.collectionListPanel = ASDisplayNode() - self.collectionListPanel.backgroundColor = UIColor(0xF5F6F8) + self.collectionListPanel.backgroundColor = theme.chat.inputPanel.panelBackgroundColor self.collectionListSeparator = ASDisplayNode() self.collectionListSeparator.isLayerBacked = true - self.collectionListSeparator.backgroundColor = UIColor(0xBEC2C6) + self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSerapatorColor self.listView = ListView() self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) @@ -274,7 +282,7 @@ final class ChatMediaInputNode: ChatInputNode { }) self.clipsToBounds = true - self.backgroundColor = UIColor(0xE8EBF0) + self.backgroundColor = theme.chat.inputMediaPanel.gifsBackgroundColor self.addSubnode(self.collectionListPanel) self.addSubnode(self.collectionListSeparator) @@ -322,8 +330,11 @@ final class ChatMediaInputNode: ChatInputNode { let inputNodeInteraction = self.inputNodeInteraction! - let transitions = itemCollectionsView - |> map { (view, update) -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in + let transitions = combineLatest(itemCollectionsView, self.themeAndStringsPromise.get()) + |> map { viewAndUpdate, themeAndStrings -> (ItemCollectionsView, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in + let (view, update) = viewAndUpdate + let (theme, strings) = themeAndStrings + var recentStickers: OrderedItemListView? for orderedView in view.orderedItemListsViews { if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { @@ -331,8 +342,8 @@ final class ChatMediaInputNode: ChatInputNode { break } } - let panelEntries = chatMediaInputPanelEntries(view: view, recentStickers: recentStickers) - let gridEntries = chatMediaInputGridEntries(view: view, recentStickers: recentStickers) + let panelEntries = chatMediaInputPanelEntries(view: view, recentStickers: recentStickers, theme: theme) + let gridEntries = chatMediaInputGridEntries(view: view, recentStickers: recentStickers, strings: strings, theme: theme) let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntries, gridEntries)) return (view, preparedChatMediaInputPanelEntryTransition(account: account, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: account, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction), previousGridEntries.isEmpty) } @@ -395,6 +406,19 @@ final class ChatMediaInputNode: ChatInputNode { self.disposable.dispose() } + private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if self.theme !== theme || self.strings !== strings { + self.theme = theme + self.strings = strings + + self.collectionListPanel.backgroundColor = theme.chat.inputPanel.panelBackgroundColor + self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSerapatorColor + self.backgroundColor = theme.chat.inputMediaPanel.gifsBackgroundColor + + self.themeAndStringsPromise.set(.single((theme, strings))) + } + } + override func didLoad() { super.didLoad() @@ -473,6 +497,11 @@ final class ChatMediaInputNode: ChatInputNode { override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { self.validLayout = (width, interfaceState) + + if self.theme !== interfaceState.theme || self.strings !== interfaceState.strings { + self.updateThemeAndStrings(theme: interfaceState.theme, strings: interfaceState.strings) + } + let separatorHeight = UIScreenPixel let panelHeight = self.heightForWidth(width: width) diff --git a/TelegramUI/ChatMediaInputPanelEntries.swift b/TelegramUI/ChatMediaInputPanelEntries.swift index 5664245d7a..c526558b1f 100644 --- a/TelegramUI/ChatMediaInputPanelEntries.swift +++ b/TelegramUI/ChatMediaInputPanelEntries.swift @@ -28,8 +28,8 @@ enum ChatMediaInputPanelEntryStableId: Hashable { } else { return false } - case let .stickerPack(id): - if case .stickerPack(id) = rhs { + case let .stickerPack(lhsId): + if case let .stickerPack(rhsId) = rhs, lhsId == rhsId { return true } else { return false @@ -50,9 +50,9 @@ enum ChatMediaInputPanelEntryStableId: Hashable { } enum ChatMediaInputPanelEntry: Comparable, Identifiable { - case recentGifs - case recentPacks - case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?) + case recentGifs(PresentationTheme) + case recentPacks(PresentationTheme) + case stickerPack(index: Int, info: StickerPackCollectionInfo, topItem: StickerPackItem?, theme: PresentationTheme) var stableId: ChatMediaInputPanelEntryStableId { switch self { @@ -60,27 +60,27 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { return .recentGifs case .recentPacks: return .recentPacks - case let .stickerPack(_, info, _): + case let .stickerPack(_, info, _, _): return .stickerPack(info.id.id) } } static func ==(lhs: ChatMediaInputPanelEntry, rhs: ChatMediaInputPanelEntry) -> Bool { switch lhs { - case .recentGifs: - if case .recentGifs = rhs { + case let .recentGifs(lhsTheme): + if case let .recentGifs(rhsTheme) = rhs, lhsTheme === rhsTheme { return true } else { return false } - case .recentPacks: - if case .recentPacks = rhs { + case let .recentPacks(lhsTheme): + if case let .recentPacks(rhsTheme) = rhs, lhsTheme === rhsTheme { return true } else { return false } - case let .stickerPack(index, info, topItem): - if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem { + case let .stickerPack(index, info, topItem, lhsTheme): + if case let .stickerPack(rhsIndex, rhsInfo, rhsTopItem, rhsTheme) = rhs, index == rhsIndex, info == rhsInfo, topItem == rhsTopItem, lhsTheme === rhsTheme { return true } else { return false @@ -97,7 +97,6 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { default: return true } - return true case .recentPacks: switch rhs { case .recentGifs, recentPacks: @@ -105,11 +104,11 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { default: return true } - case let .stickerPack(lhsIndex, lhsInfo, _): + case let .stickerPack(lhsIndex, lhsInfo, _, _): switch rhs { case .recentGifs, .recentPacks: return false - case let .stickerPack(rhsIndex, rhsInfo, _): + case let .stickerPack(rhsIndex, rhsInfo, _, _): if lhsIndex == rhsIndex { return lhsInfo.id.id < rhsInfo.id.id } else { @@ -121,18 +120,18 @@ enum ChatMediaInputPanelEntry: Comparable, Identifiable { func item(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction) -> ListViewItem { switch self { - case .recentGifs: - return ChatMediaInputRecentGifsItem(inputNodeInteraction: inputNodeInteraction, selected: { + case let .recentGifs(theme): + return ChatMediaInputRecentGifsItem(inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue, id: 0) inputNodeInteraction.navigateToCollectionId(collectionId) }) - case .recentPacks: - return ChatMediaInputRecentStickerPacksItem(inputNodeInteraction: inputNodeInteraction, selected: { + case let .recentPacks(theme): + return ChatMediaInputRecentStickerPacksItem(inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { let collectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) inputNodeInteraction.navigateToCollectionId(collectionId) }) - case let .stickerPack(index, info, topItem): - return ChatMediaInputStickerPackItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, stickerPackItem: topItem, index: index, selected: { + case let .stickerPack(index, info, topItem, theme): + return ChatMediaInputStickerPackItem(account: account, inputNodeInteraction: inputNodeInteraction, collectionId: info.id, stickerPackItem: topItem, index: index, theme: theme, selected: { inputNodeInteraction.navigateToCollectionId(info.id) }) } diff --git a/TelegramUI/ChatMediaInputRecentGifsItem.swift b/TelegramUI/ChatMediaInputRecentGifsItem.swift index 09aac21abb..f717c021d0 100644 --- a/TelegramUI/ChatMediaInputRecentGifsItem.swift +++ b/TelegramUI/ChatMediaInputRecentGifsItem.swift @@ -5,40 +5,19 @@ import TelegramCore import SwiftSignalKit import Postbox -private let iconImage = generateImage(CGSize(width: 26.0, height: 26.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x9099A2).cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - let diameter: CGFloat = 22.0 - context.strokeEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - diameter) / 2.0), y: floor((size.width - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) - context.setFillColor(UIColor(0x9099A2).cgColor) - UIGraphicsPushContext(context) - - context.setTextDrawingMode(.stroke) - context.setLineWidth(0.65) - - ("GIF" as NSString).draw(in: CGRect(origin: CGPoint(x: 6.0, y: 8.0), size: size), withAttributes: [NSFontAttributeName: Font.regular(8.0), NSForegroundColorAttributeName: UIColor(0x9099A2)]) - - context.setTextDrawingMode(.fill) - context.setLineWidth(0.8) - - ("GIF" as NSString).draw(in: CGRect(origin: CGPoint(x: 6.0, y: 8.0), size: size), withAttributes: [NSFontAttributeName: Font.regular(8.0), NSForegroundColorAttributeName: UIColor(0x9099A2)]) - //("GIF" as NSString).draw(in: CGRect(origin: CGPoint(x: 6.0, y: 8.0), size: size), withAttributes: [NSFontAttributeName: Font.bold(8.0), NSForegroundColorAttributeName: UIColor(0x9099A2)]) - UIGraphicsPopContext() -}) - final class ChatMediaInputRecentGifsItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction let selectedItem: () -> Void + let theme: PresentationTheme var selectable: Bool { return true } - init(inputNodeInteraction: ChatMediaInputNodeInteraction, selected: @escaping () -> Void) { + init(inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping () -> Void) { self.inputNodeInteraction = inputNodeInteraction self.selectedItem = selected + self.theme = theme } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { @@ -47,6 +26,7 @@ final class ChatMediaInputRecentGifsItem: ListViewItem { node.contentSize = CGSize(width: 41.0, height: 41.0) node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) node.inputNodeInteraction = self.inputNodeInteraction + node.updateTheme(theme: self.theme) completion(node, { return (nil, {}) }) @@ -55,6 +35,7 @@ final class ChatMediaInputRecentGifsItem: ListViewItem { public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + (node as? ChatMediaInputRecentGifsItemNode)?.updateTheme(theme: self.theme) }) } @@ -68,7 +49,7 @@ private let boundingImageSize = CGSize(width: 30.0, height: 30.0) private let highlightSize = CGSize(width: 35.0, height: 35.0) private let verticalOffset: CGFloat = 3.0 + UIScreenPixel -private let highlightBackground = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(0x9099A2, 0.2)) +private let highlightBackground = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(rgb: 0x9099A2, alpha: 0.2)) final class ChatMediaInputRecentGifsItemNode: ListViewItemNode { private let imageNode: ASImageNode @@ -77,10 +58,11 @@ final class ChatMediaInputRecentGifsItemNode: ListViewItemNode { var currentCollectionId: ItemCollectionId? var inputNodeInteraction: ChatMediaInputNodeInteraction? + var theme: PresentationTheme? + init() { self.highlightNode = ASImageNode() self.highlightNode.isLayerBacked = true - self.highlightNode.image = highlightBackground self.highlightNode.isHidden = true self.imageNode = ASImageNode() @@ -88,7 +70,6 @@ final class ChatMediaInputRecentGifsItemNode: ListViewItemNode { self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) - self.imageNode.image = iconImage self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) super.init(layerBacked: false, dynamicBounce: false) @@ -105,9 +86,13 @@ final class ChatMediaInputRecentGifsItemNode: ListViewItemNode { deinit { } - func updateStickerPackItem(account: Account, item: StickerPackItem?, collectionId: ItemCollectionId) { - self.currentCollectionId = collectionId - self.updateIsHighlighted() + func updateTheme(theme: PresentationTheme) { + if self.theme !== theme { + self.theme = theme + + self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) + self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentGifsIconImage(theme) + } } func updateIsHighlighted() { diff --git a/TelegramUI/ChatMediaInputRecentStickerPacksItem.swift b/TelegramUI/ChatMediaInputRecentStickerPacksItem.swift index f2d09c9e34..72d71c135f 100644 --- a/TelegramUI/ChatMediaInputRecentStickerPacksItem.swift +++ b/TelegramUI/ChatMediaInputRecentStickerPacksItem.swift @@ -5,31 +5,19 @@ import TelegramCore import SwiftSignalKit import Postbox -private let iconImage = generateImage(CGSize(width: 26.0, height: 26.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x9099A2).cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - let diameter: CGFloat = 22.0 - context.strokeEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - diameter) / 2.0), y: floor((size.width - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) - context.translateBy(x: 1.5, y: 2.5) - context.move(to: CGPoint(x: 11.0, y: 5.5)) - context.addLine(to: CGPoint(x: 11.0, y: 11.0)) - context.addLine(to: CGPoint(x: 14.5, y: 14.5)) - context.strokePath() -}) - final class ChatMediaInputRecentStickerPacksItem: ListViewItem { let inputNodeInteraction: ChatMediaInputNodeInteraction + let theme: PresentationTheme let selectedItem: () -> Void var selectable: Bool { return true } - init(inputNodeInteraction: ChatMediaInputNodeInteraction, selected: @escaping () -> Void) { + init(inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping () -> Void) { self.inputNodeInteraction = inputNodeInteraction self.selectedItem = selected + self.theme = theme } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { @@ -38,14 +26,18 @@ final class ChatMediaInputRecentStickerPacksItem: ListViewItem { node.contentSize = CGSize(width: 41.0, height: 41.0) node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) node.inputNodeInteraction = self.inputNodeInteraction + node.updateTheme(theme: self.theme) completion(node, { - return (nil, {}) + return (nil, { + + }) }) } } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + (node as? ChatMediaInputRecentStickerPacksItemNode)?.updateTheme(theme: self.theme) }) } @@ -59,8 +51,6 @@ private let boundingImageSize = CGSize(width: 30.0, height: 30.0) private let highlightSize = CGSize(width: 35.0, height: 35.0) private let verticalOffset: CGFloat = 3.0 + UIScreenPixel -private let highlightBackground = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(0x9099A2, 0.2)) - final class ChatMediaInputRecentStickerPacksItemNode: ListViewItemNode { private let imageNode: ASImageNode private let highlightNode: ASImageNode @@ -68,10 +58,11 @@ final class ChatMediaInputRecentStickerPacksItemNode: ListViewItemNode { var currentCollectionId: ItemCollectionId? var inputNodeInteraction: ChatMediaInputNodeInteraction? + var theme: PresentationTheme? + init() { self.highlightNode = ASImageNode() self.highlightNode.isLayerBacked = true - self.highlightNode.image = highlightBackground self.highlightNode.isHidden = true self.imageNode = ASImageNode() @@ -79,7 +70,6 @@ final class ChatMediaInputRecentStickerPacksItemNode: ListViewItemNode { self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) - self.imageNode.image = iconImage self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) super.init(layerBacked: false, dynamicBounce: false) @@ -96,9 +86,13 @@ final class ChatMediaInputRecentStickerPacksItemNode: ListViewItemNode { deinit { } - func updateStickerPackItem(account: Account, item: StickerPackItem?, collectionId: ItemCollectionId) { - self.currentCollectionId = collectionId - self.updateIsHighlighted() + func updateTheme(theme: PresentationTheme) { + if self.theme !== theme { + self.theme = theme + + self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) + self.imageNode.image = PresentationResourcesChat.chatInputMediaPanelRecentStickersIcon(theme) + } } func updateIsHighlighted() { diff --git a/TelegramUI/ChatMediaInputStickerGridItem.swift b/TelegramUI/ChatMediaInputStickerGridItem.swift index 089f67db9c..42e908d825 100644 --- a/TelegramUI/ChatMediaInputStickerGridItem.swift +++ b/TelegramUI/ChatMediaInputStickerGridItem.swift @@ -8,27 +8,29 @@ import Postbox final class ChatMediaInputStickerGridSection: GridSection { let collectionId: ItemCollectionId let collectionInfo: StickerPackCollectionInfo? + let theme: PresentationTheme let height: CGFloat = 26.0 var hashValue: Int { return self.collectionId.hashValue } - init(collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo?) { + init(collectionId: ItemCollectionId, collectionInfo: StickerPackCollectionInfo?, theme: PresentationTheme) { self.collectionId = collectionId self.collectionInfo = collectionInfo + self.theme = theme } func isEqual(to: GridSection) -> Bool { if let to = to as? ChatMediaInputStickerGridSection { - return self.collectionId == to.collectionId + return self.collectionId == to.collectionId && self.theme === to.theme } else { return false } } func node() -> ASDisplayNode { - return ChatMediaInputStickerGridSectionNode(collectionInfo: self.collectionInfo) + return ChatMediaInputStickerGridSectionNode(collectionInfo: self.collectionInfo, theme: self.theme) } } @@ -37,14 +39,14 @@ private let sectionTitleFont = Font.medium(12.0) final class ChatMediaInputStickerGridSectionNode: ASDisplayNode { let titleNode: ASTextNode - init(collectionInfo: StickerPackCollectionInfo?) { + init(collectionInfo: StickerPackCollectionInfo?, theme: PresentationTheme) { self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true super.init() self.addSubnode(self.titleNode) - self.titleNode.attributedText = NSAttributedString(string: collectionInfo?.title.uppercased() ?? "", font: sectionTitleFont, textColor: UIColor(0x9099A2)) + self.titleNode.attributedText = NSAttributedString(string: collectionInfo?.title.uppercased() ?? "", font: sectionTitleFont, textColor: theme.chat.inputMediaPanel.stickersSectionTextColor) self.titleNode.maximumNumberOfLines = 1 self.titleNode.truncationMode = .byTruncatingTail } @@ -69,14 +71,14 @@ final class ChatMediaInputStickerGridItem: GridItem { let section: GridSection? - init(account: Account, collectionId: ItemCollectionId, stickerPackInfo: StickerPackCollectionInfo?, index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, interfaceInteraction: ChatControllerInteraction?, inputNodeInteraction: ChatMediaInputNodeInteraction, selected: @escaping () -> Void) { + init(account: Account, collectionId: ItemCollectionId, stickerPackInfo: StickerPackCollectionInfo?, index: ItemCollectionViewEntryIndex, stickerItem: StickerPackItem, interfaceInteraction: ChatControllerInteraction?, inputNodeInteraction: ChatMediaInputNodeInteraction, theme: PresentationTheme, selected: @escaping () -> Void) { self.account = account self.index = index self.stickerItem = stickerItem self.interfaceInteraction = interfaceInteraction self.inputNodeInteraction = inputNodeInteraction self.selected = selected - self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo) + self.section = ChatMediaInputStickerGridSection(collectionId: collectionId, collectionInfo: stickerPackInfo, theme: theme) } func node(layout: GridNodeLayout) -> GridItemNode { diff --git a/TelegramUI/ChatMediaInputStickerPackItem.swift b/TelegramUI/ChatMediaInputStickerPackItem.swift index 1db0278e1a..ab2ef367be 100644 --- a/TelegramUI/ChatMediaInputStickerPackItem.swift +++ b/TelegramUI/ChatMediaInputStickerPackItem.swift @@ -12,18 +12,20 @@ final class ChatMediaInputStickerPackItem: ListViewItem { let stickerPackItem: StickerPackItem? let selectedItem: () -> Void let index: Int + let theme: PresentationTheme var selectable: Bool { return true } - init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, stickerPackItem: StickerPackItem?, index: Int, selected: @escaping () -> Void) { + init(account: Account, inputNodeInteraction: ChatMediaInputNodeInteraction, collectionId: ItemCollectionId, stickerPackItem: StickerPackItem?, index: Int, theme: PresentationTheme, selected: @escaping () -> Void) { self.account = account self.inputNodeInteraction = inputNodeInteraction self.collectionId = collectionId self.stickerPackItem = stickerPackItem self.selectedItem = selected self.index = index + self.theme = theme } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { @@ -32,7 +34,7 @@ final class ChatMediaInputStickerPackItem: ListViewItem { node.contentSize = CGSize(width: 41.0, height: 41.0) node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) node.inputNodeInteraction = self.inputNodeInteraction - node.updateStickerPackItem(account: self.account, item: self.stickerPackItem, collectionId: self.collectionId) + node.updateStickerPackItem(account: self.account, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme) completion(node, { return (nil, {}) }) @@ -41,6 +43,7 @@ final class ChatMediaInputStickerPackItem: ListViewItem { public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + (node as? ChatMediaInputStickerPackItemNode)?.updateStickerPackItem(account: self.account, item: self.stickerPackItem, collectionId: self.collectionId, theme: self.theme) }) } @@ -54,8 +57,6 @@ private let boundingImageSize = CGSize(width: 30.0, height: 30.0) private let highlightSize = CGSize(width: 35.0, height: 35.0) private let verticalOffset: CGFloat = 3.0 -private let highlightBackground = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(0x9099A2, 0.2)) - final class ChatMediaInputStickerPackItemNode: ListViewItemNode { private let imageNode: TransformImageNode private let highlightNode: ASImageNode @@ -63,13 +64,13 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { var inputNodeInteraction: ChatMediaInputNodeInteraction? var currentCollectionId: ItemCollectionId? private var currentItem: StickerPackItem? + private var theme: PresentationTheme? private let stickerFetchedDisposable = MetaDisposable() init() { self.highlightNode = ASImageNode() self.highlightNode.isLayerBacked = true - self.highlightNode.image = highlightBackground self.highlightNode.isHidden = true self.imageNode = TransformImageNode() @@ -77,7 +78,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { self.highlightNode.frame = CGRect(origin: CGPoint(x: floor((boundingSize.width - highlightSize.width) / 2.0) + verticalOffset, y: floor((boundingSize.height - highlightSize.height) / 2.0)), size: highlightSize) - self.imageNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.imageNode.alphaTransitionOnFirstUpdate = true super.init(layerBacked: false, dynamicBounce: false) @@ -90,9 +91,15 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { self.stickerFetchedDisposable.dispose() } - func updateStickerPackItem(account: Account, item: StickerPackItem?, collectionId: ItemCollectionId) { + func updateStickerPackItem(account: Account, item: StickerPackItem?, collectionId: ItemCollectionId, theme: PresentationTheme) { self.currentCollectionId = collectionId + if self.theme !== theme { + self.theme = theme + + self.highlightNode.image = PresentationResourcesChat.chatMediaInputPanelHighlightedIconImage(theme) + } + if self.currentItem != item { self.currentItem = item diff --git a/TelegramUI/ChatMediaInputStickerPane.swift b/TelegramUI/ChatMediaInputStickerPane.swift index 95cd0123a9..3e91c1fe8b 100644 --- a/TelegramUI/ChatMediaInputStickerPane.swift +++ b/TelegramUI/ChatMediaInputStickerPane.swift @@ -17,8 +17,8 @@ final class ChatMediaInputStickerPane: ASDisplayNode { } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { - self.gridNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), lineSpacing: 0.0)), transition: .immediate), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0))), transition: .immediate), stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.gridNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) } } diff --git a/TelegramUI/ChatMediaInputTrendingItem.swift b/TelegramUI/ChatMediaInputTrendingItem.swift index 1040220b20..c1bcb5f7b8 100644 --- a/TelegramUI/ChatMediaInputTrendingItem.swift +++ b/TelegramUI/ChatMediaInputTrendingItem.swift @@ -7,12 +7,12 @@ import Postbox private let iconImage = generateImage(CGSize(width: 26.0, height: 26.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x9099A2).cgColor) + context.setStrokeColor(UIColor(rgb: 0x9099A2).cgColor) context.setLineWidth(2.0) context.setLineCap(.round) let diameter: CGFloat = 22.0 context.strokeEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - diameter) / 2.0), y: floor((size.width - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) - context.setFillColor(UIColor(0x9099A2).cgColor) + context.setFillColor(UIColor(rgb: 0x9099A2).cgColor) UIGraphicsPushContext(context) context.setTextDrawingMode(.stroke) @@ -61,7 +61,7 @@ private let boundingImageSize = CGSize(width: 30.0, height: 30.0) private let highlightSize = CGSize(width: 35.0, height: 35.0) private let verticalOffset: CGFloat = 3.0 + UIScreenPixel -private let highlightBackground = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(0x9099A2, 0.2)) +private let highlightBackground = generateStretchableFilledCircleImage(radius: 9.0, color: UIColor(rgb: 0x9099A2, alpha: 0.2)) final class ChatMediaInputTrendingItemNode: ListViewItemNode { private let imageNode: ASImageNode diff --git a/TelegramUI/ChatMessageActionButtonsNode.swift b/TelegramUI/ChatMessageActionButtonsNode.swift index ab76fb0b5e..31e096409e 100644 --- a/TelegramUI/ChatMessageActionButtonsNode.swift +++ b/TelegramUI/ChatMessageActionButtonsNode.swift @@ -4,10 +4,6 @@ import TelegramCore import Display private let titleFont = Font.medium(16.0) -private let middleImage = messageBubbleActionButtonImage(color: UIColor(0x596E89), position: .middle) -private let bottomLeftImage = messageBubbleActionButtonImage(color: UIColor(0x596E89), position: .bottomLeft) -private let bottomRightImage = messageBubbleActionButtonImage(color: UIColor(0x596E89), position: .bottomRight) -private let bottomSingleImage = messageBubbleActionButtonImage(color: UIColor(0x596E89), position: .bottomSingle) private final class ChatMessageActionButtonNode: ASDisplayNode { private let backgroundNode: ASImageNode @@ -56,24 +52,24 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { } } - class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) { + class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ theme: PresentationTheme, _ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) { let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode) - return { button, constrainedWidth, position in + return { theme, button, constrainedWidth, position in let sideInset: CGFloat = 8.0 let minimumSideInset: CGFloat = 4.0 - let (titleSize, titleApply) = titleLayout(NSAttributedString(string: button.title, font: titleFont, textColor: .white), nil, 1, .end, CGSize(width: max(1.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleSize, titleApply) = titleLayout(NSAttributedString(string: button.title, font: titleFont, textColor: theme.chat.bubble.actionButtonsTextColor), nil, 1, .end, CGSize(width: max(1.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let backgroundImage: UIImage + let backgroundImage: UIImage? switch position { case .middle: - backgroundImage = middleImage + backgroundImage = PresentationResourcesChat.chatBubbleActionButtonMiddleImage(theme) case .bottomLeft: - backgroundImage = bottomLeftImage + backgroundImage = PresentationResourcesChat.chatBubbleActionButtonBottomLeftImage(theme) case .bottomRight: - backgroundImage = bottomRightImage + backgroundImage = PresentationResourcesChat.chatBubbleActionButtonBottomRightImage(theme) case .bottomSingle: - backgroundImage = bottomSingleImage + backgroundImage = PresentationResourcesChat.chatBubbleActionButtonBottomSingleImage(theme) } return (titleSize.size.width + sideInset + sideInset, { width in @@ -125,10 +121,10 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { //(_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (maxWidth: CGFloat, layout: (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) - class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ replyMarkup: ReplyMarkupMessageAttribute, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)) { + class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ theme: PresentationTheme, _ replyMarkup: ReplyMarkupMessageAttribute, _ constrainedWidth: CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)) { let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? [] - return { replyMarkup, constrainedWidth in + return { theme, replyMarkup, constrainedWidth in let buttonHeight: CGFloat = 42.0 let buttonSpacing: CGFloat = 4.0 @@ -161,9 +157,9 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) if buttonIndex < currentButtonLayouts.count { - prepareButtonLayout = currentButtonLayouts[buttonIndex](button, maximumButtonWidth, buttonPosition) + prepareButtonLayout = currentButtonLayouts[buttonIndex](theme, button, maximumButtonWidth, buttonPosition) } else { - prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(button, maximumButtonWidth, buttonPosition) + prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(theme, button, maximumButtonWidth, buttonPosition) } maximumRowButtonWidth = max(maximumRowButtonWidth, prepareButtonLayout.minimumWidth) diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 18e5839eb3..06246038a9 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -5,45 +5,281 @@ import SwiftSignalKit import Postbox import TelegramCore -private func backgroundImage(color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context -> Void in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0x748391, 0.45).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - })?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8) +private let titleFont = Font.regular(13.0) +private let titleBoldFont = Font.bold(13.0) + +func serviceMessageString(theme: PresentationTheme, strings: PresentationStrings, message: Message, accountPeerId: PeerId) -> NSAttributedString? { + var attributedString: NSAttributedString? + + let theme = theme.chat.serviceMessage + + let bodyAttributes = MarkdownAttributeSet(font: titleFont, textColor: theme.serviceMessagePrimaryTextColor, additionalAttributes: [:]) + let linkAttributes = MarkdownAttributeSet(font: titleBoldFont, textColor: theme.serviceMessagePrimaryTextColor, additionalAttributes: [:]) + + for media in message.media { + if let action = media as? TelegramMediaAction { + let authorName = message.author?.displayTitle ?? "" + + var isChannel = false + if message.id.peerId.namespace == Namespaces.Peer.CloudChannel, let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + isChannel = true + } + + switch action.action { + case .groupCreated: + if isChannel { + attributedString = NSAttributedString(string: strings.Notification_CreatedChannel, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } else { + attributedString = NSAttributedString(string: strings.Notification_CreatedGroup, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } + case let .addedMembers(peerIds): + if peerIds.first == message.author?.id { + attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedChat(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + } else { + attributedString = addAttributesToStringWithRanges(strings.Notification_Invited(authorName, peerDisplayTitles(peerIds, message.peers)), body: bodyAttributes, argumentAttributes: [0: linkAttributes, 1: linkAttributes]) + } + case let .removedMembers(peerIds): + if peerIds.first == message.author?.id { + attributedString = addAttributesToStringWithRanges(strings.Notification_LeftChat(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + } else { + attributedString = addAttributesToStringWithRanges(strings.Notification_Kicked(authorName, peerDisplayTitles(peerIds, message.peers)), body: bodyAttributes, argumentAttributes: [0: linkAttributes, 1: linkAttributes]) + } + case let .photoUpdated(image): + if authorName.isEmpty || isChannel { + if isChannel { + if image != nil { + attributedString = NSAttributedString(string: strings.Channel_MessagePhotoUpdated, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } else { + attributedString = NSAttributedString(string: strings.Channel_MessagePhotoRemoved, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } + } else { + if image != nil { + attributedString = NSAttributedString(string: strings.Group_MessagePhotoUpdated, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } else { + attributedString = NSAttributedString(string: strings.Group_MessagePhotoRemoved, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } + } + } else { + if image != nil { + attributedString = addAttributesToStringWithRanges(strings.Notification_ChangedGroupPhoto(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + } else { + attributedString = addAttributesToStringWithRanges(strings.Notification_RemovedGroupPhoto(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + } + } + case let .titleUpdated(title): + if authorName.isEmpty || isChannel { + if isChannel { + attributedString = NSAttributedString(string: strings.Channel_MessageTitleUpdated(title).0, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } else { + attributedString = NSAttributedString(string: strings.Group_MessageTitleUpdated(title).0, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } + } else { + attributedString = addAttributesToStringWithRanges(strings.Notification_ChangedGroupName(authorName, title), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + } + case .pinnedMessageUpdated: + enum PinnnedMediaType { + case text(String) + case photo + case video + case round + case audio + case file + case gif + case sticker + case location + case contact + case deleted + } + + var pinnedMessage: Message? + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { + pinnedMessage = message + } + } + + var type: PinnnedMediaType + if let pinnedMessage = pinnedMessage { + type = .text(pinnedMessage.text) + inner: for media in pinnedMessage.media { + if let _ = media as? TelegramMediaImage { + type = .photo + } else if let file = media as? TelegramMediaFile { + type = .file + if file.isAnimated { + type = .gif + } else { + for attribute in file.attributes { + switch attribute { + case let .Video(_, _, flags): + if flags.contains(.instantRoundVideo) { + type = .round + } else { + type = .video + } + break inner + case let .Audio(isVoice, _, performer, title, _): + if isVoice { + type = .audio + } else { + let descriptionString: String + if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { + descriptionString = title + " — " + performer + } else if let title = title, !title.isEmpty { + descriptionString = title + } else if let performer = performer, !performer.isEmpty { + descriptionString = performer + } else if let fileName = file.fileName { + descriptionString = fileName + } else { + descriptionString = strings.Message_Audio + } + type = .text(descriptionString) + } + break inner + case .Sticker: + type = .sticker + break inner + case .Animated: + break + default: + break + } + } + } + } else if let _ = media as? TelegramMediaMap { + type = .location + } else if let _ = media as? TelegramMediaContact { + type = .contact + } + } + } else { + type = .deleted + } + + switch type { + case let .text(text): + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedTextMessage(authorName, text), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case .photo: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedPhotoMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case .video: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedVideoMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case .round: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedRoundMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case .audio: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedAudioMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case .file: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedDocumentMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case .gif: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedAnimationMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case .sticker: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedStickerMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case .location: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedLocationMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case .contact: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedContactMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case .deleted: + attributedString = addAttributesToStringWithRanges(strings.Notification_PinnedDeletedMessage(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + } + case .joinedByLink: + attributedString = addAttributesToStringWithRanges(strings.Notification_JoinedGroupByLink(authorName), body: bodyAttributes, argumentAttributes: [0: linkAttributes]) + case .channelMigratedFromGroup, .groupMigratedToChannel: + attributedString = NSAttributedString(string: strings.Notification_ChannelMigratedFrom, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + case let .messageAutoremoveTimeoutUpdated(timeout): + if timeout > 0 { + let timeValue = timeIntervalString(strings: strings, value: timeout) + + let string: String + if message.author?.id == accountPeerId { + string = strings.Notification_MessageLifetimeChangedOutgoing(timeValue).0 + } else { + let authorString: String + if let author = messageMainPeer(message) { + authorString = author.compactDisplayTitle + } else { + authorString = "" + } + string = strings.Notification_MessageLifetimeChanged(authorString, timeValue).0 + } + attributedString = NSAttributedString(string: string, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } else { + let string: String + if message.author?.id == accountPeerId { + string = strings.Notification_MessageLifetimeRemovedOutgoing + } else { + let authorString: String + if let author = messageMainPeer(message) { + authorString = author.compactDisplayTitle + } else { + authorString = "" + } + string = strings.Notification_MessageLifetimeRemoved(authorString).0 + } + attributedString = NSAttributedString(string: string, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + } + case .historyCleared: + break + case .historyScreenshot: + attributedString = NSAttributedString(string: strings.Notification_SecretChatScreenshot, font: titleFont, textColor: theme.serviceMessagePrimaryTextColor) + case let .gameScore(gameId: _, score): + var gameTitle: String? + inner: for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { + for media in message.media { + if let game = media as? TelegramMediaGame { + gameTitle = game.title + break inner + } + } + } + } + + var baseString: String + if message.author?.id == accountPeerId { + if let _ = gameTitle { + baseString = strings.ServiceMessage_GameScoreSelfExtended(score) + } else { + baseString = strings.ServiceMessage_GameScoreSelfSimple(score) + } + } else { + if let _ = gameTitle { + baseString = strings.ServiceMessage_GameScoreExtended(score) + } else { + baseString = strings.ServiceMessage_GameScoreSimple(score) + } + } + let baseStringValue = baseString as NSString + var ranges: [(Int, NSRange)] = [] + if baseStringValue.range(of: "{name}").location != NSNotFound { + ranges.append((0, baseStringValue.range(of: "{name}"))) + } + if baseStringValue.range(of: "{game}").location != NSNotFound { + ranges.append((1, baseStringValue.range(of: "{game}"))) + } + ranges.sort(by: { $0.1.location < $1.1.location }) + + attributedString = addAttributesToStringWithRanges(formatWithArgumentRanges(baseString, ranges, [authorName, gameTitle ?? ""]), body: bodyAttributes, argumentAttributes: [0: linkAttributes, 1: linkAttributes]) + case .phoneCall: + break + default: + attributedString = nil + } + + break + } + } + + return attributedString } -private let titleFont = UIFont.systemFont(ofSize: 13.0) - -private let timeoutValues: [(Int32, String)] = [ - (1, "1 second"), - (2, "2 seconds"), - (3, "3 seconds"), - (4, "4 seconds"), - (5, "5 seconds"), - (6, "6 seconds"), - (7, "7 seconds"), - (8, "8 seconds"), - (9, "9 seconds"), - (10, "10 seconds"), - (11, "11 seconds"), - (12, "12 seconds"), - (13, "13 seconds"), - (14, "14 seconds"), - (15, "15 seconds"), - (30, "30 seconds"), - (1 * 60, "1 minute"), - (1 * 60 * 60, "1 hour"), - (24 * 60 * 60, "1 day"), - (7 * 24 * 60 * 60, "1 week"), -] - class ChatMessageActionItemNode: ChatMessageItemView { let labelNode: TextNode let backgroundNode: ASImageNode private let fetchDisposable = MetaDisposable() + private var appliedItem: ChatMessageItem? + required init() { self.labelNode = TextNode() self.labelNode.isLayerBacked = true @@ -56,7 +292,6 @@ class ChatMessageActionItemNode: ChatMessageItemView { super.init(layerBacked: false) - self.backgroundNode.image = backgroundImage(color: UIColor(0x007ee5)) self.addSubnode(self.backgroundNode) self.addSubnode(self.labelNode) } @@ -77,98 +312,17 @@ class ChatMessageActionItemNode: ChatMessageItemView { let labelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants + let currentItem = self.appliedItem + return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in - var attributedString: NSAttributedString? + var updatedBackgroundImage: UIImage? - for media in item.message.media { - if let action = media as? TelegramMediaAction { - let authorName = item.message.author?.displayTitle ?? "" - switch action.action { - case .groupCreated: - attributedString = NSAttributedString(string: tr(.ChatServiceGroupCreated), font: titleFont, textColor: UIColor.white) - case let .addedMembers(peerIds): - if peerIds.first == item.message.author?.id { - attributedString = NSAttributedString(string: tr(.ChatServiceGroupAddedSelf(authorName)), font: titleFont, textColor: UIColor.white) - } else { - attributedString = NSAttributedString(string: tr(.ChatServiceGroupAddedMembers(authorName, peerDisplayTitles(peerIds, item.message.peers))), font: titleFont, textColor: UIColor.white) - } - case let .removedMembers(peerIds): - if peerIds.first == item.message.author?.id { - attributedString = NSAttributedString(string: tr(.ChatServiceGroupRemovedSelf(authorName)), font: titleFont, textColor: UIColor.white) - } else { - attributedString = NSAttributedString(string: tr(.ChatServiceGroupRemovedMembers(authorName, peerDisplayTitles(peerIds, item.message.peers))), font: titleFont, textColor: UIColor.white) - } - case let .photoUpdated(image): - if let _ = image { - attributedString = NSAttributedString(string: tr(.ChatServiceGroupUpdatedPhoto(authorName)), font: titleFont, textColor: UIColor.white) - } else { - attributedString = NSAttributedString(string: tr(.ChatServiceGroupRemovedPhoto(authorName)), font: titleFont, textColor: UIColor.white) - } - case let .titleUpdated(title): - attributedString = NSAttributedString(string: tr(.ChatServiceGroupUpdatedTitle(authorName, title)), font: titleFont, textColor: UIColor.white) - case .pinnedMessageUpdated: - var replyMessageText = "" - for attribute in item.message.attributes { - if let attribute = attribute as? ReplyMessageAttribute, let message = item.message.associatedMessages[attribute.messageId] { - replyMessageText = message.text - } - } - attributedString = NSAttributedString(string: tr(.ChatServiceGroupUpdatedPinnedMessage(authorName, replyMessageText)), font: titleFont, textColor: UIColor.white) - case .joinedByLink: - attributedString = NSAttributedString(string: tr(.ChatServiceGroupJoinedByLink(authorName)), font: titleFont, textColor: UIColor.white) - case .channelMigratedFromGroup, .groupMigratedToChannel: - attributedString = NSAttributedString(string: tr(.ChatServiceGroupMigratedToSupergroup), font: titleFont, textColor: UIColor.white) - case let .messageAutoremoveTimeoutUpdated(timeout): - /* - "Notification.MessageLifetimeChanged" = "%1$@ set the self-destruct timer to %2$@"; - "Notification.MessageLifetimeChangedOutgoing" = "You set the self-destruct timer to %1$@"; - "Notification.MessageLifetimeRemoved" = "%1$@ disabled the self-destruct timer"; - "Notification.MessageLifetimeRemovedOutgoing" = "You disabled the self-destruct timer"; - */ - if timeout > 0 { - var timeValue: String = "\(timeout) s" - for (value, text) in timeoutValues { - if value == timeout { - timeValue = text - } - } - - let string: String - if item.message.author?.id == item.account.peerId { - string = String(format: NSLocalizedString("Notification.MessageLifetimeChangedOutgoing", comment: ""), timeValue) - } else { - let authorString: String - if let author = messageMainPeer(item.message) { - authorString = author.compactDisplayTitle - } else { - authorString = "" - } - string = String(format: NSLocalizedString("Notification.MessageLifetimeChanged", comment: ""), authorString, timeValue) - } - attributedString = NSAttributedString(string: string, font: titleFont, textColor: UIColor.white) - } else { - let string: String - if item.message.author?.id == item.account.peerId { - string = NSLocalizedString("Notification.MessageLifetimeRemovedOutgoing", comment: "") - } else { - let authorString: String - if let author = messageMainPeer(item.message) { - authorString = author.compactDisplayTitle - } else { - authorString = "" - } - string = String(format: NSLocalizedString("Notification.MessageLifetimeRemoved", comment: ""), authorString) - } - attributedString = NSAttributedString(string: string, font: titleFont, textColor: UIColor.white) - } - default: - attributedString = nil - } - - break - } + if item.theme !== currentItem?.theme { + updatedBackgroundImage = PresentationResourcesChat.chatServiceBubbleFillImage(item.theme) } + let attributedString = serviceMessageString(theme: item.theme, strings: item.strings, message: item.message, accountPeerId: item.account.peerId) + let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0) @@ -179,6 +333,12 @@ class ChatMessageActionItemNode: ChatMessageItemView { return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 20.0), insets: layoutInsets), { [weak self] animation in if let strongSelf = self { + strongSelf.appliedItem = item + + if let updatedBackgroundImage = updatedBackgroundImage { + strongSelf.backgroundNode.image = updatedBackgroundImage + } + let _ = apply() strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize) diff --git a/TelegramUI/ChatMessageBackground.swift b/TelegramUI/ChatMessageBackground.swift index 405be527ab..5ca62162cd 100644 --- a/TelegramUI/ChatMessageBackground.swift +++ b/TelegramUI/ChatMessageBackground.swift @@ -40,25 +40,6 @@ enum ChatMessageBackgroundType: Equatable { } } -private let chatMessageBackgroundIncomingImage = messageBubbleImage(incoming: true, highlighted: false, neighbors: .none) -private let chatMessageBackgroundIncomingHighlightedImage = messageBubbleImage(incoming: true, highlighted: true, neighbors: .none) -private let chatMessageBackgroundIncomingMergedTopImage = messageBubbleImage(incoming: true, highlighted: false, neighbors: .top) -private let chatMessageBackgroundIncomingMergedTopHighlightedImage = messageBubbleImage(incoming: true, highlighted: true, neighbors: .top) -private let chatMessageBackgroundIncomingMergedBottomImage = messageBubbleImage(incoming: true, highlighted: false, neighbors: .bottom) -private let chatMessageBackgroundIncomingMergedBottomHighlightedImage = messageBubbleImage(incoming: true, highlighted: true, neighbors: .bottom) -private let chatMessageBackgroundIncomingMergedBothImage = messageBubbleImage(incoming: true, highlighted: false, neighbors: .both) -private let chatMessageBackgroundIncomingMergedBothHighlightedImage = messageBubbleImage(incoming: true, highlighted: true, neighbors: .both) - -private let chatMessageBackgroundOutgoingImage = messageBubbleImage(incoming: false, highlighted: false, neighbors: .none) -private let chatMessageBackgroundOutgoingHighlightedImage = messageBubbleImage(incoming: false, highlighted: true, neighbors: .none) -private let chatMessageBackgroundOutgoingMergedTopImage = messageBubbleImage(incoming: false, highlighted: false, neighbors: .top) -private let chatMessageBackgroundOutgoingMergedTopHighlightedImage = messageBubbleImage(incoming: false, highlighted: true, neighbors: .top) -private let chatMessageBackgroundOutgoingMergedBottomImage = messageBubbleImage(incoming: false, highlighted: false, neighbors: .bottom) -private let chatMessageBackgroundOutgoingMergedBottomHighlightedImage = messageBubbleImage(incoming: false, highlighted: true, neighbors: .bottom) -private let chatMessageBackgroundOutgoingMergedBothImage = messageBubbleImage(incoming: false, highlighted: false, neighbors: .both) -private let chatMessageBackgroundOutgoingMergedBothHighlightedImage = messageBubbleImage(incoming: false, highlighted: true, neighbors: .both) - - class ChatMessageBackground: ASImageNode { private var type: ChatMessageBackgroundType? private var currentHighlighted = false @@ -71,7 +52,7 @@ class ChatMessageBackground: ASImageNode { self.displayWithoutProcessing = true } - func setType(type: ChatMessageBackgroundType, highlighted: Bool) { + func setType(type: ChatMessageBackgroundType, highlighted: Bool, graphics: PrincipalThemeEssentialGraphics) { if let currentType = self.type, currentType == type, self.currentHighlighted == highlighted { return } @@ -83,24 +64,24 @@ class ChatMessageBackground: ASImageNode { case let .Incoming(mergeType): switch mergeType { case .None: - image = highlighted ? chatMessageBackgroundIncomingHighlightedImage : chatMessageBackgroundIncomingImage + image = highlighted ? graphics.chatMessageBackgroundIncomingHighlightedImage : graphics.chatMessageBackgroundIncomingImage case .Top: - image = highlighted ? chatMessageBackgroundIncomingMergedTopHighlightedImage : chatMessageBackgroundIncomingMergedTopImage + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedTopHighlightedImage : graphics.chatMessageBackgroundIncomingMergedTopImage case .Bottom: - image = highlighted ? chatMessageBackgroundIncomingMergedBottomHighlightedImage : chatMessageBackgroundIncomingMergedBottomImage + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedBottomHighlightedImage : graphics.chatMessageBackgroundIncomingMergedBottomImage case .Both: - image = highlighted ? chatMessageBackgroundIncomingMergedBothHighlightedImage : chatMessageBackgroundIncomingMergedBothImage + image = highlighted ? graphics.chatMessageBackgroundIncomingMergedBothHighlightedImage : graphics.chatMessageBackgroundIncomingMergedBothImage } case let .Outgoing(mergeType): switch mergeType { case .None: - image = highlighted ? chatMessageBackgroundOutgoingHighlightedImage : chatMessageBackgroundOutgoingImage + image = highlighted ? graphics.chatMessageBackgroundOutgoingHighlightedImage : graphics.chatMessageBackgroundOutgoingImage case .Top: - image = highlighted ? chatMessageBackgroundOutgoingMergedTopHighlightedImage : chatMessageBackgroundOutgoingMergedTopImage + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedTopHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedTopImage case .Bottom: - image = highlighted ? chatMessageBackgroundOutgoingMergedBottomHighlightedImage : chatMessageBackgroundOutgoingMergedBottomImage + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedBottomHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedBottomImage case .Both: - image = highlighted ? chatMessageBackgroundOutgoingMergedBothHighlightedImage : chatMessageBackgroundOutgoingMergedBothImage + image = highlighted ? graphics.chatMessageBackgroundOutgoingMergedBothHighlightedImage : graphics.chatMessageBackgroundOutgoingMergedBothImage } } self.image = image diff --git a/TelegramUI/ChatMessageBubbleContentNode.swift b/TelegramUI/ChatMessageBubbleContentNode.swift index d9dfdefc63..58b122a4c3 100644 --- a/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageBubbleContentNode.swift @@ -33,11 +33,13 @@ enum ChatMessageBubbleContentTapAction { case none case url(String) case textMention(String) - case peerMention(PeerId) + case peerMention(PeerId, String) case botCommand(String) case hashtag(String?, String) case instantPage case holdToPreviewSecretMedia + case call(PeerId) + case ignore } class ChatMessageBubbleContentNode: ASDisplayNode { @@ -80,4 +82,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode { func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { return .none } + + func updateTouchesAtPoint(_ point: CGPoint?) { + } } diff --git a/TelegramUI/ChatMessageBubbleImages.swift b/TelegramUI/ChatMessageBubbleImages.swift index 019e4b761c..c17cb15572 100644 --- a/TelegramUI/ChatMessageBubbleImages.swift +++ b/TelegramUI/ChatMessageBubbleImages.swift @@ -1,14 +1,6 @@ import Foundation import Display -private let incomingFillColor = UIColor(0xffffff) -private let incomingFillHighlightedColor = UIColor(0xd9f4ff) -private let incomingStrokeColor = UIColor(0x86A9C9, 0.5) - -private let outgoingFillColor = UIColor(0xE1FFC7) -private let outgoingFillHighlightedColor = UIColor(0xc8ffa6) -private let outgoingStrokeColor = UIColor(0x86A9C9, 0.5) - enum MessageBubbleImageNeighbors { case none case top @@ -16,21 +8,21 @@ enum MessageBubbleImageNeighbors { case both } -func messageSingleBubbleLikeImage(incoming: Bool, highlighted: Bool) -> UIImage { +func messageSingleBubbleLikeImage(fillColor: UIColor, strokeColor: UIColor) -> UIImage { let diameter: CGFloat = 36.0 return generateImage(CGSize(width: 36.0, height: diameter), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) let lineWidth: CGFloat = 0.5 - context.setFillColor((incoming ? (highlighted ? incomingStrokeColor : incomingStrokeColor) : (highlighted ? outgoingStrokeColor : outgoingStrokeColor)).cgColor) + context.setFillColor(strokeColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - context.setFillColor((incoming ? (highlighted ? incomingFillHighlightedColor : incomingFillColor) : (highlighted ? outgoingFillHighlightedColor : outgoingFillColor)).cgColor) + context.setFillColor(fillColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: lineWidth, y: lineWidth), size: CGSize(width: size.width - lineWidth * 2.0, height: size.height - lineWidth * 2.0))) })!.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) } -func messageBubbleImage(incoming: Bool, highlighted: Bool, neighbors: MessageBubbleImageNeighbors) -> UIImage { +func messageBubbleImage(incoming: Bool, fillColor: UIColor, strokeColor: UIColor, neighbors: MessageBubbleImageNeighbors) -> UIImage { let diameter: CGFloat = 36.0 let corner: CGFloat = 7.0 return generateImage(CGSize(width: 42.0, height: diameter), contextGenerator: { size, context in @@ -50,9 +42,9 @@ func messageBubbleImage(incoming: Bool, highlighted: Bool, neighbors: MessageBub let lineWidth: CGFloat = 1.0 - context.setFillColor((incoming ? (highlighted ? incomingFillHighlightedColor : incomingFillColor) : (highlighted ? outgoingFillHighlightedColor : outgoingFillColor)).cgColor) + context.setFillColor(fillColor.cgColor) context.setLineWidth(lineWidth) - context.setStrokeColor((incoming ? incomingStrokeColor : outgoingStrokeColor).cgColor) + context.setStrokeColor(strokeColor.cgColor) switch neighbors { case .none: @@ -131,16 +123,3 @@ func messageBubbleActionButtonImage(color: UIColor, position: MessageBubbleActio } })!.stretchableImage(withLeftCapWidth: Int(size.width / 2.0), topCapHeight: Int(size.height / 2.0)) } - -func generateInstantVideoBackground(incoming: Bool, highlighted: Bool = false) -> UIImage? { - return generateImage(CGSize(width: 212.0, height: 212.0), rotatedContext: { size, context in - let lineWidth: CGFloat = 0.5 - - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor((incoming ? (highlighted ? incomingStrokeColor : incomingStrokeColor) : (highlighted ? outgoingStrokeColor : outgoingStrokeColor)).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - context.setFillColor((incoming ? (highlighted ? incomingFillHighlightedColor : incomingFillColor) : (highlighted ? outgoingFillHighlightedColor : outgoingFillColor)).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: lineWidth, y: lineWidth), size: CGSize(width: size.width - lineWidth * 2.0, height: size.height - lineWidth * 2.0))) - }) -} diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 751a7e1866..6a00b2521e 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -15,6 +15,8 @@ private func contentNodeClassesForItem(_ item: ChatMessageItem) -> [AnyClass] { } else { result.append(ChatMessageFileBubbleContentNode.self) } + } else if let action = media as? TelegramMediaAction, case .phoneCall = action.action { + result.append(ChatMessageCallBubbleContentNode.self) } } @@ -46,17 +48,14 @@ private let inlineBotPrefixFont = Font.regular(14.0) private let inlineBotNameFont = nameFont private let chatMessagePeerIdColors: [UIColor] = [ - UIColor(0xfc5c51), - UIColor(0xfa790f), - UIColor(0x0fb297), - UIColor(0x3ca5ec), - UIColor(0x3d72ed), - UIColor(0x895dd5) + UIColor(rgb: 0xfc5c51), + UIColor(rgb: 0xfa790f), + UIColor(rgb: 0x0fb297), + UIColor(rgb: 0x3ca5ec), + UIColor(rgb: 0x3d72ed), + UIColor(rgb: 0x895dd5) ] -private let shareButtonBackgroundImage = generateFilledCircleImage(diameter: 29.0, color: UIColor(0x748391, 0.45)) -private let shareButtonImage = UIImage(bundleImageName: "Chat/Message/ShareIcon")?.precomposed() - class ChatMessageBubbleItemNode: ChatMessageItemView { private let backgroundNode: ChatMessageBackground private var transitionClippingNode: ASDisplayNode? @@ -79,6 +78,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private var backgroundFrameTransition: (CGRect, CGRect)? + private var appliedItem: ChatMessageItem? + override var visibility: ListViewItemNodeVisibility { didSet { if self.visibility != oldValue { @@ -165,7 +166,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { switch tapAction { case .none: break - case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage: + case .ignore: + return .fail + case .url, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .call: return .waitForSingleTap case .holdToPreviewSecretMedia: return .waitForHold(timeout: 0.12, acceptTap: false) @@ -175,6 +178,17 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { return .waitForDoubleTap } + recognizer.highlight = { [weak self] point in + if let strongSelf = self { + for contentNode in strongSelf.contentNodes { + var translatedPoint: CGPoint? + if let point = point { + translatedPoint = CGPoint(x: point.x - contentNode.frame.minX, y: point.y - contentNode.frame.minY) + } + contentNode.updateTouchesAtPoint(translatedPoint) + } + } + } self.view.addGestureRecognizer(recognizer) } @@ -193,13 +207,33 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let layoutConstants = self.layoutConstants + let currentItem = self.appliedItem + return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in let message = item.message let incoming = item.message.effectivelyIncoming let displayAuthorInfo = !mergedTop && incoming && item.peerId.isGroupOrChannel && item.message.author != nil - let avatarInset: CGFloat = (item.peerId.isGroupOrChannel && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0 + let avatarInset: CGFloat + var hasAvatar = false + + if item.peerId.isGroupOrChannel && item.message.author != nil { + var isBroadcastChannel = false + if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + isBroadcastChannel = true + } + + if !isBroadcastChannel { + hasAvatar = true + } + } + + if hasAvatar { + avatarInset = layoutConstants.avatarDiameter + } else { + avatarInset = 0.0 + } let tmpWidth = width * layoutConstants.bubble.maximumWidthFillFactor let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) @@ -302,9 +336,14 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var forwardInfoSizeApply: (CGSize, () -> ChatMessageForwardInfoNode?) = (CGSize(), { nil }) if displayHeader { - if let author = message.author, displayAuthorInfo { - authorNameString = author.displayTitle - authorNameColor = chatMessagePeerIdColors[Int(author.id.id % 6)] + if displayAuthorInfo { + if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + authorNameString = peer.displayTitle + authorNameColor = chatMessagePeerIdColors[Int(peer.id.id % 6)] + } else if let author = message.author { + authorNameString = author.displayTitle + authorNameColor = chatMessagePeerIdColors[Int(author.id.id % 6)] + } } if authorNameString != nil || inlineBotNameString != nil { @@ -312,7 +351,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { headerSize.height += 4.0 } - let inlineBotNameColor = incoming ? UIColor(0x007ee5) : UIColor(0x00a700) + let inlineBotNameColor = incoming ? item.theme.chat.bubble.incomingAccentColor : item.theme.chat.bubble.outgoingAccentColor let attributedString: NSAttributedString if let authorNameString = authorNameString, let authorNameColor = authorNameColor, let inlineBotNameString = inlineBotNameString { @@ -326,7 +365,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } else if let inlineBotNameString = inlineBotNameString { attributedString = NSAttributedString(string: "via @\(inlineBotNameString)", font: inlineBotNameFont, textColor: inlineBotNameColor) } else { - attributedString = NSAttributedString(string: "", font: nameFont, textColor: UIColor.black) + attributedString = NSAttributedString(string: "", font: nameFont, textColor: inlineBotNameColor) } let sizeAndApply = authorNameLayout(attributedString, nil, 1, .end, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) @@ -342,7 +381,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if headerSize.height < CGFloat.ulpOfOne { headerSize.height += 4.0 } - let sizeAndApply = forwardInfoLayout(incoming, forwardInfo.source == nil ? forwardInfo.author : forwardInfo.source!, forwardInfo.source == nil ? nil : forwardInfo.author, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude)) + let sizeAndApply = forwardInfoLayout(item.theme, incoming, forwardInfo.source == nil ? forwardInfo.author : forwardInfo.source!, forwardInfo.source == nil ? nil : forwardInfo.author, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude)) forwardInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() }) forwardInfoOriginY = headerSize.height @@ -356,7 +395,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } else { headerSize.height += 2.0 } - let sizeAndApply = replyInfoLayout(item.account, .bubble(incoming: incoming), replyMessage, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude)) + let sizeAndApply = replyInfoLayout(item.theme, item.account, .bubble(incoming: incoming), replyMessage, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude)) replyInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() }) replyInfoOriginY = headerSize.height @@ -396,7 +435,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var actionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode))? if let replyMarkup = replyMarkup { - let (minWidth, buttonsLayout) = actionButtonsLayout(replyMarkup, maximumNodeWidth) + let (minWidth, buttonsLayout) = actionButtonsLayout(item.theme, replyMarkup, maximumNodeWidth) maxContentWidth = max(maxContentWidth, minWidth) actionButtonsFinalize = buttonsLayout } @@ -447,22 +486,30 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } + var updatedShareButtonBackground: UIImage? + var updatedShareButtonNode: HighlightableButtonNode? if needShareButton { if currentShareButtonNode != nil { updatedShareButtonNode = currentShareButtonNode + if item.theme !== currentItem?.theme { + updatedShareButtonBackground = PresentationResourcesChat.chatBubbleShareButtonImage(item.theme) + } } else { let buttonNode = HighlightableButtonNode() - buttonNode.setBackgroundImage(shareButtonBackgroundImage, for: [.normal]) - buttonNode.setImage(shareButtonImage, for: [.normal]) + buttonNode.setBackgroundImage(PresentationResourcesChat.chatBubbleShareButtonImage(item.theme), for: [.normal]) updatedShareButtonNode = buttonNode } } let layout = ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets) + let graphics = PresentationResourcesChat.principalGraphics(item.theme) + return (layout, { [weak self] animation in if let strongSelf = self { + strongSelf.appliedItem = item + strongSelf.messageId = message.id strongSelf.messageStableId = message.stableId @@ -473,7 +520,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } else { backgroundType = .Incoming(mergeType) } - strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState) + strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics) strongSelf.backgroundType = backgroundType @@ -599,6 +646,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { strongSelf.addSubnode(updatedShareButtonNode) updatedShareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside) } + if let updatedShareButtonBackground = updatedShareButtonBackground { + strongSelf.shareButtonNode?.setBackgroundImage(updatedShareButtonBackground, for: [.normal]) + } } else if let shareButtonNode = strongSelf.shareButtonNode { shareButtonNode.removeFromSupernode() strongSelf.shareButtonNode = nil @@ -797,7 +847,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { loop: for contentNode in self.contentNodes { let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY)) switch tapAction { - case .none: + case .none, .ignore: break case let .url(url): foundTapAction = true @@ -805,7 +855,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { controllerInteraction.openUrl(url) } break loop - case let .peerMention(peerId): + case let .peerMention(peerId, _): foundTapAction = true if let controllerInteraction = self.controllerInteraction { controllerInteraction.openPeer(peerId, .chat(textInputState: nil), nil) @@ -837,7 +887,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { break loop case .holdToPreviewSecretMedia: foundTapAction = true - break + case let .call(peerId): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.callPeer(peerId) + } + break loop } } if !foundTapAction { @@ -845,7 +900,53 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } case .longTap, .doubleTap: if let item = self.item, self.backgroundNode.frame.contains(location) { - self.controllerInteraction?.openMessageContextMenu(item.message.id, self, self.backgroundNode.frame) + var foundTapAction = false + loop: for contentNode in self.contentNodes { + let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY)) + switch tapAction { + case .none, .ignore: + break + case let .url(url): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.longTap(.url(url)) + } + break loop + case let .peerMention(peerId, mention): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.longTap(.mention(mention)) + } + break loop + case let .textMention(name): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.longTap(.mention(name)) + } + break loop + case let .botCommand(command): + foundTapAction = true + if let item = self.item, let controllerInteraction = self.controllerInteraction { + controllerInteraction.longTap(.command(command)) + } + break loop + case let .hashtag(_, hashtag): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.longTap(.hashtag(hashtag)) + } + break loop + case .instantPage: + break + case .holdToPreviewSecretMedia: + break + case let .call(peerId): + break + } + } + if !foundTapAction { + self.controllerInteraction?.openMessageContextMenu(item.message.id, self, self.backgroundNode.frame) + } } case .hold: if let item = self.item, item.message.containsSecretMedia { @@ -970,7 +1071,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } override func updateHighlightedState(animated: Bool) { - if let controllerInteraction = self.controllerInteraction { + if let controllerInteraction = self.controllerInteraction, let item = self.item { var highlighted = false if let messageStableId = self.messageStableId, let highlightedState = controllerInteraction.highlightedState { if highlightedState.messageStableId == messageStableId { @@ -981,17 +1082,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if self.highlightedState != highlighted { self.highlightedState = highlighted if let backgroundType = self.backgroundType { + let graphics = PresentationResourcesChat.principalGraphics(item.theme) + if highlighted { - self.backgroundNode.setType(type: backgroundType, highlighted: true) + self.backgroundNode.setType(type: backgroundType, highlighted: true, graphics: graphics) } else { if let previousContents = self.backgroundNode.layer.contents, animated { - self.backgroundNode.setType(type: backgroundType, highlighted: false) + self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics) if let updatedContents = self.backgroundNode.layer.contents { self.backgroundNode.layer.animate(from: previousContents as AnyObject, to: updatedContents as AnyObject, keyPath: "contents", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: 0.42) } } else { - self.backgroundNode.setType(type: backgroundType, highlighted: false) + self.backgroundNode.setType(type: backgroundType, highlighted: false, graphics: graphics) } } } diff --git a/TelegramUI/ChatMessageCallBubbleContentNode.swift b/TelegramUI/ChatMessageCallBubbleContentNode.swift new file mode 100644 index 0000000000..be453909e9 --- /dev/null +++ b/TelegramUI/ChatMessageCallBubbleContentNode.swift @@ -0,0 +1,222 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox + +private let titleFont: UIFont = Font.medium(16.0) +private let labelFont: UIFont = Font.regular(13.0) + +private let incomingGreenIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallIncomingArrow"), color: UIColor(rgb: 0x36c033)) +private let incomingRedIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallIncomingArrow"), color: UIColor(rgb: 0xff4747)) + +private let outgoingGreenIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallOutgoingArrow"), color: UIColor(rgb: 0x36c033)) +private let outgoingRedIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/CallOutgoingArrow"), color: UIColor(rgb: 0xff4747)) + +class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode { + private let titleNode: TextNode + private let labelNode: TextNode + private let iconNode: ASImageNode + private let buttonNode: HighlightableButtonNode + + private var item: ChatMessageItem? + + required init() { + self.titleNode = TextNode() + self.labelNode = TextNode() + + self.iconNode = ASImageNode() + self.iconNode.displayWithoutProcessing = true + self.iconNode.displaysAsynchronously = false + self.iconNode.isLayerBacked = true + + self.buttonNode = HighlightableButtonNode() + + super.init() + + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .topLeft + self.titleNode.contentsScale = UIScreenScale + self.titleNode.displaysAsynchronously = true + self.addSubnode(self.titleNode) + + self.labelNode.isLayerBacked = true + self.labelNode.contentMode = .topLeft + self.labelNode.contentsScale = UIScreenScale + self.labelNode.displaysAsynchronously = true + self.addSubnode(self.labelNode) + + self.addSubnode(self.iconNode) + + self.addSubnode(self.buttonNode) + self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + + return { item, layoutConstants, position, _ in + return (CGFloat.greatestFiniteMagnitude, { constrainedSize in + let message = item.message + + let incoming = item.message.effectivelyIncoming + + let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height) + + let bubbleTheme = item.theme.chat.bubble + + var titleString: String + if message.flags.contains(.Incoming) { + titleString = item.strings.Notification_CallIncoming + } else { + titleString = item.strings.Notification_CallOutgoing + } + + var callDuration: Int32? + var callSuccessful = true + for media in item.message.media { + if let action = media as? TelegramMediaAction, case let .phoneCall(_, discardReason, duration) = action.action { + callDuration = duration + if let discardReason = discardReason { + switch discardReason { + case .busy, .disconnect: + callSuccessful = false + titleString = item.strings.Notification_CallCanceled + case .missed: + callSuccessful = false + titleString = item.strings.Notification_CallMissed + case .hangup: + break + } + } + break + } + } + + var attributedTitle: NSAttributedString? + if message.flags.contains(.Incoming) { + attributedTitle = NSAttributedString(string: titleString, font: titleFont, textColor: bubbleTheme.incomingPrimaryTextColor) + } else { + attributedTitle = NSAttributedString(string: titleString, font: titleFont, textColor: bubbleTheme.outgoingPrimaryTextColor) + } + + var callIcon: UIImage? + if callSuccessful { + if incoming { + callIcon = incomingGreenIcon + } else { + callIcon = outgoingGreenIcon + } + } else { + if incoming { + callIcon = incomingRedIcon + } else { + callIcon = outgoingRedIcon + } + } + + var buttonImage: UIImage? + if incoming { + buttonImage = PresentationResourcesChat.chatBubbleIncomingCallButtonImage(item.theme) + } else { + buttonImage = PresentationResourcesChat.chatBubbleOutgoingCallButtonImage(item.theme) + } + + var t = Int(item.message.timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo) + var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + + if let duration = callDuration, duration != 0 { + if duration >= 60 { + dateText += ", " + item.strings.Call_Minutes(duration / 60) + } else { + dateText += ", " + item.strings.Call_Seconds(duration) + } + } + + var attributedLabel: NSAttributedString? + attributedLabel = NSAttributedString(string: dateText, font: labelFont, textColor: message.effectivelyIncoming ? bubbleTheme.incomingFileDurationColor : bubbleTheme.outgoingFileDurationColor) + + let (titleLayout, titleApply) = makeTitleLayout(attributedTitle, nil, 0, .end, textConstrainedSize, .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLabelLayout(attributedLabel, nil, 0, .end, textConstrainedSize, .natural, nil, UIEdgeInsets()) + + let titleSize = titleLayout.size + let labelSize = labelLayout.size + + var titleFrame = CGRect(origin: CGPoint(), size: titleSize) + var labelFrame = CGRect(origin: CGPoint(x: 14.0, y: 0.0), size: labelSize) + + titleFrame = titleFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top + 4.0) + labelFrame = labelFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top + titleSize.height + 4.0) + + var boundingSize: CGSize + boundingSize = CGSize(width: max(titleFrame.size.width, labelFrame.size.width + 14.0), height: 47.0) + boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom + + boundingSize.width += 54.0 + + return (boundingSize.width, { boundingWidth in + return (boundingSize, { [weak self] animation in + if let strongSelf = self { + strongSelf.item = item + + let _ = titleApply() + let _ = labelApply() + + strongSelf.titleNode.frame = titleFrame + strongSelf.labelNode.frame = labelFrame + + if let callIcon = callIcon { + if strongSelf.iconNode.image != callIcon { + strongSelf.iconNode.image = callIcon + } + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: titleFrame.minX + 1.0, y: labelFrame.minY + 4.0), size: callIcon.size) + } + + if let buttonImage = buttonImage { + strongSelf.buttonNode.setImage(buttonImage, for: []) + strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: boundingWidth - buttonImage.size.width - 8.0, y: 15.0), size: buttonImage.size) + } + } + }) + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateAdded(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + @objc func callButtonPressed() { + if let item = self.item { + item.controllerInteraction.callPeer(item.message.id.peerId) + } + } + + override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + if self.buttonNode.frame.contains(point) { + return .ignore + } else if self.bounds.contains(point), let item = self.item { + return .call(item.message.id.peerId) + } else { + return .none + } + } +} diff --git a/TelegramUI/ChatMessageDateAndStatusNode.swift b/TelegramUI/ChatMessageDateAndStatusNode.swift index 78b3c16af9..bf65ae8c1d 100644 --- a/TelegramUI/ChatMessageDateAndStatusNode.swift +++ b/TelegramUI/ChatMessageDateAndStatusNode.swift @@ -6,46 +6,6 @@ import SwiftSignalKit private let dateFont = UIFont.italicSystemFont(ofSize: 11.0) -private func generateCheckImage(partial: Bool, color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 11.0, height: 9.0), contextGenerator: { size, context in - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.scaleBy(x: 1.0, y: -1.0) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - - context.clear(CGRect(origin: CGPoint(), size: size)) - context.scaleBy(x: 0.5, y: 0.5) - context.setStrokeColor(color.cgColor) - context.setLineWidth(2.5) - if partial { - let _ = try? drawSvgPath(context, path: "M1,14.5 L2.5,16 L16.4985125,1 ") - } else { - let _ = try? drawSvgPath(context, path: "M1,10 L7,16 L20.9985125,1 ") - } - context.strokePath() - }) -} - -private func generateClockFrameImage(color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(color.cgColor) - context.setFillColor(color.cgColor) - let strokeWidth: CGFloat = 1.0 - context.setLineWidth(strokeWidth) - context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: size.width - strokeWidth, height: size.height - strokeWidth)) - context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: strokeWidth * 3.0, width: strokeWidth, height: 11.0 / 2.0 - strokeWidth * 3.0)) - }) -} - -private func generateClockMinImage(color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(color.cgColor) - let strokeWidth: CGFloat = 1.0 - context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: (11.0 - strokeWidth) / 2.0, width: 11.0 / 2.0 - strokeWidth, height: strokeWidth)) - }) -} - private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { if let _ = layer.animation(forKey: "clockFrameAnimation") { return @@ -58,29 +18,10 @@ private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) { basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0)) basicAnimation.repeatCount = Float.infinity basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) + basicAnimation.beginTime = 1.0 layer.add(basicAnimation, forKey: "clockFrameAnimation") } -private let checkBubbleFullImage = generateCheckImage(partial: false, color: UIColor(0x19C700)) -private let checkBubblePartialImage = generateCheckImage(partial: true, color: UIColor(0x19C700)) - -private let checkMediaFullImage = generateCheckImage(partial: false, color: .white) -private let checkMediaPartialImage = generateCheckImage(partial: true, color: .white) - -private let incomingDateColor = UIColor(0x525252, 0.6) -private let outgoingDateColor = UIColor(0x008c09, 0.8) - -private let imageBackground = generateStretchableFilledCircleImage(diameter: 18.0, color: UIColor(white: 0.0, alpha: 0.5)) - -private let clockBubbleFrameImage = generateClockFrameImage(color: UIColor(0x42b649)) -private let clockBubbleMinImage = generateClockMinImage(color: UIColor(0x42b649)) -private let clockMediaFrameImage = generateClockFrameImage(color: .white) -private let clockMediaMinImage = generateClockMinImage(color: .white) - -private let incomingImpressionIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ImpressionCount"), color: incomingDateColor) -private let outgoingImpressionIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ImpressionCount"), color: outgoingDateColor) -private let mediaImpressionIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ImpressionCount"), color: .white) - enum ChatMessageDateAndStatusOutgoingType { case Sent(read: Bool) case Sending @@ -92,9 +33,11 @@ enum ChatMessageDateAndStatusType { case BubbleOutgoing(ChatMessageDateAndStatusOutgoingType) case ImageIncoming case ImageOutgoing(ChatMessageDateAndStatusOutgoingType) + case FreeIncoming + case FreeOutgoing(ChatMessageDateAndStatusOutgoingType) } -class ChatMessageDateAndStatusNode: ASTransformLayerNode { +class ChatMessageDateAndStatusNode: ASDisplayNode { private var backgroundNode: ASImageNode? private var checkSentNode: ASImageNode? private var checkReadNode: ASImageNode? @@ -113,7 +56,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { self.addSubnode(self.dateNode) } - func asyncLayout() -> (_ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, (Bool) -> Void) { + func asyncLayout() -> (_ theme: PresentationTheme, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize) -> (CGSize, (Bool) -> Void) { let dateLayout = TextNode.asyncLayout(self.dateNode) var checkReadNode = self.checkReadNode @@ -124,7 +67,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { var currentBackgroundNode = self.backgroundNode var currentImpressionIcon = self.impressionIcon - return { edited, impressionCount, dateText, type, constrainedSize in + return { theme, edited, impressionCount, dateText, type, constrainedSize in let dateColor: UIColor var backgroundImage: UIImage? var outgoingStatus: ChatMessageDateAndStatusOutgoingType? @@ -136,50 +79,75 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { let clockMinImage: UIImage? var impressionImage: UIImage? + let graphics = PresentationResourcesChat.principalGraphics(theme) + switch type { case .BubbleIncoming: - dateColor = incomingDateColor + dateColor = theme.chat.bubble.incomingSecondaryTextColor leftInset = 10.0 - loadedCheckFullImage = checkBubbleFullImage - loadedCheckPartialImage = checkBubblePartialImage - clockFrameImage = clockBubbleFrameImage - clockMinImage = clockBubbleMinImage + loadedCheckFullImage = graphics.checkBubbleFullImage + loadedCheckPartialImage = graphics.checkBubblePartialImage + clockFrameImage = graphics.clockBubbleIncomingFrameImage + clockMinImage = graphics.clockBubbleIncomingMinImage if impressionCount != nil { - impressionImage = incomingImpressionIcon + impressionImage = graphics.incomingDateAndStatusImpressionIcon } case let .BubbleOutgoing(status): - dateColor = outgoingDateColor + dateColor = theme.chat.bubble.outgoingSecondaryTextColor outgoingStatus = status leftInset = 10.0 - loadedCheckFullImage = checkBubbleFullImage - loadedCheckPartialImage = checkBubblePartialImage - clockFrameImage = clockBubbleFrameImage - clockMinImage = clockBubbleMinImage + loadedCheckFullImage = graphics.checkBubbleFullImage + loadedCheckPartialImage = graphics.checkBubblePartialImage + clockFrameImage = graphics.clockBubbleOutgoingFrameImage + clockMinImage = graphics.clockBubbleOutgoingMinImage if impressionCount != nil { - impressionImage = outgoingImpressionIcon + impressionImage = graphics.outgoingDateAndStatusImpressionIcon } case .ImageIncoming: - dateColor = .white - backgroundImage = imageBackground + dateColor = theme.chat.bubble.mediaDateAndStatusTextColor + backgroundImage = graphics.dateAndStatusMediaBackground leftInset = 0.0 - loadedCheckFullImage = checkMediaFullImage - loadedCheckPartialImage = checkMediaPartialImage - clockFrameImage = clockMediaFrameImage - clockMinImage = clockMediaMinImage + loadedCheckFullImage = graphics.checkMediaFullImage + loadedCheckPartialImage = graphics.checkMediaPartialImage + clockFrameImage = graphics.clockMediaFrameImage + clockMinImage = graphics.clockMediaMinImage if impressionCount != nil { - impressionImage = mediaImpressionIcon + impressionImage = graphics.mediaImpressionIcon } case let .ImageOutgoing(status): - dateColor = .white + dateColor = theme.chat.bubble.mediaDateAndStatusTextColor outgoingStatus = status - backgroundImage = imageBackground + backgroundImage = graphics.dateAndStatusMediaBackground leftInset = 0.0 - loadedCheckFullImage = checkMediaFullImage - loadedCheckPartialImage = checkMediaPartialImage - clockFrameImage = clockMediaFrameImage - clockMinImage = clockMediaMinImage + loadedCheckFullImage = graphics.checkMediaFullImage + loadedCheckPartialImage = graphics.checkMediaPartialImage + clockFrameImage = graphics.clockMediaFrameImage + clockMinImage = graphics.clockMediaMinImage if impressionCount != nil { - impressionImage = mediaImpressionIcon + impressionImage = graphics.mediaImpressionIcon + } + case .FreeIncoming: + dateColor = theme.chat.bubble.mediaDateAndStatusTextColor + backgroundImage = graphics.dateAndStatusFreeBackground + leftInset = 0.0 + loadedCheckFullImage = graphics.checkMediaFullImage + loadedCheckPartialImage = graphics.checkMediaPartialImage + clockFrameImage = graphics.clockMediaFrameImage + clockMinImage = graphics.clockMediaMinImage + if impressionCount != nil { + impressionImage = graphics.mediaImpressionIcon + } + case let .FreeOutgoing(status): + dateColor = theme.chat.bubble.mediaDateAndStatusTextColor + outgoingStatus = status + backgroundImage = graphics.dateAndStatusFreeBackground + leftInset = 0.0 + loadedCheckFullImage = graphics.checkMediaFullImage + loadedCheckPartialImage = graphics.checkMediaPartialImage + clockFrameImage = graphics.clockMediaFrameImage + clockMinImage = graphics.clockMediaMinImage + if impressionCount != nil { + impressionImage = graphics.mediaImpressionIcon } } diff --git a/TelegramUI/ChatMessageDateHeader.swift b/TelegramUI/ChatMessageDateHeader.swift index dfbf7975c8..a29e8d8bf2 100644 --- a/TelegramUI/ChatMessageDateHeader.swift +++ b/TelegramUI/ChatMessageDateHeader.swift @@ -17,9 +17,14 @@ final class ChatMessageDateHeader: ListViewItemHeader { private let roundedTimestamp: Int32 let id: Int64 + let theme: PresentationTheme + let strings: PresentationStrings - init(timestamp: Int32) { + init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings) { self.timestamp = timestamp + self.theme = theme + self.strings = strings + if timestamp == Int32.max { self.roundedTimestamp = timestamp / (granularity) * (granularity) } else { @@ -33,34 +38,42 @@ final class ChatMessageDateHeader: ListViewItemHeader { let height: CGFloat = 34.0 func node() -> ListViewItemHeaderNode { - return ChatMessageDateHeaderNode(timestamp: self.roundedTimestamp) + return ChatMessageDateHeaderNode(timestamp: self.roundedTimestamp, theme: self.theme, strings: self.strings) } } -private func backgroundImage(color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context -> Void in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(color.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - })?.stretchableImage(withLeftCapWidth: 13, topCapHeight: 13) -} - private let titleFont = Font.medium(13.0) -private let months: [String] = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December" -] +private func monthAtIndex(_ index: Int, strings: PresentationStrings) -> String { + switch index { + case 0: + return strings.Month_GenJanuary + case 1: + return strings.Month_GenFebruary + case 2: + return strings.Month_GenMarch + case 3: + return strings.Month_GenApril + case 4: + return strings.Month_GenMay + case 5: + return strings.Month_GenJune + case 6: + return strings.Month_GenJuly + case 7: + return strings.Month_GenAugust + case 8: + return strings.Month_GenSeptember + case 9: + return strings.Month_GenOctober + case 10: + return strings.Month_GenNovember + case 11: + return strings.Month_GenDecember + default: + return "" + } +} final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { let labelNode: TextNode @@ -68,12 +81,18 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { let stickBackgroundNode: ASImageNode private let timestamp: Int32 + private var theme: PresentationTheme + private var strings: PresentationStrings private var flashingOnScrolling = false private var stickDistanceFactor: CGFloat = 0.0 - init(timestamp: Int32) { + //private let testNode = ASDisplayNode() + + init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings) { self.timestamp = timestamp + self.theme = theme + self.strings = strings self.labelNode = TextNode() self.labelNode.isLayerBacked = true @@ -89,18 +108,24 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { self.stickBackgroundNode.displayWithoutProcessing = true self.stickBackgroundNode.displaysAsynchronously = false - super.init(dynamicBounce: true, isRotated: true) + //self.testNode.backgroundColor = .black + //self.testNode.isLayerBacked = true - self.isLayerBacked = true - self.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0.0, 0.0, 1.0) + super.init(layerBacked: true, dynamicBounce: true, isRotated: true, seeThrough: false) - self.backgroundNode.image = backgroundImage(color: UIColor(0x748391, 0.45)) - self.stickBackgroundNode.image = backgroundImage(color: UIColor(0x939fab, 0.5)) + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) + + let graphics = PresentationResourcesChat.principalGraphics(theme) + + self.backgroundNode.image = graphics.dateStaticBackground + self.stickBackgroundNode.image = graphics.dateFloatingBackground self.stickBackgroundNode.alpha = 0.0 self.backgroundNode.addSubnode(self.stickBackgroundNode) self.addSubnode(self.backgroundNode) self.addSubnode(self.labelNode) + //self.addSubnode(self.testNode) + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) var t: time_t = time_t(timestamp) @@ -112,18 +137,41 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { localtime_r(&now, &timeinfoNow) let text: String - if timeinfo.tm_year == timeinfoNow.tm_year && timeinfo.tm_yday == timeinfoNow.tm_yday { - text = "Today" + if timeinfo.tm_year == timeinfoNow.tm_year { + if timeinfo.tm_yday == timeinfoNow.tm_yday { + text = strings.Weekday_Today + } else { + text = "\(monthAtIndex(Int(timeinfo.tm_mon), strings: strings)) \(timeinfo.tm_mday)" + } } else { - text = "\(months[Int(timeinfo.tm_mon)]) \(timeinfo.tm_mday)" + text = "\(monthAtIndex(Int(timeinfo.tm_mon), strings: strings)) \(timeinfo.tm_mday), \(1900 + timeinfo.tm_year)" } let attributedString = NSAttributedString(string: text, font: titleFont, textColor: UIColor.white) let labelLayout = TextNode.asyncLayout(self.labelNode) let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - apply() + let _ = apply() self.labelNode.frame = CGRect(origin: CGPoint(), size: size.size) + + /*(self.layer as! CASeeThroughTracingLayer).updateRelativePosition = { [weak self] position in + if let strongSelf = self { + strongSelf.testNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 70.0 + position.y), size: CGSize(width: 40.0, height: 20.0)) + print("position \(position.x), \(position.y)") + } + }*/ + } + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + + let graphics = PresentationResourcesChat.principalGraphics(theme) + + self.backgroundNode.image = graphics.dateStaticBackground + self.stickBackgroundNode.image = graphics.dateFloatingBackground + + self.setNeedsLayout() } override func layout() { diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index 037079c7cf..33ba537963 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -64,7 +64,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { automaticDownload = true } - let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.message, selectedFile!, automaticDownload, item.message.effectivelyIncoming, statusType, CGSize(width: constrainedSize.width, height: constrainedSize.height)) + let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item, selectedFile!, automaticDownload, item.message.effectivelyIncoming, statusType, CGSize(width: constrainedSize.width, height: constrainedSize.height)) return (initialWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { constrainedSize in let (refinedWidth, finishLayout) = refineLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageForwardInfoNode.swift b/TelegramUI/ChatMessageForwardInfoNode.swift index 9b2782e2bb..806f467715 100644 --- a/TelegramUI/ChatMessageForwardInfoNode.swift +++ b/TelegramUI/ChatMessageForwardInfoNode.swift @@ -6,17 +6,17 @@ import Postbox private let prefixFont = Font.regular(13.0) private let peerFont = Font.medium(13.0) -class ChatMessageForwardInfoNode: ASTransformLayerNode { +class ChatMessageForwardInfoNode: ASDisplayNode { private var textNode: TextNode? override init() { super.init() } - class func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ incoming: Bool, _ peer: Peer, _ authorPeer: Peer?, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageForwardInfoNode) { + class func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ theme: PresentationTheme, _ incoming: Bool, _ peer: Peer, _ authorPeer: Peer?, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageForwardInfoNode) { let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode) - return { incoming, peer, authorPeer, constrainedSize in + return { theme, incoming, peer, authorPeer, constrainedSize in let prefix: NSString = "Forwarded Message\nFrom: " let peerString: String if let authorPeer = authorPeer { @@ -25,7 +25,7 @@ class ChatMessageForwardInfoNode: ASTransformLayerNode { peerString = peer.displayTitle } let completeString: NSString = "\(prefix)\(peerString)" as NSString - let color = incoming ? UIColor(0x007bff) : UIColor(0x00a516) + let color = incoming ? theme.chat.bubble.incomingAccentColor : theme.chat.bubble.outgoingAccentColor let string = NSMutableAttributedString(string: completeString as String, attributes: [NSForegroundColorAttributeName: color, NSFontAttributeName: prefixFont]) string.addAttributes([NSFontAttributeName: peerFont], range: NSMakeRange(prefix.length, completeString.length - prefix.length)) let (textLayout, textApply) = textNodeLayout(string, nil, 2, .end, constrainedSize, .natural, nil, UIEdgeInsets()) diff --git a/TelegramUI/ChatMessageInstantVideoItemNode.swift b/TelegramUI/ChatMessageInstantVideoItemNode.swift index 5a96fdbdf5..82ac1bbdac 100644 --- a/TelegramUI/ChatMessageInstantVideoItemNode.swift +++ b/TelegramUI/ChatMessageInstantVideoItemNode.swift @@ -5,8 +5,6 @@ import SwiftSignalKit import Postbox import TelegramCore -private let backgroundImage = generateInstantVideoBackground(incoming: true) - class ChatMessageInstantVideoItemNode: ChatMessageItemView { let backgroundNode: ASImageNode let videoNode: ManagedVideoNode @@ -15,6 +13,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { private var selectionNode: ChatMessageSelectionNode? + private var appliedItem: ChatMessageItem? var telegramFile: TelegramMediaFile? private let fetchDisposable = MetaDisposable() @@ -22,6 +21,23 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { private var replyInfoNode: ChatMessageReplyInfoNode? private var replyBackgroundNode: ASImageNode? + private let dateAndStatusNode: ChatMessageDateAndStatusNode + private let muteIconNode: ASImageNode + + private let playbackStatusDisposable = MetaDisposable() + + override var visibility: ListViewItemNodeVisibility { + didSet { + if self.visibility != oldValue { + if let item = self.item, let telegramFile = self.telegramFile, case .visible = self.visibility { + self.videoNode.acquireContext(account: item.account, mediaManager: item.account.telegramApplicationContext.mediaManager, id: PeerMessageManagedMediaId(messageId: item.message.id), resource: telegramFile.resource, priority: 1) + } else { + self.videoNode.discardContext() + } + } + } + } + required init() { self.backgroundNode = ASImageNode() self.backgroundNode.displaysAsynchronously = false @@ -29,12 +45,18 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { self.videoNode = ManagedVideoNode() - super.init(layerBacked: false) + self.dateAndStatusNode = ChatMessageDateAndStatusNode() + self.muteIconNode = ASImageNode() + self.muteIconNode.isLayerBacked = true + self.muteIconNode.displayWithoutProcessing = true + self.muteIconNode.displaysAsynchronously = false - self.backgroundNode.image = backgroundImage + super.init(layerBacked: false) self.addSubnode(self.backgroundNode) self.addSubnode(self.videoNode) + self.addSubnode(self.dateAndStatusNode) + self.addSubnode(self.muteIconNode) } required init?(coder aDecoder: NSCoder) { @@ -43,6 +65,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { deinit { self.fetchDisposable.dispose() + self.playbackStatusDisposable.dispose() } override func didLoad() { @@ -56,14 +79,28 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - let displaySize = CGSize(width: 210.0, height: 210.0) + let displaySize = CGSize(width: 212.0, height: 212.0) let previousFile = self.telegramFile let layoutConstants = self.layoutConstants let makeReplyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) let currentReplyBackgroundNode = self.replyBackgroundNode + let currentItem = self.appliedItem + + let makeDateAndStatusLayout = self.dateAndStatusNode.asyncLayout() + return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in + var updatedTheme: PresentationTheme? + + var updatedBackgroundImage: UIImage? + var updatedMuteIconImage: UIImage? + if item.theme !== currentItem?.theme { + updatedTheme = item.theme + updatedBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(item.theme) + updatedMuteIconImage = PresentationResourcesChat.chatInstantMessageMuteIconImage(item.theme) + } + let incoming = item.message.effectivelyIncoming let imageSize = displaySize @@ -80,7 +117,30 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { } } - let avatarInset: CGFloat = (item.peerId.isGroupOrChannel && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0 + var updatedPlaybackStatus: Signal? + if let updatedFile = updatedFile, updatedMedia { + updatedPlaybackStatus = fileMediaResourceStatus(account: item.account, file: updatedFile, message: item.message) + } + + let avatarInset: CGFloat + var hasAvatar = false + + if item.peerId.isGroupOrChannel && item.message.author != nil { + var isBroadcastChannel = false + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + isBroadcastChannel = true + } + + if !isBroadcastChannel { + hasAvatar = true + } + } + + if hasAvatar { + avatarInset = layoutConstants.avatarDiameter + } else { + avatarInset = 0.0 + } var layoutInsets = layoutConstants.instantVideo.insets if dateHeaderAtBottom { @@ -97,20 +157,73 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { for attribute in item.message.attributes { if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { let availableWidth = max(60.0, width - imageSize.width - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) - replyInfoApply = makeReplyInfoLayout(item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + replyInfoApply = makeReplyInfoLayout(item.theme, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) if let currentReplyBackgroundNode = currentReplyBackgroundNode { updatedReplyBackgroundNode = currentReplyBackgroundNode } else { updatedReplyBackgroundNode = ASImageNode() } - replyBackgroundImage = backgroundImage + replyBackgroundImage = PresentationResourcesChat.chatServiceBubbleFillImage(item.theme) break } } + let statusType: ChatMessageDateAndStatusType + if item.message.effectivelyIncoming { + statusType = .FreeIncoming + } else { + if item.message.flags.contains(.Failed) { + statusType = .FreeOutgoing(.Failed) + } else if item.message.flags.isSending { + statusType = .FreeOutgoing(.Sending) + } else { + statusType = .FreeOutgoing(.Sent(read: item.read)) + } + } + + var t = Int(item.message.timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo) + + var edited = false + var sentViaBot = false + var viewCount: Int? + for attribute in item.message.attributes { + if let _ = attribute as? EditedMessageAttribute { + edited = true + } else if let attribute = attribute as? ViewCountMessageAttribute { + viewCount = attribute.count + } else if let _ = attribute as? InlineBotMessageAttribute { + sentViaBot = true + } + } + + var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + + if let author = item.message.author as? TelegramUser { + if author.botInfo != nil { + sentViaBot = true + } + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + dateText = "\(author.displayTitle), \(dateText)" + } + } + + let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)) + return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: imageSize.height), insets: layoutInsets), { [weak self] animation in if let strongSelf = self { + strongSelf.appliedItem = item + + if let updatedBackgroundImage = updatedBackgroundImage { + strongSelf.backgroundNode.image = updatedBackgroundImage + } + + if let updatedMuteIconImage = updatedMuteIconImage { + strongSelf.muteIconNode.image = updatedMuteIconImage + } + strongSelf.telegramFile = updatedFile strongSelf.videoNode.frame = videoFrame @@ -118,8 +231,45 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { strongSelf.backgroundNode.frame = videoFrame.insetBy(dx: -2.0, dy: -2.0) - if let telegramFile = updatedFile, updatedMedia, let context = item.account.applicationContext as? TelegramApplicationContext { - strongSelf.videoNode.acquireContext(account: item.account, mediaManager: context.mediaManager, id: PeerMessageManagedMediaId(messageId: item.message.id), resource: telegramFile.resource) + if let image = strongSelf.muteIconNode.image { + strongSelf.muteIconNode.frame = CGRect(origin: CGPoint(x: floor(videoFrame.minX + (videoFrame.size.width - image.size.width) / 2.0), y: videoFrame.maxY - image.size.height - 8.0), size: image.size) + } + + if let updatedPlaybackStatus = updatedPlaybackStatus { + strongSelf.playbackStatusDisposable.set((updatedPlaybackStatus |> deliverOnMainQueue).start(next: { status in + if let strongSelf = self { + let displayMute: Bool + switch status { + case let .fetchStatus(fetchStatus): + switch fetchStatus { + case .Local: + displayMute = true + default: + displayMute = false + } + case .playbackStatus: + displayMute = false + } + if displayMute != (!strongSelf.muteIconNode.alpha.isZero) { + if displayMute { + strongSelf.muteIconNode.alpha = 1.0 + strongSelf.muteIconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + strongSelf.muteIconNode.layer.animateScale(from: 0.4, to: 1.0, duration: 0.15) + } else { + strongSelf.muteIconNode.alpha = 0.0 + strongSelf.muteIconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + strongSelf.muteIconNode.layer.animateScale(from: 1.0, to: 0.4, duration: 0.15) + } + } + } + })) + } + + dateAndStatusApply(false) + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: min(floor(videoFrame.midX) + 70.0, width - dateAndStatusSize.width - 4.0), y: videoFrame.maxY - dateAndStatusSize.height), size: dateAndStatusSize) + + if let telegramFile = updatedFile, updatedMedia, let context = item.account.applicationContext as? TelegramApplicationContext, strongSelf.visibility == .visible { + strongSelf.videoNode.acquireContext(account: item.account, mediaManager: context.mediaManager, id: PeerMessageManagedMediaId(messageId: item.message.id), resource: telegramFile.resource, priority: 1) } strongSelf.progressNode?.position = strongSelf.videoNode.position @@ -171,6 +321,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { case .tap: + if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { + if let item = self.item, let author = item.message.author { + self.controllerInteraction?.openPeer(author.id, .info, item.message.id) + } + return + } + if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) { if let item = self.item { for attribute in item.message.attributes { diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index e62c275edc..5a0f08d247 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -14,19 +14,6 @@ private let titleFont = Font.regular(16.0) private let descriptionFont = Font.regular(13.0) private let durationFont = Font.regular(11.0) -private let incomingTitleColor = UIColor(0x0b8bed) -private let outgoingTitleColor = UIColor(0x3faa3c) -private let incomingDescriptionColor = UIColor(0x999999) -private let outgoingDescriptionColor = UIColor(0x6fb26a) -private let incomingDurationColor = UIColor(0x525252, 0.6) -private let outgoingDurationColor = UIColor(0x008c09, 0.8) - -private let consumableContentIncomingIcon = generateFilledCircleImage(diameter: 4.0, color: UIColor(0x1581e2)) -private let consumableContentOutgoingIcon = generateFilledCircleImage(diameter: 4.0, color: UIColor(0x19c700)) - -private let fileIconIncomingImage = UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocumentIncoming")?.precomposed() -private let fileIconOutgoingImage = UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocumentOutgoing")?.precomposed() - final class ChatMessageInteractiveFileNode: ASTransformNode { private let titleNode: TextNode private let descriptionNode: TextNode @@ -46,7 +33,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { var activateLocalContent: () -> Void = { } private var account: Account? - private var message: Message? + private var item: ChatMessageItem? private var file: TelegramMediaFile? init() { @@ -85,7 +72,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if let resourceStatus = self.resourceStatus { switch resourceStatus { case let .fetchStatus(fetchStatus): - if let account = self.account, let message = self.message, message.flags.isSending { + if let account = self.account, let message = self.item?.message, message.flags.isSending { let _ = account.postbox.modify({ modifier -> Void in modifier.deleteMessages([message.id]) }).start() @@ -117,15 +104,24 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - func asyncLayout() -> (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { + func asyncLayout() -> (_ account: Account, _ item: ChatMessageItem, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { let currentFile = self.file let titleAsyncLayout = TextNode.asyncLayout(self.titleNode) let descriptionAsyncLayout = TextNode.asyncLayout(self.descriptionNode) - let currentMessage = self.message let statusLayout = self.dateAndStatusNode.asyncLayout() - return { account, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in + let currentItem = self.item + + return { account, item, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in + let message = item.message + + var updatedTheme: PresentationTheme? + + if item.theme !== currentItem?.theme { + updatedTheme = item.theme + } + return (CGFloat.greatestFiniteMagnitude, { constrainedSize in //var updateImageSignal: Signal DrawingContext, NoError>? var updatedStatusSignal: Signal? @@ -138,6 +134,8 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { mediaUpdated = true } + let currentMessage = currentItem?.message + var statusUpdated = mediaUpdated if currentMessage?.id != message.id || currentMessage?.flags != message.flags { statusUpdated = true @@ -165,9 +163,9 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if let attribute = attribute as? ConsumableContentMessageAttribute { if !attribute.consumed { if incoming { - consumableContentIcon = consumableContentIncomingIcon + consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentIncomingIcon(item.theme) } else { - consumableContentIcon = consumableContentOutgoingIcon + consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentOutgoingIcon(item.theme) } } break @@ -191,12 +189,19 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { sentViaBot = true } } - if let author = message.author as? TelegramUser, author.botInfo != nil { - sentViaBot = true - } - let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) - let (size, apply) = statusLayout(edited && !sentViaBot, viewCount, dateText, statusType, constrainedSize) + var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + + if let author = item.message.author as? TelegramUser { + if author.botInfo != nil { + sentViaBot = true + } + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + dateText = "\(author.displayTitle), \(dateText)" + } + } + + let (size, apply) = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, constrainedSize) statusSize = size statusApply = apply } @@ -209,6 +214,8 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { var isVoice = false var audioDuration: Int32 = 0 + let bubbleTheme = item.theme.chat.bubble + for attribute in file.attributes { if case let .Audio(voice, duration, title, performer, waveform) = attribute { isAudio = true @@ -226,14 +233,14 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { audioDuration = Int32(duration) if voice { isVoice = true - candidateDescriptionString = NSAttributedString(string: String(format: "%d:%02d", duration / 60, duration % 60), font: durationFont, textColor:incoming ? incomingDurationColor : outgoingDurationColor) + candidateDescriptionString = NSAttributedString(string: String(format: "%d:%02d", duration / 60, duration % 60), font: durationFont, textColor:incoming ? bubbleTheme.incomingFileDurationColor : bubbleTheme.outgoingFileDurationColor) if let waveform = waveform { waveform.withDataNoCopy { data in audioWaveform = AudioWaveform(bitstream: data, bitsPerSample: 5) } } } else { - candidateTitleString = NSAttributedString(string: title ?? "Unknown Track", font: titleFont, textColor: incoming ? incomingTitleColor : outgoingTitleColor) + candidateTitleString = NSAttributedString(string: title ?? "Unknown Track", font: titleFont, textColor: incoming ? bubbleTheme.incomingFileTitleColor : bubbleTheme.outgoingFileTitleColor) let descriptionText: String if let performer = performer { descriptionText = performer @@ -242,7 +249,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } else { descriptionText = "" } - candidateDescriptionString = NSAttributedString(string: descriptionText, font: descriptionFont, textColor:incoming ? incomingDescriptionColor : outgoingDescriptionColor) + candidateDescriptionString = NSAttributedString(string: descriptionText, font: descriptionFont, textColor:incoming ? bubbleTheme.incomingFileDescriptionColor : bubbleTheme.outgoingFileDescriptionColor) } break } @@ -254,7 +261,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if let candidateTitleString = candidateTitleString { titleString = candidateTitleString } else if !isVoice { - titleString = NSAttributedString(string: file.fileName ?? "File", font: titleFont, textColor: incoming ? incomingTitleColor : outgoingTitleColor) + titleString = NSAttributedString(string: file.fileName ?? "File", font: titleFont, textColor: incoming ? bubbleTheme.incomingFileTitleColor : bubbleTheme.outgoingFileTitleColor) } if let candidateDescriptionString = candidateDescriptionString { @@ -266,7 +273,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } else { descriptionText = "" } - descriptionString = NSAttributedString(string: descriptionText, font: descriptionFont, textColor:incoming ? incomingDescriptionColor : outgoingDescriptionColor) + descriptionString = NSAttributedString(string: descriptionText, font: descriptionFont, textColor:incoming ? bubbleTheme.incomingFileDescriptionColor : bubbleTheme.outgoingFileDescriptionColor) } let textConstrainedSize = CGSize(width: constrainedSize.width - 44.0 - 8.0, height: constrainedSize.height) @@ -294,6 +301,8 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { minLayoutWidth = max(titleLayout.size.width, descriptionLayout.size.width) + 44.0 + 8.0 } + let fileIconImage = incoming ? PresentationResourcesChat.chatBubbleRadialIndicatorFileIconIncoming(item.theme) : PresentationResourcesChat.chatBubbleRadialIndicatorFileIconOutgoing(item.theme) + return (minLayoutWidth, { boundingWidth in let progressDiameter: CGFloat = isVoice ? 37.0 : 44.0 let progressFrame = CGRect(origin: CGPoint(x: 0.0, y: isVoice ? -5.0 : 0.0), size: CGSize(width: progressDiameter, height: progressDiameter)) @@ -319,7 +328,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { return (fittedLayoutSize, { [weak self] in if let strongSelf = self { strongSelf.account = account - strongSelf.message = message + strongSelf.item = item strongSelf.file = file let _ = titleApply() @@ -356,7 +365,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { strongSelf.addSubnode(strongSelf.waveformNode) } strongSelf.waveformNode.frame = CGRect(origin: CGPoint(x: 43.0, y: -1.0), size: CGSize(width: boundingWidth - 41.0, height: 12.0)) - strongSelf.waveformNode.setup(color: UIColor(incoming ? 0x007ee5 : 0x3fc33b), waveform: audioWaveform) + strongSelf.waveformNode.setup(color: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor, waveform: audioWaveform) } else if strongSelf.waveformNode.supernode != nil { strongSelf.waveformNode.removeFromSupernode() } @@ -372,10 +381,12 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { strongSelf.resourceStatus = status if strongSelf.progressNode == nil { - let progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(incoming ? 0x007ee5 : 0x3fc33b), foregroundColor: incoming ? UIColor.white : UIColor(0xe1ffc7), icon: incoming ? fileIconIncomingImage : fileIconOutgoingImage)) + let progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor, foregroundColor: incoming ? bubbleTheme.incomingFillColor : bubbleTheme.outgoingFillColor, icon: fileIconImage)) strongSelf.progressNode = progressNode progressNode.frame = progressFrame strongSelf.addSubnode(progressNode) + } else if let _ = updatedTheme { + strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor, foregroundColor: incoming ? bubbleTheme.incomingFillColor : bubbleTheme.outgoingFillColor, icon: fileIconImage)) } switch status { @@ -424,12 +435,12 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ item: ChatMessageItem, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { let currentAsyncLayout = node?.asyncLayout() - return { account, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in + return { account, item, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in var fileNode: ChatMessageInteractiveFileNode - var fileLayout: (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAnsStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) + var fileLayout: (_ account: Account, _ item: ChatMessageItem, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAnsStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { fileNode = node @@ -439,7 +450,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { fileLayout = fileNode.asyncLayout() } - let (initialWidth, continueLayout) = fileLayout(account, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize) + let (initialWidth, continueLayout) = fileLayout(account, item, file, automaticDownload, incoming, dateAndStatusType, constrainedSize) return (initialWidth, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 0515217dc0..b34bd96c39 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -10,8 +10,6 @@ private struct FetchControls { let cancel: () -> Void } -private let secretMediaIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: .white) - final class ChatMessageInteractiveMediaNode: ASTransformNode { private let imageNode: TransformImageNode private var videoNode: ManagedVideoNode? @@ -22,7 +20,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { private var account: Account? private var messageIdAndFlags: (MessageId, MessageFlags)? private var media: Media? - private var message: Message? + private var item: ChatMessageItem? private let statusDisposable = MetaDisposable() private let fetchControls = Atomic(value: nil) @@ -104,7 +102,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - func asyncLayout() -> (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { + func asyncLayout() -> (_ account: Account, _ item: ChatMessageItem, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { let currentMessageIdAndFlags = self.messageIdAndFlags let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() @@ -112,9 +110,19 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { let currentVideoNode = self.videoNode let hasCurrentVideoNode = currentVideoNode != nil - return { account, message, media, corners, automaticDownload, constrainedSize, layoutConstants in + let currentItem = self.item + + return { account, item, media, corners, automaticDownload, constrainedSize, layoutConstants in var nativeSize: CGSize + var updatedTheme: PresentationTheme? + + if item.theme !== currentItem?.theme { + updatedTheme = item.theme + } + + let message = item.message + let isSecretMedia = message.containsSecretMedia var secretBeginTimeAndTimeout: (Double, Double)? if isSecretMedia { @@ -158,7 +166,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { var secretProgressIcon: UIImage? if isSecretMedia { - secretProgressIcon = secretMediaIcon + secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(item.theme) } return (maxWidth, updatedCorners, { constrainedSize in @@ -278,7 +286,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { strongSelf.account = account strongSelf.messageIdAndFlags = (message.id, message.flags) strongSelf.media = media - strongSelf.message = message + strongSelf.item = item strongSelf.imageNode.frame = imageFrame strongSelf.progressNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) strongSelf.timeoutNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) @@ -301,7 +309,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if let videoNode = strongSelf.videoNode { if let updateVideoFile = updateVideoFile { if let applicationContext = account.applicationContext as? TelegramApplicationContext { - videoNode.acquireContext(account: account, mediaManager: applicationContext.mediaManager, id: PeerMessageManagedMediaId(messageId: message.id), resource: updateVideoFile.resource) + videoNode.acquireContext(account: account, mediaManager: applicationContext.mediaManager, id: PeerMessageManagedMediaId(messageId: message.id), resource: updateVideoFile.resource, priority: 1) } } @@ -315,12 +323,14 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if let secretBeginTimeAndTimeout = secretBeginTimeAndTimeout { if strongSelf.timeoutNode == nil { - let timeoutNode = RadialTimeoutNode(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor(white: 1.0, alpha: 0.6)) + let timeoutNode = RadialTimeoutNode(backgroundColor: item.theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: item.theme.chat.bubble.mediaOverlayControlForegroundColor) timeoutNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) timeoutNode.position = strongSelf.imageNode.position strongSelf.timeoutNode = timeoutNode strongSelf.addSubnode(timeoutNode) timeoutNode.setTimeout(beginTimestamp: secretBeginTimeAndTimeout.0, timeout: secretBeginTimeAndTimeout.1) + } else if let updatedTheme = updatedTheme { + strongSelf.timeoutNode?.updateTheme(backgroundColor: updatedTheme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: updatedTheme.chat.bubble.mediaOverlayControlForegroundColor) } if let progressNode = strongSelf.progressNode { @@ -353,11 +363,13 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { if progressRequired { if strongSelf.progressNode == nil { - let progressNode = RadialProgressNode() + let progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: item.theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: item.theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) progressNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) progressNode.position = strongSelf.imageNode.position strongSelf.progressNode = progressNode strongSelf.addSubnode(progressNode) + } else if let _ = updatedTheme { + strongSelf.progressNode?.updateTheme(RadialProgressTheme(backgroundColor: item.theme.chat.bubble.mediaOverlayControlBackgroundColor, foregroundColor: item.theme.chat.bubble.mediaOverlayControlForegroundColor, icon: nil)) } } else { if let progressNode = strongSelf.progressNode { @@ -409,12 +421,12 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ message: ChatMessageItem, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() - return { account, message, media, corners, automaticDownload, constrainedSize, layoutConstants in + return { account, item, media, corners, automaticDownload, constrainedSize, layoutConstants in var imageNode: ChatMessageInteractiveMediaNode - var imageLayout: (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) + var imageLayout: (_ account: Account, _ item: ChatMessageItem, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, ImageCorners, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node @@ -424,7 +436,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { imageLayout = imageNode.asyncLayout() } - let (initialWidth, corners, continueLayout) = imageLayout(account, message, media, corners, automaticDownload, constrainedSize, layoutConstants) + let (initialWidth, corners, continueLayout) = imageLayout(account, item, media, corners, automaticDownload, constrainedSize, layoutConstants) return (initialWidth, corners, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index 1c372f3217..f961a08cfb 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -77,6 +77,8 @@ func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) - } public final class ChatMessageItem: ListViewItem, CustomStringConvertible { + let theme: PresentationTheme + let strings: PresentationStrings let account: Account let peerId: PeerId let controllerInteraction: ChatControllerInteraction @@ -86,7 +88,9 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { public let accessoryItem: ListViewAccessoryItem? let header: ChatMessageDateHeader - public init(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, message: Message, read: Bool) { + public init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, message: Message, read: Bool) { + self.theme = theme + self.strings = strings self.account = account self.peerId = peerId self.controllerInteraction = controllerInteraction @@ -97,7 +101,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let incoming = message.effectivelyIncoming let displayAuthorInfo = incoming && message.author != nil && peerId.isGroupOrChannel - self.header = ChatMessageDateHeader(timestamp: message.timestamp) + self.header = ChatMessageDateHeader(timestamp: message.timestamp, theme: theme, strings: strings) if displayAuthorInfo { var hasActionMedia = false @@ -107,7 +111,11 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { break } } - if !hasActionMedia { + var isBroadcastChannel = false + if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + isBroadcastChannel = true + } + if !hasActionMedia && !isBroadcastChannel { if let author = message.author { accessoryItem = ChatMessageAvatarAccessoryItem(account: account, peerId: author.id, peer: author, messageTimestamp: message.timestamp) } @@ -135,8 +143,12 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { break } } - } else if let _ = media as? TelegramMediaAction { - viewClassName = ChatMessageActionItemNode.self + } else if let action = media as? TelegramMediaAction { + if case .phoneCall = action.action { + viewClassName = ChatMessageBubbleItemNode.self + } else { + viewClassName = ChatMessageActionItemNode.self + } } } diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 1366dc02b4..a1caf427f9 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -59,7 +59,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { let initialImageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) - let (initialWidth, _, refineLayout) = interactiveImageLayout(item.account, item.message, selectedMedia!, initialImageCorners, item.account.settings.automaticDownloadSettingsForPeerId(item.peerId).downloadPhotos, CGSize(width: constrainedSize.width, height: constrainedSize.height), layoutConstants) + let (initialWidth, _, refineLayout) = interactiveImageLayout(item.account, item, selectedMedia!, initialImageCorners, item.account.settings.automaticDownloadSettingsForPeerId(item.peerId).downloadPhotos, CGSize(width: constrainedSize.width, height: constrainedSize.height), layoutConstants) return (initialWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { constrainedSize in let (refinedWidth, finishLayout) = refineLayout(constrainedSize) @@ -83,10 +83,17 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { sentViaBot = true } } - if let author = item.message.author as? TelegramUser, author.botInfo != nil { - sentViaBot = true + + var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + + if let author = item.message.author as? TelegramUser { + if author.botInfo != nil { + sentViaBot = true + } + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + dateText = "\(author.displayTitle), \(dateText)" + } } - let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) let statusType: ChatMessageDateAndStatusType? if case .None = position.bottom { @@ -111,7 +118,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: imageLayoutSize.width, height: CGFloat.greatestFiniteMagnitude)) + let (size, apply) = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: imageLayoutSize.width, height: CGFloat.greatestFiniteMagnitude)) statusSize = size statusApply = apply } diff --git a/TelegramUI/ChatMessageNotificationItem.swift b/TelegramUI/ChatMessageNotificationItem.swift index d7d9bb3e8f..a1242eb7c7 100644 --- a/TelegramUI/ChatMessageNotificationItem.swift +++ b/TelegramUI/ChatMessageNotificationItem.swift @@ -69,10 +69,14 @@ final class ChatMessageNotificationItemNode: NotificationItemNode { if let peer = messageMainPeer(item.message) { self.avatarNode.setPeer(account: item.account, peer: peer) - } - - if let peer = item.message.peers[item.message.id.peerId] { - self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.semibold(16.0), textColor: .black) + + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.semibold(16.0), textColor: .black) + } else if let author = item.message.author, author.id != peer.id { + self.titleNode.attributedText = NSAttributedString(string: author.displayTitle + "@" + peer.displayTitle, font: Font.semibold(16.0), textColor: .black) + } else { + self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle, font: Font.semibold(16.0), textColor: .black) + } } var updatedMedia: Media? diff --git a/TelegramUI/ChatMessageReplyInfoNode.swift b/TelegramUI/ChatMessageReplyInfoNode.swift index 3492e82abb..c7151feae4 100644 --- a/TelegramUI/ChatMessageReplyInfoNode.swift +++ b/TelegramUI/ChatMessageReplyInfoNode.swift @@ -76,7 +76,7 @@ enum ChatMessageReplyInfoType { case standalone } -class ChatMessageReplyInfoNode: ASTransformLayerNode { +class ChatMessageReplyInfoNode: ASDisplayNode { private let contentNode: ASDisplayNode private let lineNode: ASDisplayNode private var titleNode: TextNode? @@ -101,14 +101,14 @@ class ChatMessageReplyInfoNode: ASTransformLayerNode { self.contentNode.addSubnode(self.lineNode) } - class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ account: Account, _ type: ChatMessageReplyInfoType, _ message: Message, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode) { + class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ theme: PresentationTheme, _ account: Account, _ type: ChatMessageReplyInfoType, _ message: Message, _ constrainedSize: CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode) { let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode) let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode) let imageNodeLayout = TransformImageNode.asyncLayout(maybeNode?.imageNode) let previousMedia = maybeNode?.previousMedia - return { account, type, message, constrainedSize in + return { theme, account, type, message, constrainedSize in let titleString = message.author?.displayTitle ?? "" let (textString, textMedia) = textStringForReplyMessage(message) @@ -118,13 +118,13 @@ class ChatMessageReplyInfoNode: ASTransformLayerNode { switch type { case let .bubble(incoming): - titleColor = incoming ? UIColor(0x007bff) : UIColor(0x00a516) - lineColor = incoming ? UIColor(0x3ca7fe) : UIColor(0x29cc10) - textColor = .black + titleColor = incoming ? theme.chat.bubble.incomingAccentColor : theme.chat.bubble.outgoingAccentColor + lineColor = incoming ? theme.chat.bubble.incomingAccentColor : theme.chat.bubble.outgoingAccentColor + textColor = incoming ? theme.chat.bubble.incomingPrimaryTextColor : theme.chat.bubble.outgoingPrimaryTextColor case .standalone: - titleColor = .white - lineColor = .white - textColor = .white + titleColor = theme.chat.serviceMessage.serviceMessagePrimaryTextColor + lineColor = titleColor + textColor = titleColor } var leftInset: CGFloat = 10.0 diff --git a/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/TelegramUI/ChatMessageSelectionInputPanelNode.swift index d1be24e02a..a210d4f7cb 100644 --- a/TelegramUI/ChatMessageSelectionInputPanelNode.swift +++ b/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -8,7 +8,9 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private let deleteButton: UIButton private let forwardButton: UIButton - private var presentationInterfaceState = ChatPresentationInterfaceState() + private var presentationInterfaceState: ChatPresentationInterfaceState? + + private var theme: PresentationTheme var selectedMessageCount: Int = 0 { didSet { @@ -17,14 +19,16 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { } } - override init() { + init(theme: PresentationTheme) { + self.theme = theme + self.deleteButton = UIButton() self.forwardButton = UIButton() - self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: UIColor(0x007ee5)), for: [.normal]) - self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: UIColor(0xdededf)), for: [.disabled]) - self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: UIColor(0x007ee5)), for: [.normal]) - self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: UIColor(0xdededf)), for: [.disabled]) + self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) + self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) + self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) super.init() @@ -38,6 +42,17 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), for: [.touchUpInside]) } + func updateTheme(theme: PresentationTheme) { + if self.theme !== theme { + self.theme = theme + + self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) + self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) + self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) + } + } + @objc func deleteButtonPressed() { self.interfaceInteraction?.deleteSelectedMessages() } @@ -54,19 +69,9 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { if let channel = interfaceState.peer as? TelegramChannel { switch channel.info { case .broadcast: - switch channel.role { - case .creator, .editor, .moderator: - canDelete = true - case .member: - canDelete = false - } + canDelete = channel.hasAdminRights(.canDeleteMessages) case .group: - switch channel.role { - case .creator, .editor, .moderator: - canDelete = true - case .member: - canDelete = false - } + canDelete = channel.hasAdminRights(.canDeleteMessages) } } else { canDelete = true diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index 8b297e9f1c..2b41f05f45 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -5,8 +5,6 @@ import SwiftSignalKit import Postbox import TelegramCore -private let backgroundImage = generateStretchableFilledCircleImage(radius: 4.0, color: UIColor(0x748391, 0.45)) - class ChatMessageStickerItemNode: ChatMessageItemView { let imageNode: TransformImageNode var progressNode: RadialProgressNode? @@ -86,7 +84,25 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - let avatarInset: CGFloat = (item.peerId.isGroupOrChannel && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0 + let avatarInset: CGFloat + var hasAvatar = false + + if item.peerId.isGroupOrChannel && item.message.author != nil { + var isBroadcastChannel = false + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + isBroadcastChannel = true + } + + if !isBroadcastChannel { + hasAvatar = true + } + } + + if hasAvatar { + avatarInset = layoutConstants.avatarDiameter + } else { + avatarInset = 0.0 + } var layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) if dateHeaderAtBottom { @@ -105,14 +121,14 @@ class ChatMessageStickerItemNode: ChatMessageItemView { for attribute in item.message.attributes { if let replyAttribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[replyAttribute.messageId] { let availableWidth = max(60.0, width - imageSize.width - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) - replyInfoApply = makeReplyInfoLayout(item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) + replyInfoApply = makeReplyInfoLayout(item.theme, item.account, .standalone, replyMessage, CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude)) if let currentReplyBackgroundNode = currentReplyBackgroundNode { updatedReplyBackgroundNode = currentReplyBackgroundNode } else { updatedReplyBackgroundNode = ASImageNode() } - replyBackgroundImage = backgroundImage + replyBackgroundImage = PresentationResourcesChat.chatFreeformContentAdditionalInfoBackgroundImage(item.theme) break } } @@ -170,6 +186,12 @@ class ChatMessageStickerItemNode: ChatMessageItemView { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { case .tap: + if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { + if let item = self.item, let author = item.message.author { + self.controllerInteraction?.openPeer(author.id, .info, item.message.id) + } + return + } /*if let nameNode = self.nameNode, nameNode.frame.contains(location) { if let item = self.item { for attribute in item.message.attributes { diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index a99ec732bf..29cf79e3f4 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -11,6 +11,9 @@ private let messageFixedFont: UIFont = UIFont(name: "Menlo-Regular", size: 16.0) class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNode private let statusNode: ChatMessageDateAndStatusNode + private var linkHighlightingNode: LinkHighlightingNode? + + private var item: ChatMessageItem? required init() { self.textNode = TextNode() @@ -58,10 +61,17 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { sentViaBot = true } } - if let author = item.message.author as? TelegramUser, author.botInfo != nil { - sentViaBot = true + + var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + + if let author = item.message.author as? TelegramUser { + if author.botInfo != nil { + sentViaBot = true + } + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + dateText = "\(author.displayTitle), \(dateText)" + } } - let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) let statusType: ChatMessageDateAndStatusType? if case .None = position.bottom { @@ -84,7 +94,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var statusApply: ((Bool) -> Void)? if let statusType = statusType { - let (size, apply) = statusLayout(edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) + let (size, apply) = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) statusSize = size statusApply = apply } @@ -112,10 +122,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } } + + let bubbleTheme = item.theme.chat.bubble + if let entities = entities { - attributedText = stringWithAppliedEntities(message.text, entities: entities.entities, baseFont: messageFont, boldFont: messageBoldFont, fixedFont: messageFixedFont) + attributedText = stringWithAppliedEntities(message.text, entities: entities.entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: messageFont, boldFont: messageBoldFont, fixedFont: messageFixedFont) } else { - attributedText = NSAttributedString(string: message.text, font: messageFont, textColor: UIColor.black) + attributedText = NSAttributedString(string: message.text, font: messageFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor) } let (textLayout, textApply) = textLayout(attributedText, nil, 0, .end, textConstrainedSize, .natural, nil, UIEdgeInsets()) @@ -158,6 +171,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return (boundingSize, { [weak self] animation in if let strongSelf = self { + strongSelf.item = item let cachedLayout = strongSelf.textNode.cachedLayout if case .System = animation { @@ -228,19 +242,64 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.frame - let attributes = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) - if let url = attributes[TextNode.UrlAttribute] as? String { - return .url(url) - } else if let peerId = attributes[TextNode.TelegramPeerMentionAttribute] as? NSNumber { - return .peerMention(PeerId(peerId.int64Value)) - } else if let peerName = attributes[TextNode.TelegramPeerTextMentionAttribute] as? String { - return .textMention(peerName) - } else if let botCommand = attributes[TextNode.TelegramBotCommandAttribute] as? String { - return .botCommand(botCommand) - } else if let hashtag = attributes[TextNode.TelegramHashtagAttribute] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) + if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let url = attributes[TextNode.UrlAttribute] as? String { + return .url(url) + } else if let peerMention = attributes[TextNode.TelegramPeerMentionAttribute] as? TelegramPeerMention { + return .peerMention(peerMention.peerId, peerMention.mention) + } else if let peerName = attributes[TextNode.TelegramPeerTextMentionAttribute] as? String { + return .textMention(peerName) + } else if let botCommand = attributes[TextNode.TelegramBotCommandAttribute] as? String { + return .botCommand(botCommand) + } else if let hashtag = attributes[TextNode.TelegramHashtagAttribute] as? TelegramHashtag { + return .hashtag(hashtag.peerName, hashtag.hashtag) + } else { + return .none + } } else { return .none } } + + override func updateTouchesAtPoint(_ point: CGPoint?) { + if let item = self.item { + var rects: [CGRect]? + if let point = point { + let textNodeFrame = self.textNode.frame + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + let possibleNames: [String] = [ + TextNode.UrlAttribute, + TextNode.TelegramPeerMentionAttribute, + TextNode.TelegramPeerTextMentionAttribute, + TextNode.TelegramBotCommandAttribute, + TextNode.TelegramHashtagAttribute + ] + for name in possibleNames { + if let _ = attributes[name] { + rects = self.textNode.attributeRects(name: name, at: index) + break + } + } + } + } + + if let rects = rects { + let linkHighlightingNode: LinkHighlightingNode + if let current = self.linkHighlightingNode { + linkHighlightingNode = current + } else { + linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming ? item.theme.chat.bubble.incomingLinkHighlightColor : item.theme.chat.bubble.outgoingLinkHighlightColor) + self.linkHighlightingNode = linkHighlightingNode + self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode) + } + linkHighlightingNode.frame = self.textNode.frame + linkHighlightingNode.updateRects(rects) + } else if let linkHighlightingNode = self.linkHighlightingNode { + self.linkHighlightingNode = nil + linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in + linkHighlightingNode?.removeFromSupernode() + }) + } + } + } } diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 5d41ed0e66..779ea0d195 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -5,21 +5,6 @@ import AsyncDisplayKit import SwiftSignalKit import TelegramCore -private func generateLineImage(color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 2.0, height: 3.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(color.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 1.0), size: CGSize(width: 2.0, height: 2.0))) - })?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1) -} - -private let incomingLineImage = generateLineImage(color: UIColor(0x3ca7fe)) -private let outgoingLineImage = generateLineImage(color: UIColor(0x29cc10)) - -private let incomingAccentColor = UIColor(0x3ca7fe) -private let outgoingAccentColor = UIColor(0x00a700) - private let titleFont: UIFont = UIFont.boldSystemFont(ofSize: 15.0) private let textFont: UIFont = UIFont.systemFont(ofSize: 15.0) @@ -77,6 +62,8 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { return { item, layoutConstants, _, constrainedSize in let insets = UIEdgeInsets(top: 0.0, left: 9.0 + 8.0, bottom: 5.0, right: 8.0) + let incoming = item.message.effectivelyIncoming + var webPage: TelegramMediaWebpage? var webPageContent: TelegramMediaWebpageLoadedContent? for media in item.message.media { @@ -105,10 +92,17 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { sentViaBot = true } } - if let author = item.message.author as? TelegramUser, author.botInfo != nil { - sentViaBot = true + + var dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + + if let author = item.message.author as? TelegramUser { + if author.botInfo != nil { + sentViaBot = true + } + if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { + dateText = "\(author.displayTitle), \(dateText)" + } } - let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) var textString: NSAttributedString? var inlineImageDimensions: CGSize? @@ -123,24 +117,26 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { let string = NSMutableAttributedString() var notEmpty = false + let bubbleTheme = item.theme.chat.bubble + if let websiteName = webpage.websiteName, !websiteName.isEmpty { - string.append(NSAttributedString(string: websiteName, font: titleFont, textColor: item.message.effectivelyIncoming ? incomingAccentColor : outgoingAccentColor)) + string.append(NSAttributedString(string: websiteName, font: titleFont, textColor: incoming ? bubbleTheme.incomingAccentColor : bubbleTheme.outgoingAccentColor)) notEmpty = true } if let title = webpage.title, !title.isEmpty { if notEmpty { - string.append(NSAttributedString(string: "\n", font: textFont, textColor: UIColor.black)) + string.append(NSAttributedString(string: "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) } - string.append(NSAttributedString(string: title, font: titleFont, textColor: UIColor.black)) + string.append(NSAttributedString(string: title, font: titleFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) notEmpty = true } if let text = webpage.text, !text.isEmpty { if notEmpty { - string.append(NSAttributedString(string: "\n", font: textFont, textColor: UIColor.black)) + string.append(NSAttributedString(string: "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) } - string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: UIColor.black)) + string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor)) notEmpty = true } @@ -148,7 +144,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { if let file = webpage.file { if file.isVideo { - let (initialImageWidth, _, refineLayout) = contentImageLayout(item.account, item.message, file, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) + let (initialImageWidth, _, refineLayout) = contentImageLayout(item.account, item, file, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) initialWidth = initialImageWidth + insets.left + insets.right refineContentImageLayout = refineLayout } else { @@ -156,12 +152,12 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { if file.isVoice { automaticDownload = true } - let (_, refineLayout) = contentFileLayout(item.account, item.message, file, automaticDownload, item.message.effectivelyIncoming, nil, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) + let (_, refineLayout) = contentFileLayout(item.account, item, file, automaticDownload, item.message.effectivelyIncoming, nil, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) refineContentFileLayout = refineLayout } } else if let image = webpage.image { if let type = webpage.type, ["photo"].contains(type) { - let (initialImageWidth, _, refineLayout) = contentImageLayout(item.account, item.message, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) + let (initialImageWidth, _, refineLayout) = contentImageLayout(item.account, item, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) initialWidth = initialImageWidth + insets.left + insets.right refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { @@ -201,7 +197,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { var statusSizeAndApply: (CGSize, (Bool) -> Void)? if refineContentImageLayout == nil && refineContentFileLayout == nil { - statusSizeAndApply = statusLayout(edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) + statusSizeAndApply = statusLayout(item.theme, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) } let (textLayout, textApply) = textAsyncLayout(textString, nil, 12, .end, textConstrainedSize, .natural, textCutout, UIEdgeInsets()) @@ -234,7 +230,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top) - let lineImage = item.message.effectivelyIncoming ? incomingLineImage : outgoingLineImage + let lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(item.theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(item.theme) var boundingSize = textFrame.size if let statusFrame = statusFrame { diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index fb718743b4..4720356b93 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -7,14 +7,23 @@ final class ChatPanelInterfaceInteractionStatuses { let editingMessage: Signal let startingBot: Signal let unblockingPeer: Signal + let searching: Signal + let loadingMessage: Signal - init(editingMessage: Signal, startingBot: Signal, unblockingPeer: Signal) { + init(editingMessage: Signal, startingBot: Signal, unblockingPeer: Signal, searching: Signal, loadingMessage: Signal) { self.editingMessage = editingMessage self.startingBot = startingBot self.unblockingPeer = unblockingPeer + self.searching = searching + self.loadingMessage = loadingMessage } } +enum ChatPanelSearchNavigationAction { + case earlier + case later +} + final class ChatPanelInterfaceInteraction { let setupReplyMessage: (MessageId) -> Void let setupEditMessage: (MessageId) -> Void @@ -25,6 +34,10 @@ final class ChatPanelInterfaceInteraction { let updateInputModeAndDismissedButtonKeyboardMessageId: ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void let editMessage: (MessageId, String) -> Void let beginMessageSearch: () -> Void + let dismissMessageSearch: () -> Void + let updateMessageSearch: (String) -> Void + let navigateMessageSearch: (ChatPanelSearchNavigationAction) -> Void + let openCalendarSearch: () -> Void let navigateToMessage: (MessageId) -> Void let openPeerInfo: () -> Void let togglePeerNotifications: () -> Void @@ -44,7 +57,7 @@ final class ChatPanelInterfaceInteraction { let deleteChat: () -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, dismissMessageSearch: @escaping () -> Void, updateMessageSearch: @escaping (String) -> Void, navigateMessageSearch: @escaping (ChatPanelSearchNavigationAction) -> Void, openCalendarSearch: @escaping () -> Void, navigateToMessage: @escaping (MessageId) -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, sendBotStart: @escaping (String?) -> Void, botSwitchChatWithPayload: @escaping (PeerId, String) -> Void, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId) -> Void, unpinMessage: @escaping () -> Void, reportPeer: @escaping () -> Void, dismissReportPeer: @escaping () -> Void, deleteChat: @escaping () -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection @@ -54,6 +67,10 @@ final class ChatPanelInterfaceInteraction { self.updateInputModeAndDismissedButtonKeyboardMessageId = updateInputModeAndDismissedButtonKeyboardMessageId self.editMessage = editMessage self.beginMessageSearch = beginMessageSearch + self.dismissMessageSearch = dismissMessageSearch + self.updateMessageSearch = updateMessageSearch + self.navigateMessageSearch = navigateMessageSearch + self.openCalendarSearch = openCalendarSearch self.navigateToMessage = navigateToMessage self.openPeerInfo = openPeerInfo self.togglePeerNotifications = togglePeerNotifications diff --git a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift index 83b87c64e1..53ca90329b 100644 --- a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift +++ b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift @@ -5,20 +5,6 @@ import Postbox import TelegramCore import SwiftSignalKit -private let lineImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(0x007ee5)) -private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x9099A2).cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - context.move(to: CGPoint(x: 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) - context.strokePath() - context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) - context.strokePath() -}) - final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { private let account: Account private let tapButton: HighlightTrackingButtonNode @@ -42,18 +28,15 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.tapButton = HighlightTrackingButtonNode() self.closeButton = HighlightableButtonNode() - self.closeButton.setImage(closeButtonImage, for: []) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.closeButton.displaysAsynchronously = false self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) self.separatorNode.isLayerBacked = true self.lineNode = ASImageNode() self.lineNode.displayWithoutProcessing = true self.lineNode.displaysAsynchronously = false - self.lineNode.image = lineImage self.titleNode = TextNode() self.titleNode.displaysAsynchronously = true @@ -95,8 +78,6 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside]) self.addSubnode(self.tapButton) - self.backgroundColor = UIColor(0xF5F6F8) - self.addSubnode(self.separatorNode) } @@ -104,9 +85,19 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.disposable.dispose() } + private var theme: PresentationTheme? + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { let panelHeight: CGFloat = 44.0 + if self.theme !== interfaceState.theme { + self.theme = interfaceState.theme + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(interfaceState.theme), for: []) + self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(interfaceState.theme) + self.backgroundColor = interfaceState.theme.rootController.navigationBar.backgroundColor + self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor + } + if self.currentMessageId != interfaceState.pinnedMessageId { self.currentMessageId = interfaceState.pinnedMessageId if let pinnedMessageId = interfaceState.pinnedMessageId { @@ -115,7 +106,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { if let strongSelf = self, let message = view.message { strongSelf.currentMessage = message if let currentLayout = strongSelf.currentLayout { - strongSelf.enqueueTransition(width: currentLayout, transition: .immediate, message: message) + strongSelf.enqueueTransition(width: currentLayout, transition: .immediate, message: message, theme: interfaceState.theme, strings: interfaceState.strings) } } })) @@ -137,14 +128,14 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { self.currentLayout = width if let currentMessage = self.currentMessage { - self.enqueueTransition(width: width, transition: .immediate, message: currentMessage) + self.enqueueTransition(width: width, transition: .immediate, message: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings) } } return panelHeight } - private func enqueueTransition(width: CGFloat, transition: ContainedViewLayoutTransition, message: Message) { + private func enqueueTransition(width: CGFloat, transition: ContainedViewLayoutTransition, message: Message, theme: PresentationTheme, strings: PresentationStrings) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) @@ -154,9 +145,9 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let rightInset: CGFloat = 18.0 let textRightInset: CGFloat = 25.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: "Pinned message", font: Font.medium(15.0), textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: strings.Conversation_PinnedMessage, font: Font.medium(15.0), textColor: theme.chat.inputPanel.panelControlAccentColor), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: message.text, font: Font.regular(15.0), textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: message.text, font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) Queue.mainQueue().async { if let strongSelf = self { diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index e1936f2c7a..3fa98cef9d 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -166,6 +166,49 @@ enum ChatTitlePanelContext: Comparable { } } +struct ChatSearchResultsState: Equatable { + let messageIds: [MessageId] + let currentId: MessageId? + + static func ==(lhs: ChatSearchResultsState, rhs: ChatSearchResultsState) -> Bool { + if lhs.messageIds != rhs.messageIds { + return false + } + if lhs.currentId != rhs.currentId { + return false + } + return false + } +} + +struct ChatSearchData: Equatable { + let query: String + let resultsState: ChatSearchResultsState? + + init(query: String = "", resultsState: ChatSearchResultsState? = nil) { + self.query = query + self.resultsState = resultsState + } + + static func ==(lhs: ChatSearchData, rhs: ChatSearchData) -> Bool { + if lhs.query != rhs.query { + return false + } + if lhs.resultsState != rhs.resultsState { + return false + } + return true + } + + func withUpdatedQuery(_ query: String) -> ChatSearchData { + return ChatSearchData(query: query, resultsState: self.resultsState) + } + + func withUpdatedResultsState(_ resultsState: ChatSearchResultsState?) -> ChatSearchData { + return ChatSearchData(query: self.query, resultsState: resultsState) + } +} + struct ChatPresentationInterfaceState: Equatable { let interfaceState: ChatInterfaceState let peer: Peer? @@ -180,8 +223,12 @@ struct ChatPresentationInterfaceState: Equatable { let chatHistoryState: ChatHistoryNodeHistoryState? let botStartPayload: String? let urlPreview: (String, TelegramMediaWebpage)? + let search: ChatSearchData? + let chatWallpaper: TelegramWallpaper + let theme: PresentationTheme + let strings: PresentationStrings - init() { + init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings) { self.interfaceState = ChatInterfaceState() self.inputTextPanelState = ChatTextInputPanelState() self.peer = nil @@ -195,9 +242,13 @@ struct ChatPresentationInterfaceState: Equatable { self.chatHistoryState = nil self.botStartPayload = nil self.urlPreview = nil + self.search = nil + self.chatWallpaper = chatWallpaper + self.theme = theme + self.strings = strings } - init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, peerIsBlocked: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?) { + init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, peerIsBlocked: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings) { self.interfaceState = interfaceState self.peer = peer self.inputTextPanelState = inputTextPanelState @@ -211,6 +262,10 @@ struct ChatPresentationInterfaceState: Equatable { self.chatHistoryState = chatHistoryState self.botStartPayload = botStartPayload self.urlPreview = urlPreview + self.search = search + self.chatWallpaper = chatWallpaper + self.theme = theme + self.strings = strings } static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { @@ -283,58 +338,78 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if lhs.search != rhs.search { + return false + } + + if lhs.chatWallpaper != rhs.chatWallpaper { + return false + } + + if lhs.theme !== rhs.theme { + return false + } + + if lhs.strings !== rhs.strings { + return false + } + return true } func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedPeer(_ f: (Peer?) -> Peer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedInputQueryResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: f(self.inputQueryResult), inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: f(self.inputQueryResult), inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedPeerIsBlocked(_ peerIsBlocked: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedCanReportPeer(_ canReportPeer: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedBotStartPayload(_ botStartPayload: String?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedChatHistoryState(_ chatHistoryState: ChatHistoryNodeHistoryState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } func updatedUrlPreview(_ urlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, search: self.search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) + } + + func updatedSearch(_ search: ChatSearchData?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, search: search, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings) } } diff --git a/TelegramUI/ChatReportPeerTitlePanelNode.swift b/TelegramUI/ChatReportPeerTitlePanelNode.swift index 30c3bddc24..3d02eaacf4 100644 --- a/TelegramUI/ChatReportPeerTitlePanelNode.swift +++ b/TelegramUI/ChatReportPeerTitlePanelNode.swift @@ -6,7 +6,7 @@ import TelegramCore private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x9099A2).cgColor) + context.setStrokeColor(UIColor(rgb: 0x9099A2).cgColor) context.setLineWidth(2.0) context.setLineCap(.round) context.move(to: CGPoint(x: 1.0, y: 1.0)) @@ -51,7 +51,7 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { super.init() - self.backgroundColor = UIColor(0xF5F6F8) + self.backgroundColor = UIColor(rgb: 0xF5F6F8) self.addSubnode(self.separatorNode) @@ -95,8 +95,8 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode { let view = UIButton() view.setTitle(button.title, for: []) view.titleLabel?.font = Font.regular(16.0) - view.setTitleColor(UIColor(0x007ee5), for: []) - view.setTitleColor(UIColor(0x007ee5).withAlphaComponent(0.7), for: [.highlighted]) + view.setTitleColor(UIColor(rgb: 0x007ee5), for: []) + view.setTitleColor(UIColor(rgb: 0x007ee5).withAlphaComponent(0.7), for: [.highlighted]) view.addTarget(self, action: #selector(self.buttonPressed(_:)), for: [.touchUpInside]) self.view.addSubview(view) self.buttons.append((button, view)) diff --git a/TelegramUI/ChatRequestInProgressTitlePanelNode.swift b/TelegramUI/ChatRequestInProgressTitlePanelNode.swift index 869599a809..328e7c0e5e 100644 --- a/TelegramUI/ChatRequestInProgressTitlePanelNode.swift +++ b/TelegramUI/ChatRequestInProgressTitlePanelNode.swift @@ -17,7 +17,7 @@ final class ChatRequestInProgressTitlePanelNode: ChatTitleAccessoryPanelNode { super.init() - self.backgroundColor = UIColor(0xF5F6F8) + self.backgroundColor = UIColor(rgb: 0xF5F6F8) self.addSubnode(self.titleNode) self.addSubnode(self.separatorNode) diff --git a/TelegramUI/ChatSearchInputPanelNode.swift b/TelegramUI/ChatSearchInputPanelNode.swift new file mode 100644 index 0000000000..5c029e3f0c --- /dev/null +++ b/TelegramUI/ChatSearchInputPanelNode.swift @@ -0,0 +1,130 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +private let labelFont = Font.regular(15.0) + +final class ChatSearchInputPanelNode: ChatInputPanelNode { + private let upButton: HighlightableButtonNode + private let downButton: HighlightableButtonNode + private let calendarButton: HighlightableButtonNode + private let resultsLabel: TextNode + private let activityIndicator: ActivityIndicator + + private var presentationInterfaceState: ChatPresentationInterfaceState? + + private let activityDisposable = MetaDisposable() + private var displayActivity = false + + override var interfaceInteraction: ChatPanelInterfaceInteraction? { + didSet { + if let statuses = self.interfaceInteraction?.statuses { + self.activityDisposable.set((combineLatest((statuses.searching |> deliverOnMainQueue), (statuses.loadingMessage |> deliverOnMainQueue))).start(next: { [weak self] searching, loadingMessage in + let value = searching || loadingMessage + if let strongSelf = self, strongSelf.displayActivity != value { + strongSelf.displayActivity = value + strongSelf.activityIndicator.isHidden = !value + if let interfaceState = strongSelf.presentationInterfaceState { + strongSelf.calendarButton.isHidden = !((interfaceState.search?.query.isEmpty ?? true)) || strongSelf.displayActivity + } + } + })) + } else { + self.activityDisposable.set(nil) + } + } + } + + init(theme: PresentationTheme) { + self.upButton = HighlightableButtonNode() + self.upButton.isEnabled = false + self.downButton = HighlightableButtonNode() + self.downButton.isEnabled = false + self.calendarButton = HighlightableButtonNode() + self.resultsLabel = TextNode() + self.resultsLabel.isLayerBacked = true + self.resultsLabel.displaysAsynchronously = false + self.activityIndicator = ActivityIndicator(theme: theme) + self.activityIndicator.isHidden = true + + super.init() + + self.addSubnode(self.upButton) + self.addSubnode(self.downButton) + self.addSubnode(self.calendarButton) + self.addSubnode(self.resultsLabel) + self.addSubnode(self.activityIndicator) + + self.upButton.addTarget(self, action: #selector(self.upPressed), forControlEvents: [.touchUpInside]) + self.downButton.addTarget(self, action: #selector(self.downPressed), forControlEvents: [.touchUpInside]) + self.calendarButton.addTarget(self, action: #selector(self.calendarPressed), forControlEvents: [.touchUpInside]) + } + + deinit { + self.activityDisposable.dispose() + } + + @objc func upPressed() { + self.interfaceInteraction?.navigateMessageSearch(.earlier) + } + + @objc func downPressed() { + self.interfaceInteraction?.navigateMessageSearch(.later) + } + + @objc func calendarPressed() { + self.interfaceInteraction?.openCalendarSearch() + } + + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + if self.presentationInterfaceState != interfaceState { + let themeUpdated = self.presentationInterfaceState?.theme !== interfaceState.theme + + self.presentationInterfaceState = interfaceState + + if themeUpdated { + self.upButton.setImage(PresentationResourcesChat.chatInputSearchPanelUpImage(interfaceState.theme), for: [.normal]) + self.upButton.setImage(PresentationResourcesChat.chatInputSearchPanelUpDisabledImage(interfaceState.theme), for: [.disabled]) + self.downButton.setImage(PresentationResourcesChat.chatInputSearchPanelDownImage(interfaceState.theme), for: [.normal]) + self.downButton.setImage(PresentationResourcesChat.chatInputSearchPanelDownDisabledImage(interfaceState.theme), for: [.disabled]) + self.calendarButton.setImage(PresentationResourcesChat.chatInputSearchPanelCalendarImage(interfaceState.theme), for: []) + } + } + + let panelHeight: CGFloat = 47.0 + + transition.updateFrame(node: self.downButton, frame: CGRect(origin: CGPoint(x: 12.0, y: 0.0), size: CGSize(width: 40.0, height: panelHeight))) + transition.updateFrame(node: self.upButton, frame: CGRect(origin: CGPoint(x: 12.0 + 43.0, y: 0.0), size: CGSize(width: 40.0, height: panelHeight))) + transition.updateFrame(node: self.calendarButton, frame: CGRect(origin: CGPoint(x: width - 60.0, y: 0.0), size: CGSize(width: 60.0, height: panelHeight))) + + var resultIndex: Int? + var resultCount: Int? + var resultsText: NSAttributedString? + if let results = interfaceState.search?.resultsState { + resultCount = results.messageIds.count + if let currentId = results.currentId, let index = results.messageIds.index(of: currentId) { + resultIndex = index + resultsText = NSAttributedString(string: "\(index + 1) \(interfaceState.strings.Common_of) \(results.messageIds.count)", font: labelFont, textColor: interfaceState.theme.chat.inputPanel.primaryTextColor) + } else { + resultsText = NSAttributedString(string: interfaceState.strings.Conversation_SearchNoResults, font: labelFont, textColor: interfaceState.theme.chat.inputPanel.primaryTextColor) + } + } + + self.upButton.isEnabled = resultIndex != nil && resultIndex != 0 + self.downButton.isEnabled = resultIndex != nil && resultCount != nil && resultIndex != resultCount! - 1 + self.calendarButton.isHidden = (!(interfaceState.search?.query.isEmpty ?? true)) || self.displayActivity + + let makeLabelLayout = TextNode.asyncLayout(self.resultsLabel) + let (labelSize, labelApply) = makeLabelLayout(resultsText, nil, 1, .end, CGSize(width: 200.0, height: 100.0), .left, nil, UIEdgeInsets()) + let _ = labelApply() + self.resultsLabel.frame = CGRect(origin: CGPoint(x: 105.0, y: floor((panelHeight - labelSize.size.height) / 2.0)), size: labelSize.size) + + let indicatorSize = self.activityIndicator.measure(CGSize(width: 22.0, height: 22.0)) + self.activityIndicator.frame = CGRect(origin: CGPoint(x: width - 41.0, y: floor((panelHeight - indicatorSize.height) / 2.0)), size: indicatorSize) + + return panelHeight + } +} diff --git a/TelegramUI/ChatSearchNavigationContentNode.swift b/TelegramUI/ChatSearchNavigationContentNode.swift new file mode 100644 index 0000000000..fe6ce3bd73 --- /dev/null +++ b/TelegramUI/ChatSearchNavigationContentNode.swift @@ -0,0 +1,47 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let searchBarFont = Font.regular(15.0) + +final class ChatSearchNavigationContentNode: NavigationBarContentNode { + private let searchBar: SearchBarNode + private let interaction: ChatPanelInterfaceInteraction + + init(theme: PresentationTheme, strings: PresentationStrings, interaction: ChatPanelInterfaceInteraction) { + self.interaction = interaction + + self.searchBar = SearchBarNode(theme: theme, strings: strings) + self.searchBar.placeholderString = NSAttributedString(string: strings.Conversation_SearchPlaceholder, font: searchBarFont, textColor: theme.rootController.activeNavigationSearchBar.inputPlaceholderTextColor) + + super.init() + + self.addSubnode(self.searchBar) + + self.searchBar.cancel = { [weak self] in + self?.searchBar.deactivate(clear: false) + self?.interaction.dismissMessageSearch() + } + + self.searchBar.textUpdated = { [weak self] query in + self?.interaction.updateMessageSearch(query) + } + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - 64.0), size: CGSize(width: size.width, height: 64.0)) + self.searchBar.frame = searchBarFrame + } + + func activate() { + self.searchBar.activate() + } + + func deactivate() { + self.searchBar.deactivate(clear: false) + } +} diff --git a/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift b/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift index d1e8d1d654..517d165b25 100644 --- a/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift +++ b/TelegramUI/ChatSecretAutoremoveTimerActionSheet.swift @@ -6,12 +6,18 @@ import SwiftSignalKit import Photos final class ChatSecretAutoremoveTimerActionSheetController: ActionSheetController { + private let theme: PresentationTheme + private let strings: PresentationStrings + private let _ready = Promise() override var ready: Promise { return self._ready } - init(currentValue: Int32, applyValue: @escaping (Int32) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, currentValue: Int32, applyValue: @escaping (Int32) -> Void) { + self.theme = theme + self.strings = strings + super.init() self._ready.set(.single(true)) @@ -19,18 +25,16 @@ final class ChatSecretAutoremoveTimerActionSheetController: ActionSheetControlle var updatedValue = currentValue self.setItemGroups([ ActionSheetItemGroup(items: [ - AutoremoveTimeoutSelectorItem(currentValue: currentValue, valueChanged: { value in + AutoremoveTimeoutSelectorItem(theme: theme, strings: strings, currentValue: currentValue, valueChanged: { value in updatedValue = value }), - ActionSheetButtonItem(title: "Set", action: { [weak self] in - if let strongSelf = self { - self?.dismissAnimated() - } + ActionSheetButtonItem(title: strings.Wallpaper_Set, action: { [weak self] in + self?.dismissAnimated() applyValue(updatedValue) }) ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: "Cancel", action: { [weak self] in + ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in self?.dismissAnimated() }), ]) @@ -43,51 +47,61 @@ final class ChatSecretAutoremoveTimerActionSheetController: ActionSheetControlle } private final class AutoremoveTimeoutSelectorItem: ActionSheetItem { + let theme: PresentationTheme + let strings: PresentationStrings + let currentValue: Int32 let valueChanged: (Int32) -> Void - init(currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { + self.theme = theme + self.strings = strings self.currentValue = currentValue self.valueChanged = valueChanged } func node() -> ActionSheetItemNode { - return AutoremoveTimeoutSelectorItemNode(currentValue: self.currentValue, valueChanged: self.valueChanged) + return AutoremoveTimeoutSelectorItemNode(theme: self.theme, strings: self.strings, currentValue: self.currentValue, valueChanged: self.valueChanged) } func updateNode(_ node: ActionSheetItemNode) { } } -private let timeoutValues: [(Int32, String)] = [ - (0, "Off"), - (1, "1 second"), - (2, "2 seconds"), - (3, "3 seconds"), - (4, "4 seconds"), - (5, "5 seconds"), - (6, "6 seconds"), - (7, "7 seconds"), - (8, "8 seconds"), - (9, "9 seconds"), - (10, "10 seconds"), - (11, "11 seconds"), - (12, "12 seconds"), - (13, "13 seconds"), - (14, "14 seconds"), - (15, "15 seconds"), - (30, "30 seconds"), - (1 * 60, "1 minute"), - (1 * 60 * 60, "1 hour"), - (24 * 60 * 60, "1 day"), - (7 * 24 * 60 * 60, "1 week"), +private let timeoutValues: [Int32] = [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 30, + 1 * 60, + 1 * 60 * 60, + 24 * 60 * 60, + 7 * 24 * 60 * 60 ] private final class AutoremoveTimeoutSelectorItemNode: ActionSheetItemNode, UIPickerViewDelegate, UIPickerViewDataSource { + private let theme: PresentationTheme + private let strings: PresentationStrings + private let valueChanged: (Int32) -> Void private let pickerView: UIPickerView - init(currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, currentValue: Int32, valueChanged: @escaping (Int32) -> Void) { + self.theme = theme + self.strings = strings self.valueChanged = valueChanged self.pickerView = UIPickerView() @@ -101,7 +115,7 @@ private final class AutoremoveTimeoutSelectorItemNode: ActionSheetItemNode, UIPi self.pickerView.reloadAllComponents() var index: Int = 0 for i in 0 ..< timeoutValues.count { - if currentValue <= timeoutValues[i].0 { + if currentValue <= timeoutValues[i] { index = i break } @@ -122,11 +136,11 @@ private final class AutoremoveTimeoutSelectorItemNode: ActionSheetItemNode, UIPi } func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { - return NSAttributedString(string: timeoutValues[row].1, font: Font.medium(15.0), textColor: UIColor.black) + return NSAttributedString(string: timeIntervalString(strings: self.strings, value: timeoutValues[row]), font: Font.medium(15.0), textColor: UIColor.black) } func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - self.valueChanged(timeoutValues[row].0) + self.valueChanged(timeoutValues[row]) } override func layout() { diff --git a/TelegramUI/ChatTextInputAudioRecordingButton.swift b/TelegramUI/ChatTextInputAudioRecordingButton.swift index 166808d04c..c67f4e8205 100644 --- a/TelegramUI/ChatTextInputAudioRecordingButton.swift +++ b/TelegramUI/ChatTextInputAudioRecordingButton.swift @@ -4,7 +4,6 @@ import AsyncDisplayKit import TelegramCore import SwiftSignalKit -private let micIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: UIColor(0x9099A2)) private let offsetThreshold: CGFloat = 10.0 private let dismissOffsetThreshold: CGFloat = 70.0 @@ -28,7 +27,9 @@ final class ChatTextInputAudioRecordingButton: UIButton { } if let audioRecorder = self.audioRecorder { self.micLevelDisposable?.set(audioRecorder.micLevel.start(next: { [weak self] level in - self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level)) + Queue.mainQueue().async { + self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level)) + } })) } else { self.micLevelDisposable?.set(nil) @@ -41,7 +42,6 @@ final class ChatTextInputAudioRecordingButton: UIButton { super.init(frame: CGRect()) self.isExclusiveTouch = true - self.setImage(micIcon, for: []) self.adjustsImageWhenHighlighted = false self.adjustsImageWhenDisabled = false self.disablesInteractiveTransitionGestureRecognizer = true @@ -51,6 +51,10 @@ final class ChatTextInputAudioRecordingButton: UIButton { fatalError("init(coder:) has not been implemented") } + func updateTheme(theme: PresentationTheme) { + self.setImage(PresentationResourcesChat.chatInputPanelVoiceButtonImage(theme), for: []) + } + deinit { if let micLevelDisposable = self.micLevelDisposable { micLevelDisposable.dispose() @@ -78,7 +82,7 @@ final class ChatTextInputAudioRecordingButton: UIButton { recordingOverlay = ChatTextInputAudioRecordingOverlay(anchorView: self) self.recordingOverlay = recordingOverlay } - if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext, let topWindow = applicationContext.getTopWindow() { + if let account = self.account, let applicationContext = account.applicationContext as? TelegramApplicationContext, let topWindow = applicationContext.applicationBindings.getTopWindow() { recordingOverlay.present(in: topWindow) } return true diff --git a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift index 06c01b1219..1e05311d40 100644 --- a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift +++ b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift @@ -2,18 +2,16 @@ import Foundation import AsyncDisplayKit import Display -private let arrowImage = UIImage(bundleImageName: "Chat/Input/Text/AudioRecordingCancelArrow")?.precomposed() - final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { private let arrowNode: ASImageNode private let labelNode: TextNode - override init() { + init(theme: PresentationTheme, strings: PresentationStrings) { self.arrowNode = ASImageNode() self.arrowNode.isLayerBacked = true self.arrowNode.displayWithoutProcessing = true self.arrowNode.displaysAsynchronously = false - self.arrowNode.image = arrowImage + self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme) self.labelNode = TextNode() self.labelNode.isLayerBacked = true @@ -24,13 +22,17 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { self.addSubnode(self.labelNode) let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: "Slide to cancel", font: Font.regular(14.0), textColor: UIColor(0xaaaab2)), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil, UIEdgeInsets()) - labelApply() + let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: strings.Conversation_SlideToCancel, font: Font.regular(14.0), textColor: theme.chat.inputPanel.panelControlColor), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil, UIEdgeInsets()) + let _ = labelApply() - let arrowSize = arrowImage?.size ?? CGSize() + let arrowSize = self.arrowNode.image?.size ?? CGSize() let height = max(arrowSize.height, labelLayout.size.height) self.frame = CGRect(origin: CGPoint(), size: CGSize(width: arrowSize.width + 12.0 + labelLayout.size.width, height: height)) self.arrowNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - arrowSize.height) / 2.0)), size: arrowSize) self.labelNode.frame = CGRect(origin: CGPoint(x: arrowSize.width + 6.0, y: floor((height - labelLayout.size.height) / 2.0) - UIScreenPixel), size: labelLayout.size) } + + func updateTheme(theme: PresentationTheme) { + self.arrowNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingCancelArrowImage(theme) + } } diff --git a/TelegramUI/ChatTextInputAudioRecordingOverlayButton.swift b/TelegramUI/ChatTextInputAudioRecordingOverlayButton.swift index 7a643b17c5..5fe4c55ec9 100644 --- a/TelegramUI/ChatTextInputAudioRecordingOverlayButton.swift +++ b/TelegramUI/ChatTextInputAudioRecordingOverlayButton.swift @@ -6,8 +6,8 @@ import UIKit private let innerCircleDiameter: CGFloat = 110.0 private let outerCircleDiameter = innerCircleDiameter + 50.0 private let outerCircleMinScale = innerCircleDiameter / outerCircleDiameter -private let innerCircleImage = generateFilledCircleImage(diameter: innerCircleDiameter, color: UIColor(0x007ee5)) -private let outerCircleImage = generateFilledCircleImage(diameter: outerCircleDiameter, color: UIColor(0x007ee5, 0.2)) +private let innerCircleImage = generateFilledCircleImage(diameter: innerCircleDiameter, color: UIColor(rgb: 0x007ee5)) +private let outerCircleImage = generateFilledCircleImage(diameter: outerCircleDiameter, color: UIColor(rgb: 0x007ee5, alpha: 0.2)) private let micIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: .white)! private final class ChatTextInputAudioRecordingOverlayDisplayLinkTarget: NSObject { diff --git a/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift b/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift index cf6f4ecdb9..d8f1bdcb14 100644 --- a/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift +++ b/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift @@ -5,9 +5,12 @@ import SwiftSignalKit private final class ChatTextInputAudioRecordingTimeNodeParameters: NSObject { let timestamp: Double + let theme: PresentationTheme - init(timestamp: Double) { + init(timestamp: Double, theme: PresentationTheme) { self.timestamp = timestamp + self.theme = theme + super.init() } } @@ -45,7 +48,11 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { } } - override init() { + private var theme: PresentationTheme + + init(theme: PresentationTheme) { + self.theme = theme + self.textNode = TextNode() super.init() self.isOpaque = false @@ -55,16 +62,22 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { self.stateDisposable.dispose() } + func updateTheme(theme: PresentationTheme) { + self.theme = theme + + self.setNeedsDisplay() + } + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { let makeLayout = TextNode.asyncLayout(self.textNode) - let (size, apply) = makeLayout(NSAttributedString(string: "00:00,00", font: Font.regular(15.0), textColor: .black), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil, UIEdgeInsets()) - apply() + let (size, apply) = makeLayout(NSAttributedString(string: "00:00,00", font: Font.regular(15.0), textColor: theme.chat.inputPanel.primaryTextColor), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil, UIEdgeInsets()) + let _ = apply() self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 1.0 + UIScreenPixel), size: size.size) return size.size } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return ChatTextInputAudioRecordingTimeNodeParameters(timestamp: self.timestamp) + return ChatTextInputAudioRecordingTimeNodeParameters(timestamp: self.timestamp, theme: self.theme) } @objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: () -> Bool, isRasterizing: Bool) { @@ -80,7 +93,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { let currentAudioDurationSeconds = Int(parameters.timestamp) let currentAudioDurationMilliseconds = Int(parameters.timestamp * 100.0) % 100 let text = String(format: "%d:%02d,%02d", currentAudioDurationSeconds / 60, currentAudioDurationSeconds % 60, currentAudioDurationMilliseconds) - let string = NSAttributedString(string: text, font: textFont, textColor: .black) + let string = NSAttributedString(string: text, font: textFont, textColor: parameters.theme.chat.inputPanel.primaryTextColor) string.draw(at: CGPoint()) } } diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 7fae97badc..95d25a0f2a 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -5,52 +5,9 @@ import AsyncDisplayKit import Postbox import TelegramCore -private let textInputViewBackground: UIImage = { - let diameter: CGFloat = 35.0 - UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), true, 0.0) - let context = UIGraphicsGetCurrentContext()! - context.setFillColor(UIColor(0xF5F6F8).cgColor) - context.fill(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - context.setFillColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - context.setStrokeColor(UIColor(0xC9CDD1).cgColor) - let strokeWidth: CGFloat = 0.5 - context.setLineWidth(strokeWidth) - context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth)) - let image = UIGraphicsGetImageFromCurrentImageContext()!.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) - UIGraphicsEndImageContext() - - return image -}() - -private let searchLayoutClearButtonImage = generateImage(CGSize(width: 14.0, height: 14.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0x9099A2, 0.6).cgColor) - context.setStrokeColor(UIColor.white.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(1.5) - context.setLineCap(.round) - context.translateBy(x: size.width / 2.0, y: size.height / 2.0) - context.rotate(by: CGFloat(M_PI / 4)) - context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - - let lineHeight: CGFloat = 7.0 - - context.beginPath() - context.move(to: CGPoint(x: size.width / 2.0, y: (size.width - lineHeight) / 2.0)) - context.addLine(to: CGPoint(x: size.width / 2.0, y: (size.width - lineHeight) / 2.0 + lineHeight)) - context.strokePath() - - context.beginPath() - context.move(to: CGPoint(x: (size.width - lineHeight) / 2.0, y: size.width / 2.0)) - context.addLine(to: CGPoint(x: (size.width - lineHeight) / 2.0 + lineHeight, y: size.width / 2.0)) - context.strokePath() -}) - private let searchLayoutProgressImage = generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x9099A2, 0.6).cgColor) + context.setStrokeColor(UIColor(rgb: 0x9099A2, alpha: 0.6).cgColor) let lineWidth: CGFloat = 2.0 let cutoutWidth: CGFloat = 4.0 @@ -60,9 +17,6 @@ private let searchLayoutProgressImage = generateImage(CGSize(width: 22.0, height context.clear(CGRect(origin: CGPoint(x: (size.width - cutoutWidth) / 2.0, y: 0.0), size: CGSize(width: cutoutWidth, height: size.height / 2.0))) }) -private let attachmentIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconAttachment"), color: UIColor(0x9099A2)) -private let sendIcon = UIImage(bundleImageName: "Chat/Input/Text/IconSend")?.precomposed() - enum ChatTextInputAccessoryItem: Equatable { case keyboard case stickers @@ -148,39 +102,51 @@ struct ChatTextInputPanelState: Equatable { } } -private let keyboardImage = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconKeyboard")?.precomposed() -private let stickersImage = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconStickers")?.precomposed() -private let inputButtonsImage = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconInputButtons")?.precomposed() -private let audioRecordingDotImage = generateFilledCircleImage(diameter: 9.0, color: UIColor(0xed2521)) -private let timerImage = UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconTimer")?.precomposed() - private final class AccessoryItemIconButton: HighlightableButton { private let item: ChatTextInputAccessoryItem - init(item: ChatTextInputAccessoryItem) { + init(item: ChatTextInputAccessoryItem, theme: PresentationTheme) { self.item = item super.init(frame: CGRect()) switch item { case .keyboard: - self.setImage(keyboardImage, for: []) + self.setImage(PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), for: []) case .stickers: - self.setImage(stickersImage, for: []) + self.setImage(PresentationResourcesChat.chatInputTextFieldStickersImage(theme), for: []) case .inputButtons: - self.setImage(inputButtonsImage, for: []) + self.setImage(PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), for: []) case let .messageAutoremoveTimeout(timeout): if let timeout = timeout { self.setImage(nil, for: []) self.titleLabel?.font = Font.regular(12.0) - self.setTitleColor(UIColor.lightGray, for: []) + self.setTitleColor(theme.chat.inputPanel.inputControlColor, for: []) self.setTitle("\(timeout)s", for: []) } else { - self.setImage(timerImage, for: []) + self.setImage(PresentationResourcesChat.chatInputTextFieldTimerImage(theme), for: []) } } - - //self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5) + } + + func updateTheme(theme: PresentationTheme) { + switch self.item { + case .keyboard: + self.setImage(PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), for: []) + case .stickers: + self.setImage(PresentationResourcesChat.chatInputTextFieldStickersImage(theme), for: []) + case .inputButtons: + self.setImage(PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), for: []) + case let .messageAutoremoveTimeout(timeout): + if let timeout = timeout { + self.setImage(nil, for: []) + self.titleLabel?.font = Font.regular(12.0) + self.setTitleColor(theme.chat.inputPanel.inputControlColor, for: []) + self.setTitle("\(timeout)s", for: []) + } else { + self.setImage(PresentationResourcesChat.chatInputTextFieldTimerImage(theme), for: []) + } + } } required init?(coder aDecoder: NSCoder) { @@ -225,11 +191,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var currentPlaceholder: String? - private var presentationInterfaceState = ChatPresentationInterfaceState() + private var presentationInterfaceState: ChatPresentationInterfaceState? private var keepSendButtonEnabled = false private var extendedSearchLayout = false + private var theme: PresentationTheme? + var inputTextState: ChatTextInputState { if let textInputNode = self.textInputNode { let text = textInputNode.attributedText?.string ?? "" @@ -253,7 +221,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let textInputNode = self.textInputNode { self.updatingInputState = true - textInputNode.attributedText = NSAttributedString(string: state.inputText, font: Font.regular(17.0), textColor: UIColor.black) + var textColor: UIColor = .black + if let presentationInterfaceState = self.presentationInterfaceState { + textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + } + textInputNode.attributedText = NSAttributedString(string: state.inputText, font: Font.regular(17.0), textColor: textColor) textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) self.updatingInputState = false self.keepSendButtonEnabled = keepSendButtonEnabled @@ -275,7 +247,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return self.textInputNode?.attributedText?.string ?? "" } set(value) { if let textInputNode = self.textInputNode { - textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(17.0), textColor: UIColor.black) + var textColor: UIColor = .black + if let presentationInterfaceState = self.presentationInterfaceState { + textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + } + textInputNode.attributedText = NSAttributedString(string: value, font: Font.regular(17.0), textColor: textColor) self.editableTextNodeDidUpdateText(textInputNode) } } @@ -287,7 +263,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let accessoryButtonInset: CGFloat = 4.0 + UIScreenPixel override init() { - self.textInputBackgroundView = UIImageView(image: textInputViewBackground) + self.textInputBackgroundView = UIImageView() self.textPlaceholderNode = TextNode() self.textPlaceholderNode.isLayerBacked = true self.attachmentButton = HighlightableButton() @@ -299,7 +275,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { super.init() - self.attachmentButton.setImage(attachmentIcon, for: []) self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), for: .touchUpInside) self.view.addSubview(self.attachmentButton) @@ -314,18 +289,16 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } self.micButton.offsetRecordingControls = { [weak self] in - if let strongSelf = self { - strongSelf.updateLayout(width: strongSelf.bounds.size.width, transition: .immediate, interfaceState: strongSelf.presentationInterfaceState) + if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState { + let _ = strongSelf.updateLayout(width: strongSelf.bounds.size.width, transition: .immediate, interfaceState: presentationInterfaceState) } } self.view.addSubview(self.micButton) - self.sendButton.setImage(sendIcon, for: []) self.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), for: .touchUpInside) self.sendButton.alpha = 0.0 self.view.addSubview(self.sendButton) - self.searchLayoutClearButton.setImage(searchLayoutClearButtonImage, for: []) self.searchLayoutClearButton.addTarget(self, action: #selector(self.searchLayoutClearButtonPressed), for: .touchUpInside) self.searchLayoutClearButton.alpha = 0.0 @@ -333,10 +306,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.view.addSubview(self.textInputBackgroundView) - let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) - let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: "Message", font: Font.regular(17.0), textColor: UIColor(0xC8C8CE)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - self.textPlaceholderNode.frame = CGRect(origin: CGPoint(), size: placeholderSize.size) - let _ = placeholderApply() self.addSubnode(self.textPlaceholderNode) self.view.addSubview(self.searchLayoutClearButton) @@ -358,10 +327,22 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private func loadTextInputNode() { let textInputNode = ASEditableTextNode() - textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0)] + var textColor: UIColor = .black + var keyboardAppearance: UIKeyboardAppearance = UIKeyboardAppearance.default + if let presentationInterfaceState = self.presentationInterfaceState { + textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor + switch presentationInterfaceState.theme.chat.inputPanel.keyboardColor { + case .light: + keyboardAppearance = .default + case .dark: + keyboardAppearance = .dark + } + } + textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0), NSForegroundColorAttributeName: textColor] textInputNode.clipsToBounds = true textInputNode.delegate = self textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) + textInputNode.keyboardAppearance = keyboardAppearance self.addSubnode(textInputNode) self.textInputNode = textInputNode @@ -414,17 +395,63 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState - if let peer = interfaceState.peer, previousState.peer == nil || !peer.isEqual(previousState.peer!) { + if self.theme !== interfaceState.theme { + if self.theme == nil || !self.theme!.chat.inputPanel.inputTextColor.isEqual(interfaceState.theme.chat.inputPanel.inputTextColor) { + let textColor = interfaceState.theme.chat.inputPanel.inputTextColor + + if let textInputNode = self.textInputNode { + if let text = textInputNode.attributedText?.string { + let range = textInputNode.selectedRange + textInputNode.attributedText = NSAttributedString(string: text, font: Font.regular(17.0), textColor: textColor) + textInputNode.selectedRange = range + } + textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0), NSForegroundColorAttributeName: textColor] + } + } + + let keyboardAppearance: UIKeyboardAppearance + switch interfaceState.theme.chat.inputPanel.keyboardColor { + case .light: + keyboardAppearance = .default + case .dark: + keyboardAppearance = .dark + } + self.textInputNode?.keyboardAppearance = keyboardAppearance + + self.theme = interfaceState.theme + + + self.attachmentButton.setImage(PresentationResourcesChat.chatInputPanelAttachmentButtonImage(interfaceState.theme), for: []) + self.sendButton.setImage(PresentationResourcesChat.chatInputPanelSendButtonImage(interfaceState.theme), for: []) + self.micButton.updateTheme(theme: interfaceState.theme) + + self.textInputBackgroundView.image = PresentationResourcesChat.chatInputTextFieldBackgroundImage(interfaceState.theme) + + self.searchLayoutClearButton.setImage(PresentationResourcesChat.chatInputTextFieldClearImage(interfaceState.theme), for: []) + + if let audioRecordingDotNode = self.audioRecordingDotNode { + audioRecordingDotNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingDotImage(interfaceState.theme) + } + + self.audioRecordingTimeNode?.updateTheme(theme: interfaceState.theme) + self.audioRecordingCancelIndicator?.updateTheme(theme: interfaceState.theme) + + for (_, button) in self.accessoryItemButtons { + button.updateTheme(theme: interfaceState.theme) + } + } + + if let peer = interfaceState.peer, previousState?.peer == nil || !peer.isEqual(previousState!.peer!) { let placeholder: String if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - placeholder = "Broadcast" + placeholder = interfaceState.strings.Conversation_InputTextBroadcastPlaceholder } else { - placeholder = "Message" + placeholder = interfaceState.strings.Conversation_InputTextPlaceholder } if self.currentPlaceholder != placeholder { self.currentPlaceholder = placeholder let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) - let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: UIColor(0xbebec0)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize.size) let _ = placeholderApply() } @@ -440,9 +467,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } var updateAccessoryButtons = false - if self.presentationInterfaceState.inputTextPanelState.accessoryItems.count == self.accessoryItemButtons.count { - for i in 0 ..< self.presentationInterfaceState.inputTextPanelState.accessoryItems.count { - if self.presentationInterfaceState.inputTextPanelState.accessoryItems[i] != self.accessoryItemButtons[i].0 { + if self.presentationInterfaceState?.inputTextPanelState.accessoryItems.count == self.accessoryItemButtons.count { + for i in 0 ..< interfaceState.inputTextPanelState.accessoryItems.count { + if interfaceState.inputTextPanelState.accessoryItems[i] != self.accessoryItemButtons[i].0 { updateAccessoryButtons = true break } @@ -454,7 +481,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var removeAccessoryButtons: [AccessoryItemIconButton]? if updateAccessoryButtons { var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = [] - for item in self.presentationInterfaceState.inputTextPanelState.accessoryItems { + for item in interfaceState.inputTextPanelState.accessoryItems { var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButton)? for i in 0 ..< self.accessoryItemButtons.count { if self.accessoryItemButtons[i].0 == item { @@ -464,7 +491,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } if itemAndButton == nil { - let button = AccessoryItemIconButton(item: item) + let button = AccessoryItemIconButton(item: item, theme: interfaceState.theme) button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), for: [.touchUpInside]) itemAndButton = (item, button) } @@ -514,7 +541,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } else { animateCancelSlideIn = transition.isAnimated - audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator() + audioRecordingCancelIndicator = ChatTextInputAudioRecordingCancelIndicator(theme: interfaceState.theme, strings: interfaceState.strings) self.audioRecordingCancelIndicator = audioRecordingCancelIndicator self.insertSubnode(audioRecordingCancelIndicator, at: 0) } @@ -531,7 +558,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let currentAudioRecordingTimeNode = self.audioRecordingTimeNode { audioRecordingTimeNode = currentAudioRecordingTimeNode } else { - audioRecordingTimeNode = ChatTextInputAudioRecordingTimeNode() + audioRecordingTimeNode = ChatTextInputAudioRecordingTimeNode(theme: interfaceState.theme) self.audioRecordingTimeNode = audioRecordingTimeNode audioRecordingInfoContainerNode.addSubnode(audioRecordingTimeNode) @@ -560,7 +587,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { animateDotSlideIn = transition.isAnimated audioRecordingDotNode = ASImageNode() - audioRecordingDotNode.image = audioRecordingDotImage + audioRecordingDotNode.image = PresentationResourcesChat.chatInputPanelMediaRecordingDotImage(interfaceState.theme) self.audioRecordingDotNode = audioRecordingDotNode audioRecordingInfoContainerNode.addSubnode(audioRecordingDotNode) } @@ -597,11 +624,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { }) } - if let audioRecordingDotNode = self.audioRecordingDotNode { + if let _ = self.audioRecordingDotNode { self.audioRecordingDotNode = nil } - if let audioRecordingTimeNode = self.audioRecordingTimeNode { + if let _ = self.audioRecordingTimeNode { self.audioRecordingTimeNode = nil } @@ -639,7 +666,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { transition.updateFrame(node: textInputNode, frame: CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + audioRecordingItemsVerticalOffset, width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: panelHeight - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) } - if let contextPlaceholder = self.presentationInterfaceState.inputTextPanelState.contextPlaceholder { + if let contextPlaceholder = interfaceState.inputTextPanelState.contextPlaceholder { let placeholderLayout = TextNode.asyncLayout(self.contextPlaceholderNode) let (placeholderSize, placeholderApply) = placeholderLayout(contextPlaceholder, nil, 1, .end, CGSize(width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let contextPlaceholderNode = placeholderApply() @@ -653,7 +680,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode) } - placeholderApply() + let _ = placeholderApply() contextPlaceholderNode.frame = CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + 0.5 + audioRecordingItemsVerticalOffset), size: placeholderSize.size) } else if let contextPlaceholderNode = self.contextPlaceholderNode { @@ -699,7 +726,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { - if let textInputNode = self.textInputNode { + if let _ = self.textInputNode { let inputTextState = self.inputTextState self.interfaceInteraction?.updateTextInputState({ _ in return inputTextState }) self.updateTextNodeText(animated: true) @@ -785,7 +812,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } - let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: self.bounds.size.width) + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: self.bounds.size.width) let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight) if !self.bounds.size.height.isEqual(to: panelHeight) { self.updateHeight() @@ -821,7 +848,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func searchLayoutClearButtonPressed() { if let interfaceInteraction = self.interfaceInteraction { interfaceInteraction.updateTextInputState { textInputState in - if let (range, type, queryRange) = textInputStateContextQueryRangeAndType(textInputState), type == [.contextRequest] { + if let (_, type, queryRange) = textInputStateContextQueryRangeAndType(textInputState), type == [.contextRequest] { if let queryRange = queryRange, !queryRange.isEmpty { var inputText = textInputState.inputText inputText.replaceSubrange(queryRange, with: "") @@ -857,20 +884,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputNode?.becomeFirstResponder() } - func animateTextSend() { - /*if let textInputNode = self.textInputNode { - let snapshot = textInputNode.view.snapshotViewAfterScreenUpdates(false) - snapshot.frame = self.textInputBackgroundView.convertRect(textInputNode.view.bounds, fromView: textInputNode.view) - self.textInputBackgroundView.addSubview(snapshot) - UIView.animateWithDuration(0.3, animations: { - snapshot.alpha = 0.0 - snapshot.transform = CGAffineTransformMakeTranslation(0.0, -20.0) - }, completion: { _ in - snapshot.removeFromSuperview() - }) - }*/ - } - @objc func accessoryItemButtonPressed(_ button: UIView) { for (item, currentButton) in self.accessoryItemButtons { if currentButton === button { diff --git a/TelegramUI/ChatTitleView.swift b/TelegramUI/ChatTitleView.swift index 05320a0fd6..b6e211b441 100644 --- a/TelegramUI/ChatTitleView.swift +++ b/TelegramUI/ChatTitleView.swift @@ -7,6 +7,9 @@ import SwiftSignalKit import TelegramLegacyComponents final class ChatTitleView: UIView { + private var theme: PresentationTheme + private var strings: PresentationStrings + private let titleNode: ASTextNode private let infoNode: ASTextNode private let typingNode: ASTextNode @@ -49,13 +52,14 @@ final class ChatTitleView: UIView { } } } - let string = NSAttributedString(string: stringValue, font: Font.regular(13.0), textColor: UIColor(0x007ee5)) + let string = NSAttributedString(string: stringValue, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.accentTextColor) if self.typingNode.attributedText == nil || !self.typingNode.attributedText!.isEqual(to: string) { self.typingNode.attributedText = string self.setNeedsLayout() } if self.typingIndicator == nil { let typingIndicator = TGModernConversationTitleActivityIndicator() + typingIndicator.setColor(self.theme.rootController.navigationBar.accentTextColor) self.addSubview(typingIndicator) self.typingIndicator = typingIndicator } @@ -90,7 +94,7 @@ final class ChatTitleView: UIView { var peerView: PeerView? { didSet { if let peerView = self.peerView, let peer = peerViewMainPeer(peerView) { - let string = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: UIColor.black) + let string = NSAttributedString(string: peer.displayTitle, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) if self.titleNode.attributedText == nil || !self.titleNode.attributedText!.isEqual(to: string) { self.titleNode.attributedText = string @@ -107,15 +111,15 @@ final class ChatTitleView: UIView { if let peerView = self.peerView, let peer = peerViewMainPeer(peerView) { if let user = peer as? TelegramUser { if let _ = user.botInfo { - let string = NSAttributedString(string: "bot", font: Font.regular(13.0), textColor: UIColor(0x787878)) + let string = NSAttributedString(string: self.strings.Bot_GenericBotStatus, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true } } else if let peer = peerViewMainPeer(peerView), let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) - let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? UIColor(0x007ee5) : UIColor(0x787878)) + let (string, activity) = stringAndActivityForUserPresence(strings: self.strings, presence: presence, relativeTo: Int32(timestamp)) + let attributedString = NSAttributedString(string: string, font: Font.regular(13.0), textColor: activity ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: attributedString) { self.infoNode.attributedText = attributedString shouldUpdateLayout = true @@ -123,7 +127,7 @@ final class ChatTitleView: UIView { self.presenceManager?.reset(presence: presence) } else { - let string = NSAttributedString(string: "offline", font: Font.regular(13.0), textColor: UIColor(0x787878)) + let string = NSAttributedString(string: strings.Presence_offline, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true @@ -147,14 +151,14 @@ final class ChatTitleView: UIView { } if onlineCount > 1 { let string = NSMutableAttributedString() - string.append(NSAttributedString(string: "\(compactNumericCountString(group.participantCount)) members, ", font: Font.regular(13.0), textColor: UIColor(0x787878))) - string.append(NSAttributedString(string: "\(onlineCount) online", font: Font.regular(13.0), textColor: UIColor(0x007ee5))) + string.append(NSAttributedString(string: "\(compactNumericCountString(group.participantCount)) members, ", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) + string.append(NSAttributedString(string: "\(onlineCount) online", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.accentTextColor)) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true } } else { - let string = NSAttributedString(string: "\(compactNumericCountString(group.participantCount)) members", font: Font.regular(13.0), textColor: UIColor(0x787878)) + let string = NSAttributedString(string: "\(compactNumericCountString(group.participantCount)) members", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true @@ -162,7 +166,7 @@ final class ChatTitleView: UIView { } } else if let channel = peer as? TelegramChannel { if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { - let string = NSAttributedString(string: "\(compactNumericCountString(Int(memberCount))) members", font: Font.regular(13.0), textColor: UIColor(0x787878)) + let string = NSAttributedString(string: "\(compactNumericCountString(Int(memberCount))) members", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true @@ -170,13 +174,13 @@ final class ChatTitleView: UIView { } else { switch channel.info { case .group: - let string = NSAttributedString(string: "group", font: Font.regular(13.0), textColor: UIColor(0x787878)) + let string = NSAttributedString(string: "group", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true } case .broadcast: - let string = NSAttributedString(string: "channel", font: Font.regular(13.0), textColor: UIColor(0x787878)) + let string = NSAttributedString(string: "channel", font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor) if self.infoNode.attributedText == nil || !self.infoNode.attributedText!.isEqual(to: string) { self.infoNode.attributedText = string shouldUpdateLayout = true @@ -191,7 +195,10 @@ final class ChatTitleView: UIView { } } - override init(frame: CGRect) { + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + self.titleNode = ASTextNode() self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 1 @@ -212,7 +219,7 @@ final class ChatTitleView: UIView { self.button = HighlightTrackingButton() - super.init(frame: frame) + super.init(frame: CGRect()) self.addSubnode(self.titleNode) self.addSubnode(self.infoNode) diff --git a/TelegramUI/ChatToastAlertPanelNode.swift b/TelegramUI/ChatToastAlertPanelNode.swift index 982953ad77..ebf7f60b06 100644 --- a/TelegramUI/ChatToastAlertPanelNode.swift +++ b/TelegramUI/ChatToastAlertPanelNode.swift @@ -26,7 +26,7 @@ final class ChatToastAlertPanelNode: ChatTitleAccessoryPanelNode { super.init() - self.backgroundColor = UIColor(0xF5F6F8) + self.backgroundColor = UIColor(rgb: 0xF5F6F8) self.addSubnode(self.titleNode) self.addSubnode(self.separatorNode) diff --git a/TelegramUI/ChatUnblockInputPanelNode.swift b/TelegramUI/ChatUnblockInputPanelNode.swift index c7fa39de78..fda2b78132 100644 --- a/TelegramUI/ChatUnblockInputPanelNode.swift +++ b/TelegramUI/ChatUnblockInputPanelNode.swift @@ -11,7 +11,7 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { private var statusDisposable: Disposable? - private var presentationInterfaceState = ChatPresentationInterfaceState() + private var presentationInterfaceState: ChatPresentationInterfaceState? override var interfaceInteraction: ChatPanelInterfaceInteraction? { didSet { @@ -35,7 +35,13 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { } } - override init() { + private var theme: PresentationTheme + private var strings: PresentationStrings + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + self.button = HighlightableButtonNode() self.activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) self.activityIndicator.isHidden = true @@ -45,7 +51,7 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { self.addSubnode(self.button) self.view.addSubview(self.activityIndicator) - self.button.setAttributedTitle(NSAttributedString(string: "Unblock", font: Font.regular(17.0), textColor: UIColor(0x007ee5)), for: []) + self.button.setAttributedTitle(NSAttributedString(string: strings.Conversation_Unblock, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) self.button.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: [.touchUpInside]) } @@ -53,6 +59,15 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { self.statusDisposable?.dispose() } + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if self.theme !== theme || self.strings !== strings { + self.theme = theme + self.strings = strings + + self.button.setAttributedTitle(NSAttributedString(string: strings.Conversation_Unblock, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) + } + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.bounds.contains(point) { return self.button.view diff --git a/TelegramUI/ChatUnreadItem.swift b/TelegramUI/ChatUnreadItem.swift index c618f4dc04..1ac0d1c590 100644 --- a/TelegramUI/ChatUnreadItem.swift +++ b/TelegramUI/ChatUnreadItem.swift @@ -5,26 +5,19 @@ import AsyncDisplayKit import Display import SwiftSignalKit -private func backgroundImage() -> UIImage? { - return generateImage(CGSize(width: 1.0, height: 25.0), contextGenerator: { size, context -> Void in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(white: 0.0, alpha: 0.2).cgColor) - context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: UIScreenPixel))) - context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) - context.setFillColor(UIColor(white: 1.0, alpha: 0.9).cgColor) - context.fill(CGRect(x: 0.0, y: UIScreenPixel, width: size.width, height: size.height - UIScreenPixel - UIScreenPixel)) - })?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8) -} - private let titleFont = UIFont.systemFont(ofSize: 13.0) class ChatUnreadItem: ListViewItem { let index: MessageIndex + let theme: PresentationTheme + let strings: PresentationStrings let header: ChatMessageDateHeader - init(index: MessageIndex) { + init(index: MessageIndex, theme: PresentationTheme, strings: PresentationStrings) { self.index = index - self.header = ChatMessageDateHeader(timestamp: index.timestamp) + self.theme = theme + self.strings = strings + self.header = ChatMessageDateHeader(timestamp: index.timestamp, theme: theme, strings: strings) } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { @@ -48,6 +41,8 @@ class ChatUnreadItemNode: ListViewItemNode { let backgroundNode: ASImageNode let labelNode: TextNode + private var theme: PresentationTheme? + private let layoutConstants = ChatMessageItemLayoutConstants() init() { @@ -60,12 +55,11 @@ class ChatUnreadItemNode: ListViewItemNode { super.init(layerBacked: true, dynamicBounce: true, rotated: true) - self.backgroundNode.image = backgroundImage() self.addSubnode(self.backgroundNode) self.addSubnode(self.labelNode) - self.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0.0, 0.0, 1.0) + self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) self.scrollPositioningInsets = UIEdgeInsets(top: 5.0, left: 0.0, bottom: 6.0, right: 0.0) self.canBeUsedAsScrollToItemAnchor = false @@ -74,12 +68,7 @@ class ChatUnreadItemNode: ListViewItemNode { override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) - //self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - //self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - - //self.transitionOffset = -self.bounds.size.height * 1.6 - //self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) - //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration) + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } override func animateAdded(_ currentTimestamp: Double, duration: Double) { @@ -99,14 +88,27 @@ class ChatUnreadItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ChatUnreadItem, _ width: CGFloat, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants + let currentTheme = self.theme + return { item, width, dateAtBottom in - let (size, apply) = labelLayout(NSAttributedString(string: "Unread", font: titleFont, textColor: UIColor(0x86868d)), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + var updatedBackgroundImage: UIImage? + if currentTheme !== item.theme { + updatedBackgroundImage = PresentationResourcesChat.chatUnreadBarBackgroundImage(item.theme) + } + + let (size, apply) = labelLayout(NSAttributedString(string: item.strings.Conversation_UnreadMessages, font: titleFont, textColor: item.theme.chat.serviceMessage.unreadBarTextColor), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let backgroundSize = CGSize(width: width, height: 25.0) return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 25.0), insets: UIEdgeInsets(top: 6.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 5.0, right: 0.0)), { [weak self] in if let strongSelf = self { strongSelf.item = item + strongSelf.theme = item.theme + + if let updatedBackgroundImage = updatedBackgroundImage { + strongSelf.backgroundNode.image = updatedBackgroundImage + } + let _ = apply() strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize) diff --git a/TelegramUI/ChatVideoGalleryItem.swift b/TelegramUI/ChatVideoGalleryItem.swift index a060992c0e..22986a4780 100644 --- a/TelegramUI/ChatVideoGalleryItem.swift +++ b/TelegramUI/ChatVideoGalleryItem.swift @@ -7,17 +7,21 @@ import TelegramCore class ChatVideoGalleryItem: GalleryItem { let account: Account + let theme: PresentationTheme + let strings: PresentationStrings let message: Message let location: MessageHistoryEntryLocation? - init(account: Account, message: Message, location: MessageHistoryEntryLocation?) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, message: Message, location: MessageHistoryEntryLocation?) { self.account = account + self.theme = theme + self.strings = strings self.message = message self.location = location } func node() -> GalleryItemNode { - let node = ChatVideoGalleryItemNode(account: self.account) + let node = ChatVideoGalleryItemNode(account: self.account, theme: self.theme, strings: self.strings) for media in self.message.media { if let file = media as? TelegramMediaFile, (file.isVideo || file.mimeType.hasPrefix("video/")) { @@ -71,7 +75,7 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let footerContentNode: ChatItemGalleryFooterContentNode - init(account: Account) { + init(account: Account, theme: PresentationTheme, strings: PresentationStrings) { self.videoNode = MediaPlayerNode() self.snapshotNode = TransformImageNode() self.snapshotNode.backgroundColor = UIColor.black @@ -81,7 +85,7 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.progressButtonNode = HighlightableButtonNode() self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor.white, icon: nil)) - self.footerContentNode = ChatItemGalleryFooterContentNode(account: account) + self.footerContentNode = ChatItemGalleryFooterContentNode(account: account, theme: theme, strings: strings) super.init() @@ -126,7 +130,7 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.snapshotNode.alphaTransitionOnFirstUpdate = false let displaySize = largestSize.dividedByScreenScale() self.snapshotNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() - self.snapshotNode.setSignal(account: account, signal: chatMessageImageFile(account: account, file: file, progressive: true), dispatchOnDisplayLink: false) + self.snapshotNode.setSignal(account: account, signal: chatMessageImageFile(account: account, file: file, progressive: false), dispatchOnDisplayLink: false) self.zoomableContent = (largestSize, self.videoNode) } else { self._ready.set(.single(Void())) @@ -176,7 +180,7 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { /*let source = VideoPlayerSource(account: account, resource: CloudFileMediaResource(location: file.location, size: file.size)) self.videoNode.player = VideoPlayer(source: source)*/ - let player = MediaPlayer(audioSessionManager: (account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, postbox: account.postbox, resource: file.resource, streamable: false, video: true, preferSoftwareDecoding: false, enableSound: true) + let player = MediaPlayer(audioSessionManager: (account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, overlayMediaManager: (account.applicationContext as! TelegramApplicationContext).mediaManager.overlayMediaManager, postbox: account.postbox, resource: file.resource, streamable: false, video: true, preferSoftwareDecoding: false, enableSound: true) if loopVideo { player.actionAtEnd = .loop } diff --git a/TelegramUI/CommandChatInputPanelItem.swift b/TelegramUI/CommandChatInputPanelItem.swift index dbbffc9dc6..a3259a0196 100644 --- a/TelegramUI/CommandChatInputPanelItem.swift +++ b/TelegramUI/CommandChatInputPanelItem.swift @@ -71,7 +71,7 @@ final class CommandChatInputPanelItem: ListViewItem { private let avatarFont = Font.regular(16.0) private let textFont = Font.medium(14.0) private let descriptionFont = Font.regular(14.0) -private let descriptionColor = UIColor(0x9099A2) +private let descriptionColor = UIColor(rgb: 0x9099A2) private let arrowImage = generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -79,7 +79,7 @@ private let arrowImage = generateImage(CGSize(width: 11.0, height: 11.0), contex context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - context.setStrokeColor(UIColor(0xC7CCD0).cgColor) + context.setStrokeColor(UIColor(rgb: 0xC7CCD0).cgColor) context.setLineCap(.round) context.setLineWidth(2.0) context.setLineJoin(.round) @@ -112,15 +112,15 @@ final class CommandChatInputPanelItemNode: ListViewItemNode { self.textNode = TextNode() self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = UIColor(0xC9CDD1) + self.topSeparatorNode.backgroundColor = UIColor(rgb: 0xC9CDD1) self.topSeparatorNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xD6D6DA) + self.separatorNode.backgroundColor = UIColor(rgb: 0xD6D6DA) self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) + self.highlightedBackgroundNode.backgroundColor = UIColor(rgb: 0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true self.arrowNode = HighlightableButtonNode() diff --git a/TelegramUI/CompomentsThemes.swift b/TelegramUI/CompomentsThemes.swift new file mode 100644 index 0000000000..fd4880d261 --- /dev/null +++ b/TelegramUI/CompomentsThemes.swift @@ -0,0 +1,16 @@ +import Foundation +import Display + +extension TabBarControllerTheme { + convenience init(rootControllerTheme: PresentationTheme) { + let theme = rootControllerTheme.rootController.tabBar + self.init(backgroundColor: rootControllerTheme.list.plainBackgroundColor, tabBarBackgroundColor: theme.backgroundColor, tabBarSeparatorColor: theme.separatorColor, tabBarTextColor: theme.textColor, tabBarSelectedTextColor: theme.selectedIconColor, tabBarBadgeBackgroundColor: theme.badgeBackgroundColor, tabBarBadgeTextColor: theme.badgeTextColor) + } +} + +extension NavigationBarTheme { + convenience init(rootControllerTheme: PresentationTheme) { + let theme = rootControllerTheme.rootController.navigationBar + self.init(buttonColor: theme.buttonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: theme.backgroundColor, separatorColor: theme.separatorColor) + } +} diff --git a/TelegramUI/ComposeController.swift b/TelegramUI/ComposeController.swift index d116b17bd0..43fb8f9fd0 100644 --- a/TelegramUI/ComposeController.swift +++ b/TelegramUI/ComposeController.swift @@ -21,20 +21,41 @@ public class ComposeController: ViewController { private let createActionDisposable = MetaDisposable() + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + public init(account: Account) { self.account = account - super.init() + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.title = "Mew Message" + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.title = self.presentationData.strings.Compose_NewMessage + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.scrollToTop = { [weak self] in if let strongSelf = self { strongSelf.contactsNode.contactListNode.scrollToTop() } } + + 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() + } + } + }) } required public init(coder aDecoder: NSCoder) { @@ -43,6 +64,14 @@ public class ComposeController: ViewController { deinit { self.createActionDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + self.title = self.presentationData.strings.Compose_NewMessage + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) } override public func loadDisplayNode() { @@ -83,7 +112,7 @@ public class ComposeController: ViewController { self.contactsNode.openCreateNewSecretChat = { [weak self] in if let strongSelf = self { - let controller = ContactSelectionController(account: strongSelf.account, title: "New Secret Chat") + let controller = ContactSelectionController(account: strongSelf.account, title: { $0.Compose_NewEncryptedChat }) strongSelf.createActionDisposable.set((controller.result |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] peerId in diff --git a/TelegramUI/ComposeControllerNode.swift b/TelegramUI/ComposeControllerNode.swift index 3675b12495..121ec55ef8 100644 --- a/TelegramUI/ComposeControllerNode.swift +++ b/TelegramUI/ComposeControllerNode.swift @@ -3,10 +3,7 @@ import AsyncDisplayKit import UIKit import Postbox import TelegramCore - -private let createGroupIcon = UIImage(bundleImageName: "Contact List/CreateGroupActionIcon")?.precomposed() -private let createSecretChatIcon = UIImage(bundleImageName: "Contact List/CreateSecretChatActionIcon")?.precomposed() -private let createChannelIcon = UIImage(bundleImageName: "Contact List/CreateChannelActionIcon")?.precomposed() +import SwiftSignalKit final class ComposeControllerNode: ASDisplayNode { let contactListNode: ContactListNode @@ -25,21 +22,26 @@ final class ComposeControllerNode: ASDisplayNode { var openCreateNewSecretChat: (() -> Void)? var openCreateNewChannel: (() -> Void)? + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + init(account: Account) { self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + var openCreateNewGroupImpl: (() -> Void)? var openCreateNewSecretChatImpl: (() -> Void)? var openCreateNewChannelImpl: (() -> Void)? self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: true, options: [ - ContactListAdditionalOption(title: "New Group", icon: createGroupIcon, action: { + ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewGroup, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/CreateGroupActionIcon"), color: presentationData.theme.list.itemAccentColor), action: { openCreateNewGroupImpl?() }), - ContactListAdditionalOption(title: "New Secret Chat", icon: createSecretChatIcon, action: { + ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewEncryptedChat, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/CreateSecretChatActionIcon"), color: presentationData.theme.list.itemAccentColor), action: { openCreateNewSecretChatImpl?() }), - ContactListAdditionalOption(title: "New Channel", icon: createChannelIcon, action: { + ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewChannel, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/CreateChannelActionIcon"), color: presentationData.theme.list.itemAccentColor), action: { openCreateNewChannelImpl?() }) ])) @@ -48,7 +50,7 @@ final class ComposeControllerNode: ASDisplayNode { return UITracingLayerView() }, didLoad: nil) - self.backgroundColor = UIColor.white + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.addSubnode(self.contactListNode) @@ -61,6 +63,28 @@ final class ComposeControllerNode: ASDisplayNode { openCreateNewChannelImpl = { [weak self] in self?.openCreateNewChannel?() } + + 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() + } + } + }) + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor + + self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -69,7 +93,7 @@ final class ComposeControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) @@ -95,7 +119,7 @@ final class ComposeControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(contentNode: ContactsSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peerId) } diff --git a/TelegramUI/ContactListActionItem.swift b/TelegramUI/ContactListActionItem.swift index 6788a4a017..c17b2f2880 100644 --- a/TelegramUI/ContactListActionItem.swift +++ b/TelegramUI/ContactListActionItem.swift @@ -4,11 +4,13 @@ import AsyncDisplayKit import SwiftSignalKit class ContactListActionItem: ListViewItem { + let theme: PresentationTheme let title: String let icon: UIImage? let action: () -> Void - init(title: String, icon: UIImage?, action: @escaping () -> Void) { + init(theme: PresentationTheme, title: String, icon: UIImage?, action: @escaping () -> Void) { + self.theme = theme self.title = title self.icon = icon self.action = action @@ -64,17 +66,17 @@ class ContactListActionItemNode: ListViewItemNode { private let iconNode: ASImageNode private let titleNode: TextNode + private var theme: PresentationTheme? + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.titleNode = TextNode() @@ -88,7 +90,6 @@ class ContactListActionItemNode: ListViewItemNode { self.iconNode.displaysAsynchronously = false self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) @@ -99,11 +100,18 @@ class ContactListActionItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ContactListActionItem, _ width: CGFloat) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let currentTheme = self.theme return { item, width in + var updatedTheme: PresentationTheme? + + if currentTheme !== item.theme { + updatedTheme = item.theme + } + let leftInset: CGFloat = 65.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - 10.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor), nil, 1, .end, CGSize(width: width - 10.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let contentSize = CGSize(width: width, height: 48.0) let insets = UIEdgeInsets() @@ -113,6 +121,15 @@ class ContactListActionItemNode: ListViewItemNode { return (layout, { [weak self] in if let strongSelf = self { + strongSelf.theme = item.theme + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + let _ = titleApply() strongSelf.iconNode.image = item.icon diff --git a/TelegramUI/ContactListNameIndexHeader.swift b/TelegramUI/ContactListNameIndexHeader.swift index 9774e00993..8b1c2b4b0f 100644 --- a/TelegramUI/ContactListNameIndexHeader.swift +++ b/TelegramUI/ContactListNameIndexHeader.swift @@ -2,18 +2,20 @@ import Display final class ContactListNameIndexHeader: Equatable, ListViewItemHeader { let id: Int64 + let theme: PresentationTheme let letter: unichar let stickDirection: ListViewItemHeaderStickDirection = .top let height: CGFloat = 29.0 - init(letter: unichar) { + init(theme: PresentationTheme, letter: unichar) { + self.theme = theme self.letter = letter self.id = Int64(letter) } func node() -> ListViewItemHeaderNode { - return ContactListNameIndexHeaderNode(letter: self.letter) + return ContactListNameIndexHeaderNode(theme: self.theme, letter: self.letter) } static func ==(lhs: ContactListNameIndexHeader, rhs: ContactListNameIndexHeader) -> Bool { @@ -22,14 +24,16 @@ final class ContactListNameIndexHeader: Equatable, ListViewItemHeader { } final class ContactListNameIndexHeaderNode: ListViewItemHeaderNode { + private var theme: PresentationTheme private let letter: unichar private let sectionHeaderNode: ListSectionHeaderNode - init(letter: unichar) { + init(theme: PresentationTheme, letter: unichar) { + self.theme = theme self.letter = letter - self.sectionHeaderNode = ListSectionHeaderNode() + self.sectionHeaderNode = ListSectionHeaderNode(theme: theme) super.init() diff --git a/TelegramUI/ContactListNode.swift b/TelegramUI/ContactListNode.swift index ba264353b1..a76d647d44 100644 --- a/TelegramUI/ContactListNode.swift +++ b/TelegramUI/ContactListNode.swift @@ -72,10 +72,10 @@ private final class ContactListNodeInteraction { } private enum ContactListNodeEntry: Comparable, Identifiable { - case search - case vcard(Peer) - case option(Int, ContactListAdditionalOption) - case peer(Int, Peer, PeerPresence?, ContactListNameIndexHeader?, ContactsPeerItemSelection) + case search(PresentationTheme, PresentationStrings) + case vcard(Peer, PresentationTheme, PresentationStrings) + case option(Int, ContactListAdditionalOption, PresentationTheme, PresentationStrings) + case peer(Int, Peer, PeerPresence?, ContactListNameIndexHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings) var stableId: ContactListNodeEntryId { switch self { @@ -83,33 +83,33 @@ private enum ContactListNodeEntry: Comparable, Identifiable { return .search case .vcard: return .vcard - case let .option(index, _): + case let .option(index, _, _, _): return .option(index: index) - case let .peer(_, peer, _, _, _): + case let .peer(_, peer, _, _, _, _, _): return .peerId(peer.id.toInt64()) } } func item(account: Account, interaction: ContactListNodeInteraction) -> ListViewItem { switch self { - case .search: - return ChatListSearchItem(placeholder: "Search contacts", activate: { + case let .search(theme, strings): + return ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { interaction.activateSearch() }) - case let .vcard(peer): - return ContactsVCardItem(account: account, peer: peer, action: { peer in + case let .vcard(peer, theme, strings): + return ContactsVCardItem(theme: theme, strings: strings, account: account, peer: peer, action: { peer in interaction.openPeer(peer) }) - case let .option(_, option): - return ContactListActionItem(title: option.title, icon: option.icon, action: option.action) - case let .peer(_, peer, presence, header, selection): + case let .option(_, option, theme, strings): + return ContactListActionItem(theme: theme, title: option.title, icon: option.icon, action: option.action) + case let .peer(_, peer, presence, header, selection, theme, strings): let status: ContactsPeerItemStatus if let presence = presence { status = .presence(presence) } else { status = .none } - return ContactsPeerItem(account: account, peer: peer, chatPeer: peer, status: status, selection: selection, index: nil, header: header, action: { _ in + return ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: status, selection: selection, index: nil, header: header, action: { _ in interaction.openPeer(peer) }) } @@ -117,29 +117,27 @@ private enum ContactListNodeEntry: Comparable, Identifiable { static func ==(lhs: ContactListNodeEntry, rhs: ContactListNodeEntry) -> Bool { switch lhs { - case .search: - switch rhs { - case .search: - return true - default: - return false - } - case let .vcard(lhsPeer): - switch rhs { - case let .vcard(rhsPeer): - return lhsPeer.id == rhsPeer.id - default: - return false - } - case let .option(index, option): - if case .option(index, option) = rhs { + case let .search(lhsTheme, lhsStrings): + if case let .search(rhsTheme, rhsStrings) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings { return true } else { return false } - case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection): + case let .vcard(lhsPeer, lhsTheme, lhsStrings): + if case let .vcard(rhsPeer, rhsTheme, rhsStrings) = rhs, arePeersEqual(lhsPeer, rhsPeer), lhsTheme === rhsTheme, lhsStrings === rhsStrings { + return true + } else { + return false + } + case let .option(lhsIndex, lhsOption, lhsTheme, lhsStrings): + if case let .option(rhsIndex, rhsOption, rhsTheme, rhsStrings) = rhs, lhsIndex == rhsIndex, lhsOption == rhsOption, lhsTheme === rhsTheme, lhsStrings === rhsStrings { + return true + } else { + return false + } + case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings): switch rhs { - case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection): + case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings): if lhsIndex != rhsIndex { return false } @@ -159,6 +157,12 @@ private enum ContactListNodeEntry: Comparable, Identifiable { if lhsSelection != rhsSelection { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } return true default: return false @@ -177,20 +181,20 @@ private enum ContactListNodeEntry: Comparable, Identifiable { case .peer, .option: return true } - case let .option(lhsIndex, _): + case let .option(lhsIndex, _, _, _): switch rhs { case .search, .vcard: return false - case let .option(rhsIndex, _): + case let .option(rhsIndex, _, _, _): return lhsIndex < rhsIndex case .peer: return true } - case let .peer(lhsIndex, _, _, _, _): + case let .peer(lhsIndex, _, _, _, _, _, _): switch rhs { case .search, .vcard, .option: return false - case let .peer(rhsIndex, _, _, _, _): + case let .peer(rhsIndex, _, _, _, _, _, _): return lhsIndex < rhsIndex } } @@ -233,7 +237,7 @@ private extension PeerIndexNameRepresentation { } } -private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences: [PeerId: PeerPresence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?) -> [ContactListNodeEntry] { +private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences: [PeerId: PeerPresence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings) -> [ContactListNodeEntry] { var entries: [ContactListNodeEntry] = [] var orderedPeers: [Peer] @@ -241,10 +245,10 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences switch presentation { case let .orderedByPresence(displayVCard): - entries.append(.search) + entries.append(.search(theme, strings)) if displayVCard { if let peer = accountPeer { - entries.append(.vcard(peer)) + entries.append(.vcard(peer, theme, strings)) } } orderedPeers = peers.sorted(by: { lhs, rhs in @@ -291,16 +295,16 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences if let cached = headerCache[indexHeader] { header = cached } else { - header = ContactListNameIndexHeader(letter: indexHeader) + header = ContactListNameIndexHeader(theme: theme, letter: indexHeader) headerCache[indexHeader] = header } headers[peer.id] = header } if displaySearch { - entries.append(.search) + entries.append(.search(theme, strings)) } for i in 0 ..< options.count { - entries.append(.option(i, options[i])) + entries.append(.option(i, options[i], theme, strings)) } case .search: orderedPeers = peers @@ -332,7 +336,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [Peer], presences } else { selection = .none } - entries.append(.peer(i, orderedPeers[i], presences[orderedPeers[i].id], headers[orderedPeers[i].id], selection)) + entries.append(.peer(i, orderedPeers[i], presences[orderedPeers[i].id], headers[orderedPeers[i].id], selection, theme, strings)) } return entries } @@ -415,8 +419,7 @@ final class ContactListNode: ASDisplayNode { } private var didSetReady = false - private var enableUpdatesValue = true - private let enableUpdatesPromise = ValuePromise(true, ignoreRepeated: true) + private let contactPeersViewPromise = Promise() private let selectionStatePromise = Promise(nil) private var selectionStateValue: ContactListNodeGroupSelectionState? { @@ -425,12 +428,19 @@ final class ContactListNode: ASDisplayNode { } } + private var enableUpdatesValue = false var enableUpdates: Bool { get { return self.enableUpdatesValue } set(value) { - self.enableUpdatesValue = value - self.enableUpdatesPromise.set(value) + if value != self.enableUpdatesValue { + self.enableUpdatesValue = value + if value { + self.contactPeersViewPromise.set(self.account.postbox.contactPeersView(accountPeerId: self.account.peerId)) + } else { + self.contactPeersViewPromise.set(self.account.postbox.contactPeersView(accountPeerId: self.account.peerId) |> take(1)) + } + } } } @@ -440,13 +450,23 @@ final class ContactListNode: ASDisplayNode { private let previousEntries = Atomic<[ContactListNodeEntry]?>(value: nil) private let disposable = MetaDisposable() + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + init(account: Account, presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState? = nil) { self.account = account self.listNode = ListView() + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) + super.init() + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.selectionStateValue = selectionState self.selectionStatePromise.set(.single(selectionState)) @@ -465,13 +485,14 @@ final class ContactListNode: ASDisplayNode { var firstTime: Int32 = 1 let selectionStateSignal = self.selectionStatePromise.get() let transition: Signal + let themeAndStringsPromise = self.themeAndStringsPromise if case let .search(query) = presentation { transition = query |> mapToSignal { query in - return combineLatest(account.postbox.searchContacts(query: query), selectionStateSignal) - |> mapToQueue { peers, selectionState -> Signal in + return combineLatest(account.postbox.searchContacts(query: query), selectionStateSignal, themeAndStringsPromise.get()) + |> mapToQueue { peers, selectionState, themeAndStrings -> Signal in let signal = deferred { () -> Signal in - let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: [:], presentation: presentation, selectionState: selectionState) + let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: [:], presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1) let previous = previousEntries.swap(entries) return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: false)) } @@ -484,41 +505,53 @@ final class ContactListNode: ASDisplayNode { } } } else { - transition = self.enableUpdatesPromise.get() - |> mapToSignal { enableUpdates -> Signal in - if enableUpdates { - return combineLatest(account.postbox.contactPeersView(accountPeerId: account.peerId), selectionStateSignal) - |> mapToQueue { view, selectionState -> Signal in - let signal = deferred { () -> Signal in - let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: view.peers, presences: view.peerPresences, presentation: presentation, selectionState: selectionState) - let previous = previousEntries.swap(entries) - let animated: Bool - if let previous = previous { - animated = (entries.count - previous.count) < 20 - } else { - animated = false - } - return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: animated)) - } - - if OSAtomicCompareAndSwap32(1, 0, &firstTime) { - return signal |> runOn(Queue.mainQueue()) - } else { - return signal |> runOn(processingQueue) - } + transition = (combineLatest(self.contactPeersViewPromise.get(), selectionStateSignal, themeAndStringsPromise.get()) + |> mapToQueue { view, selectionState, themeAndStrings -> Signal in + let signal = deferred { () -> Signal in + let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: view.peers, presences: view.peerPresences, presentation: presentation, selectionState: selectionState, theme: themeAndStrings.0, strings: themeAndStrings.1) + let previous = previousEntries.swap(entries) + let animated: Bool + if let previous = previous { + animated = (entries.count - previous.count) < 20 + } else { + animated = false } - } else { - return .never() + return .single(preparedContactListNodeTransition(account: account, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, animated: animated)) } - } |> deliverOnMainQueue + + if OSAtomicCompareAndSwap32(1, 0, &firstTime) { + return signal |> runOn(Queue.mainQueue()) + } else { + return signal |> runOn(processingQueue) + } + }) + |> deliverOnMainQueue } self.disposable.set(transition.start(next: { [weak self] transition in self?.enqueueTransition(transition) })) + + 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.backgroundColor = presentationData.theme.chatList.backgroundColor + strongSelf.themeAndStringsPromise.set(.single((presentationData.theme, presentationData.strings))) + } + } + }) + + self.enableUpdates = true } deinit { self.disposable.dispose() + self.presentationDataDisposable?.dispose() } func updateSelectionState(_ f: (ContactListNodeGroupSelectionState?) -> ContactListNodeGroupSelectionState?) { diff --git a/TelegramUI/ContactMultiselectionController.swift b/TelegramUI/ContactMultiselectionController.swift index 98c46daa12..e1641d4798 100644 --- a/TelegramUI/ContactMultiselectionController.swift +++ b/TelegramUI/ContactMultiselectionController.swift @@ -36,44 +36,76 @@ public class ContactMultiselectionController: ViewController { private var didPlayPresentationAnimation = false + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + public init(account: Account, mode: ContactMultiselectionControllerMode) { self.account = account self.mode = mode - self.titleView = CounterContollerTitleView() + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - super.init() + self.titleView = CounterContollerTitleView(theme: self.presentationData.theme) + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style switch mode { case .groupCreation: - self.titleView.title = CounterContollerTitle(title: "New Group", counter: "0/5000") - let rightNavigationButton = UIBarButtonItem(title: "Next", style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) + self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.Compose_NewGroup, counter: "0/5000") + let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) self.rightNavigationButton = rightNavigationButton self.navigationItem.rightBarButtonItem = self.rightNavigationButton rightNavigationButton.isEnabled = false case .peerSelection: - self.titleView.title = CounterContollerTitle(title: "Add Users", counter: "") - let rightNavigationButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) + self.titleView.title = CounterContollerTitle(title: self.presentationData.strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder, counter: "") + let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed)) self.rightNavigationButton = rightNavigationButton - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed)) self.navigationItem.rightBarButtonItem = self.rightNavigationButton rightNavigationButton.isEnabled = false } self.navigationItem.titleView = self.titleView - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.scrollToTop = { [weak self] in if let strongSelf = self { strongSelf.contactsNode.contactListNode.scrollToTop() } } + + 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() + } + } + }) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + //self.title = self.presentationData.strings.Contacts_Title + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + } + override public func loadDisplayNode() { self.displayNode = ContactMultiselectionControllerNode(account: self.account) self._ready.set(self.contactsNode.contactListNode.ready) @@ -114,7 +146,7 @@ public class ContactMultiselectionController: ViewController { strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 switch strongSelf.mode { case .groupCreation: - strongSelf.titleView.title = CounterContollerTitle(title: "New Group", counter: "\(updatedCount)/5000") + strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroup, counter: "\(updatedCount)/5000") case .peerSelection: break } @@ -160,7 +192,7 @@ public class ContactMultiselectionController: ViewController { strongSelf.rightNavigationButton?.isEnabled = updatedCount != 0 switch strongSelf.mode { case .groupCreation: - strongSelf.titleView.title = CounterContollerTitle(title: "New Group", counter: "\(updatedCount)/5000") + strongSelf.titleView.title = CounterContollerTitle(title: strongSelf.presentationData.strings.Compose_NewGroup, counter: "\(updatedCount)/5000") case .peerSelection: break } diff --git a/TelegramUI/ContactMultiselectionControllerNode.swift b/TelegramUI/ContactMultiselectionControllerNode.swift index acbbd6d431..237a942914 100644 --- a/TelegramUI/ContactMultiselectionControllerNode.swift +++ b/TelegramUI/ContactMultiselectionControllerNode.swift @@ -42,16 +42,21 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { var dismiss: (() -> Void)? + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + init(account: Account) { self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: false, options: []), selectionState: ContactListNodeGroupSelectionState()) - self.tokenListNode = EditableTokenListNode() + self.tokenListNode = EditableTokenListNode(theme: EditableTokenListNodeTheme(backgroundColor: self.presentationData.theme.rootController.navigationBar.backgroundColor, separatorColor: self.presentationData.theme.rootController.navigationBar.separatorColor, placeholderTextColor: self.presentationData.theme.list.itemPlaceholderTextColor, primaryTextColor: self.presentationData.theme.list.itemPrimaryTextColor, selectedTextColor: self.presentationData.theme.list.itemAccentColor, keyboardColor: self.presentationData.theme.chatList.searchBarKeyboardColor), placeholder: self.presentationData.strings.Compose_TokenListPlaceholder) super.init(viewBlock: { return UITracingLayerView() }, didLoad: nil) - self.backgroundColor = UIColor.white + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.addSubnode(self.contactListNode) self.addSubnode(self.tokenListNode) @@ -88,12 +93,12 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } strongSelf.searchResultsNode = searchResultsNode searchResultsNode.enableUpdates = true - searchResultsNode.backgroundColor = .white + searchResultsNode.backgroundColor = strongSelf.presentationData.theme.chatList.backgroundColor if let (layout, navigationBarHeight) = strongSelf.containerLayout { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight insets.top += strongSelf.tokenListNode.bounds.size.height - searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: .immediate) + searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: .immediate) searchResultsNode.frame = CGRect(origin: CGPoint(), size: layout.size) } @@ -106,12 +111,30 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } } } + + 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() + } + } + }) } deinit { self.searchResultsReadyDisposable.dispose() } + private func updateThemeAndStrings() { + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) @@ -124,11 +147,11 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { insets.top += tokenListHeight - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) if let searchResultsNode = self.searchResultsNode { - searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) + searchResultsNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) searchResultsNode.frame = CGRect(origin: CGPoint(), size: layout.size) } } diff --git a/TelegramUI/ContactSelectionController.swift b/TelegramUI/ContactSelectionController.swift index fdd57f389f..33055c4bbe 100644 --- a/TelegramUI/ContactSelectionController.swift +++ b/TelegramUI/ContactSelectionController.swift @@ -13,6 +13,7 @@ public class ContactSelectionController: ViewController { } private let index: PeerNameIndex = .lastNameFirst + private let titleProducer: (PresentationStrings) -> String private var _ready = Promise() override public var ready: Promise { @@ -29,6 +30,9 @@ public class ContactSelectionController: ViewController { private let createActionDisposable = MetaDisposable() private let confirmationDisposable = MetaDisposable() + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + public var displayNavigationActivity: Bool = false { didSet { if self.displayNavigationActivity != oldValue { @@ -41,21 +45,40 @@ public class ContactSelectionController: ViewController { } } - public init(account: Account, title: String, confirmation: @escaping (PeerId) -> Signal = { _ in .single(true) }) { + public init(account: Account, title: @escaping (PresentationStrings) -> String, confirmation: @escaping (PeerId) -> Signal = { _ in .single(true) }) { self.account = account + self.titleProducer = title self.confirmation = confirmation - super.init() + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.title = title + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.title = self.titleProducer(self.presentationData.strings) + + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.scrollToTop = { [weak self] in if let strongSelf = self { strongSelf.contactsNode.contactListNode.scrollToTop() } } + + 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() + } + } + }) } required public init(coder aDecoder: NSCoder) { @@ -64,6 +87,15 @@ public class ContactSelectionController: ViewController { deinit { self.createActionDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + self.title = self.titleProducer(self.presentationData.strings) + self.tabBarItem.title = self.presentationData.strings.Contacts_Title + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) } @objc func cancelPressed() { diff --git a/TelegramUI/ContactSelectionControllerNode.swift b/TelegramUI/ContactSelectionControllerNode.swift index abb677ab69..5d1f5f7be0 100644 --- a/TelegramUI/ContactSelectionControllerNode.swift +++ b/TelegramUI/ContactSelectionControllerNode.swift @@ -3,6 +3,7 @@ import AsyncDisplayKit import UIKit import Postbox import TelegramCore +import SwiftSignalKit final class ContactSelectionControllerNode: ASDisplayNode { let contactListNode: ContactListNode @@ -18,18 +19,43 @@ final class ContactSelectionControllerNode: ASDisplayNode { var requestOpenPeerFromSearch: ((PeerId) -> Void)? var dismiss: (() -> Void)? + var presentationData: PresentationData + var presentationDataDisposable: Disposable? + init(account: Account) { self.account = account self.contactListNode = ContactListNode(account: account, presentation: .natural(displaySearch: true, options: [])) + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + super.init(viewBlock: { return UITracingLayerView() }, didLoad: nil) - self.backgroundColor = UIColor.white + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.addSubnode(self.contactListNode) + + 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() + } + } + }) + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateTheme() { + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -38,7 +64,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) @@ -64,7 +90,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(contentNode: ContactsSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peerId) } diff --git a/TelegramUI/ContactsController.swift b/TelegramUI/ContactsController.swift index ce2d95809a..e12b413266 100644 --- a/TelegramUI/ContactsController.swift +++ b/TelegramUI/ContactsController.swift @@ -19,29 +19,62 @@ public class ContactsController: ViewController { return self._ready } + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + public init(account: Account) { self.account = account - super.init() + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.title = "Contacts" - self.tabBarItem.title = "Contacts" + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + self.title = self.presentationData.strings.Contacts_Title + self.tabBarItem.title = self.presentationData.strings.Contacts_Title self.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconContacts") self.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconContactsSelected") - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.scrollToTop = { [weak self] in if let strongSelf = self { strongSelf.contactsNode.contactListNode.scrollToTop() } } + + 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() + } + } + }) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + self.title = self.presentationData.strings.Contacts_Title + self.tabBarItem.title = self.presentationData.strings.Contacts_Title + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + } + override public func loadDisplayNode() { self.displayNode = ContactsControllerNode(account: self.account) self._ready.set(self.contactsNode.contactListNode.ready) diff --git a/TelegramUI/ContactsControllerNode.swift b/TelegramUI/ContactsControllerNode.swift index 47fe5a07fe..fa7f396242 100644 --- a/TelegramUI/ContactsControllerNode.swift +++ b/TelegramUI/ContactsControllerNode.swift @@ -3,6 +3,7 @@ import AsyncDisplayKit import UIKit import Postbox import TelegramCore +import SwiftSignalKit final class ContactsControllerNode: ASDisplayNode { let contactListNode: ContactListNode @@ -17,17 +18,46 @@ final class ContactsControllerNode: ASDisplayNode { var requestDeactivateSearch: (() -> Void)? var requestOpenPeerFromSearch: ((PeerId) -> Void)? + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + init(account: Account) { self.account = account self.contactListNode = ContactListNode(account: account, presentation: .orderedByPresence(displayVCard: true)) + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + super.init(viewBlock: { return UITracingLayerView() }, didLoad: nil) - self.backgroundColor = UIColor.white + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.addSubnode(self.contactListNode) + + 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() + } + } + }) + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor + + self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -36,7 +66,7 @@ final class ContactsControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight - self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) + self.contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: insets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight), transition: transition) self.contactListNode.frame = CGRect(origin: CGPoint(), size: layout.size) @@ -62,7 +92,7 @@ final class ContactsControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(contentNode: ContactsSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ContactsSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peerId) } diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index 33a4bb126b..97e546fb16 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -42,6 +42,8 @@ enum ContactsPeerItemSelection: Equatable { } class ContactsPeerItem: ListViewItem { + let theme: PresentationTheme + let strings: PresentationStrings let account: Account let peer: Peer? let chatPeer: Peer? @@ -54,7 +56,9 @@ class ContactsPeerItem: ListViewItem { let header: ListViewItemHeader? - init(account: Account, peer: Peer?, chatPeer: Peer?, status: ContactsPeerItemStatus, selection: ContactsPeerItemSelection, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (Peer) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer?, chatPeer: Peer?, status: ContactsPeerItemStatus, selection: ContactsPeerItemSelection, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (Peer) -> Void) { + self.theme = theme + self.strings = strings self.account = account self.peer = peer self.chatPeer = chatPeer @@ -89,7 +93,7 @@ class ContactsPeerItem: ListViewItem { letter = channel.title.substring(to: channel.title.index(after: channel.title.startIndex)).uppercased() } } - self.headerAccessoryItem = ContactsSectionHeaderAccessoryItem(sectionHeader: .letter(letter)) + self.headerAccessoryItem = ContactsSectionHeaderAccessoryItem(sectionHeader: .letter(letter), theme: theme) } else { self.headerAccessoryItem = nil } @@ -184,15 +188,12 @@ class ContactsPeerItemNode: ListViewItemNode { required init() { self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = .white self.backgroundNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true self.avatarNode = AvatarNode(font: Font.regular(15.0)) @@ -260,7 +261,14 @@ class ContactsPeerItemNode: ListViewItemNode { let makeStatusLayout = TextNode.asyncLayout(self.statusNode) let currentSelectionNode = self.selectionNode + let currentItem = self.layoutParams?.0 + return { [weak self] item, width, first, last, firstWithHeader in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } var leftInset: CGFloat = 65.0 let rightInset: CGFloat = 10.0 @@ -292,21 +300,21 @@ class ContactsPeerItemNode: ListViewItemNode { if let user = peer as? TelegramUser { if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() - string.append(NSAttributedString(string: firstName, font: titleFont, textColor: .black)) - string.append(NSAttributedString(string: " ", font: titleFont, textColor: .black)) - string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: .black)) + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)) + string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor)) titleAttributedString = string } else if let firstName = user.firstName, !firstName.isEmpty { - titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: UIColor.black) + titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) } else if let lastName = user.lastName, !lastName.isEmpty { - titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: UIColor.black) + titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) } else { - titleAttributedString = NSAttributedString(string: "Deleted User", font: titleBoldFont, textColor: UIColor(0xa6a6a6)) + titleAttributedString = NSAttributedString(string: item.strings.User_DeletedAccount, font: titleBoldFont, textColor: item.theme.list.itemSecondaryTextColor) } } else if let group = peer as? TelegramGroup { - titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black) + titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) } else if let channel = peer as? TelegramChannel { - titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black) + titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) } switch item.status { @@ -316,12 +324,12 @@ class ContactsPeerItemNode: ListViewItemNode { if let presence = presence as? TelegramUserPresence { userPresence = presence let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) - statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? UIColor(0x007ee5) : UIColor(0xa6a6a6)) + let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, presence: presence, relativeTo: Int32(timestamp)) + statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor) } case .addressName: if let addressName = peer.addressName { - statusAttributedString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: UIColor(0xa6a6a6)) + statusAttributedString = NSAttributedString(string: "@" + addressName, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } } } @@ -349,6 +357,12 @@ class ContactsPeerItemNode: ListViewItemNode { if let strongSelf = strongSelf { strongSelf.layoutParams = (item, width, first, last, firstWithHeader) + if let _ = updatedTheme { + strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: leftInset - 51.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)) let _ = titleApply() diff --git a/TelegramUI/ContactsSearchContainerNode.swift b/TelegramUI/ContactsSearchContainerNode.swift index c0d34f01ba..5f97c8bf4c 100644 --- a/TelegramUI/ContactsSearchContainerNode.swift +++ b/TelegramUI/ContactsSearchContainerNode.swift @@ -6,7 +6,7 @@ import Postbox import TelegramCore private enum ContactListSearchEntry { - case peer(Peer) + case peer(Peer, PresentationTheme, PresentationStrings) } final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { @@ -18,26 +18,34 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { private let searchQuery = Promise() private let searchDisposable = MetaDisposable() + private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + init(account: Account, openPeer: @escaping (PeerId) -> Void) { self.account = account self.openPeer = openPeer + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.themeAndStringsPromise = Promise((presentationData.theme, presentationData.strings)) + self.listNode = ListView() super.init() - self.backgroundColor = UIColor.white + self.backgroundColor = presentationData.theme.chatList.backgroundColor self.addSubnode(self.listNode) self.listNode.isHidden = true + let themeAndStringsPromise = self.themeAndStringsPromise + let searchItems = searchQuery.get() |> mapToSignal { query -> Signal<[ContactListSearchEntry], NoError> in if let query = query, !query.isEmpty { - return account.postbox.searchContacts(query: query.lowercased()) + return combineLatest(account.postbox.searchContacts(query: query.lowercased()), themeAndStringsPromise.get()) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) - |> map { peers -> [ContactListSearchEntry] in - return peers.map({ .peer($0) }) + |> map { peers, themeAndStrings -> [ContactListSearchEntry] in + return peers.map({ .peer($0, themeAndStrings.0, themeAndStrings.1) }) } } else { return .single([]) @@ -54,8 +62,8 @@ final class ContactsSearchContainerNode: SearchDisplayControllerContentNode { var listItems: [ListViewItem] = [] for item in items { switch item { - case let .peer(peer): - listItems.append(ContactsPeerItem(account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, index: nil, header: nil, action: { [weak self] peer in + case let .peer(peer, theme, strings): + listItems.append(ContactsPeerItem(theme: theme, strings: strings, account: account, peer: peer, chatPeer: peer, status: .none, selection: .none, index: nil, header: nil, action: { [weak self] peer in if let openPeer = self?.openPeer { self?.listNode.clearHighlightAnimated(true) openPeer(peer.id) diff --git a/TelegramUI/ContactsSectionHeaderAccessoryItem.swift b/TelegramUI/ContactsSectionHeaderAccessoryItem.swift index f70aad2829..1ca2b8ee9b 100644 --- a/TelegramUI/ContactsSectionHeaderAccessoryItem.swift +++ b/TelegramUI/ContactsSectionHeaderAccessoryItem.swift @@ -26,13 +26,15 @@ func ==(lhs: ContactsSectionHeader, rhs: ContactsSectionHeader) -> Bool { final class ContactsSectionHeaderAccessoryItem: ListViewAccessoryItem { private let sectionHeader: ContactsSectionHeader + private let theme: PresentationTheme - init(sectionHeader: ContactsSectionHeader) { + init(sectionHeader: ContactsSectionHeader, theme: PresentationTheme) { self.sectionHeader = sectionHeader + self.theme = theme } func isEqualToItem(_ other: ListViewAccessoryItem) -> Bool { - if let other = other as? ContactsSectionHeaderAccessoryItem, self.sectionHeader == other.sectionHeader { + if let other = other as? ContactsSectionHeaderAccessoryItem, self.sectionHeader == other.sectionHeader, self.theme === other.theme { return true } else { return false @@ -40,17 +42,19 @@ final class ContactsSectionHeaderAccessoryItem: ListViewAccessoryItem { } func node() -> ListViewAccessoryItemNode { - return ContactsSectionHeaderAccessoryItemNode(sectionHeader: self.sectionHeader) + return ContactsSectionHeaderAccessoryItemNode(sectionHeader: self.sectionHeader, theme: self.theme) } } private final class ContactsSectionHeaderAccessoryItemNode: ListViewAccessoryItemNode { private let sectionHeader: ContactsSectionHeader private let sectionHeaderNode: ListSectionHeaderNode + private var theme: PresentationTheme - init(sectionHeader: ContactsSectionHeader) { + init(sectionHeader: ContactsSectionHeader, theme: PresentationTheme) { self.sectionHeader = sectionHeader - self.sectionHeaderNode = ListSectionHeaderNode() + self.theme = theme + self.sectionHeaderNode = ListSectionHeaderNode(theme: theme) super.init() diff --git a/TelegramUI/ContactsVCardItem.swift b/TelegramUI/ContactsVCardItem.swift index ea2bcc48e4..022ecfb6eb 100644 --- a/TelegramUI/ContactsVCardItem.swift +++ b/TelegramUI/ContactsVCardItem.swift @@ -10,12 +10,16 @@ private let titleFont = Font.regular(20.0) private let statusFont = Font.regular(14.0) class ContactsVCardItem: ListViewItem { + let theme: PresentationTheme + let strings: PresentationStrings let account: Account let peer: Peer let action: (Peer) -> Void let selectable: Bool = true - init(account: Account, peer: Peer, action: @escaping (Peer) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer, action: @escaping (Peer) -> Void) { + self.theme = theme + self.strings = strings self.account = account self.peer = peer self.action = action @@ -25,7 +29,7 @@ class ContactsVCardItem: ListViewItem { async { let node = ContactsVCardItemNode() let makeLayout = node.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(self.account, self.peer, width, previousItem != nil, nextItem != nil) + let (nodeLayout, nodeApply) = makeLayout(self.account, self.peer, self, width, previousItem != nil, nextItem != nil) node.contentSize = nodeLayout.contentSize node.insets = nodeLayout.insets @@ -43,7 +47,7 @@ class ContactsVCardItem: ListViewItem { let first = previousItem == nil let last = nextItem == nil - let (nodeLayout, apply) = layout(self.account, self.peer, width, first, last) + let (nodeLayout, apply) = layout(self.account, self.peer, self, width, first, last) Queue.mainQueue().async { completion(nodeLayout, { apply() @@ -72,14 +76,13 @@ class ContactsVCardItemNode: ListViewItemNode { private var account: Account? private var peer: Peer? private var avatarState: (Account, Peer)? + private var item: ContactsVCardItem? required init() { self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true self.avatarNode = AvatarNode(font: Font.regular(15.0)) @@ -98,7 +101,7 @@ class ContactsVCardItemNode: ListViewItemNode { override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { let makeLayout = self.asyncLayout() - let (nodeLayout, nodeApply) = makeLayout(self.account, self.peer, width, previousItem != nil, nextItem != nil) + let (nodeLayout, nodeApply) = makeLayout(self.account, self.peer, item as! ContactsVCardItem, width, previousItem != nil, nextItem != nil) self.contentSize = nodeLayout.contentSize self.insets = nodeLayout.insets nodeApply() @@ -113,10 +116,6 @@ class ContactsVCardItemNode: ListViewItemNode { super.setHighlighted(highlighted, animated: animated) if highlighted { - /*self.contentNode.displaysAsynchronously = false - self.contentNode.backgroundColor = UIColor.clear - self.contentNode.isOpaque = false*/ - self.highlightedBackgroundNode.alpha = 1.0 if self.highlightedBackgroundNode.supernode == nil { self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) @@ -128,28 +127,30 @@ class ContactsVCardItemNode: ListViewItemNode { if let strongSelf = self { if completed { strongSelf.highlightedBackgroundNode.removeFromSupernode() - /*strongSelf.contentNode.backgroundColor = UIColor.white - strongSelf.contentNode.isOpaque = true - strongSelf.contentNode.displaysAsynchronously = true*/ } } }) self.highlightedBackgroundNode.alpha = 0.0 } else { self.highlightedBackgroundNode.removeFromSupernode() - /*self.contentNode.backgroundColor = UIColor.white - self.contentNode.isOpaque = true - self.contentNode.displaysAsynchronously = true*/ } } } } - func asyncLayout() -> (_ account: Account?, _ peer: Peer?, _ width: CGFloat, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ account: Account?, _ peer: Peer?, _ item: ContactsVCardItem, _ width: CGFloat, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) - return { [weak self] account, peer, width, first, last in + let currentItem = self.item + + return { [weak self] account, peer, item, width, first, last in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + let leftInset: CGFloat = 91.0 let rightInset: CGFloat = 10.0 @@ -158,17 +159,17 @@ class ContactsVCardItemNode: ListViewItemNode { if let peer = peer { if let user = peer as? TelegramUser { - titleAttributedString = NSAttributedString(string: user.displayTitle, font: titleFont, textColor: UIColor.black) + titleAttributedString = NSAttributedString(string: user.displayTitle, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) if let phone = user.phone { - statusAttributedString = NSAttributedString(string: formatPhoneNumber(phone), font: statusFont, textColor: UIColor(0xa6a6a6)) + statusAttributedString = NSAttributedString(string: formatPhoneNumber(phone), font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } } else if let group = peer as? TelegramGroup { - titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: UIColor.black) - statusAttributedString = NSAttributedString(string: "group", font: statusFont, textColor: UIColor(0xa6a6a6)) + titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) + statusAttributedString = NSAttributedString(string: item.strings.Group_Status, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } else if let channel = peer as? TelegramChannel { - titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: UIColor.black) - statusAttributedString = NSAttributedString(string: "channel", font: statusFont, textColor: UIColor(0xa6a6a6)) + titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) + statusAttributedString = NSAttributedString(string: item.strings.Channel_Status, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } } @@ -180,9 +181,16 @@ class ContactsVCardItemNode: ListViewItemNode { return (nodeLayout, { [weak self] in if let strongSelf = self { + strongSelf.item = item strongSelf.peer = peer strongSelf.account = account + if let _ = updatedTheme { + strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor + //strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + if let peer = peer, let account = account, strongSelf.avatarState == nil || strongSelf.avatarState!.0 !== account || !strongSelf.avatarState!.1.isEqual(peer) { strongSelf.avatarNode.setPeer(account: account, peer: peer) } diff --git a/TelegramUI/ConvertToSupergroupController.swift b/TelegramUI/ConvertToSupergroupController.swift index d163edd448..17db8470d2 100644 --- a/TelegramUI/ConvertToSupergroupController.swift +++ b/TelegramUI/ConvertToSupergroupController.swift @@ -123,16 +123,16 @@ public func convertToSupergroupController(account: Account, peerId: PeerId) -> V } }) - let signal = statePromise.get() + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> deliverOnMainQueue - |> map { state -> (ItemListControllerState, (ItemListNodeState, ConvertToSupergroupEntry.ItemGenerationArguments)) in + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, ConvertToSupergroupEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? if state.isConverting { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } - let controllerState = ItemListControllerState(title: .text("Supergroup"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Supergroup"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back")) let listState = ItemListNodeState(entries: convertToSupergroupEntries(), style: .blocks) return (controllerState, (listState, arguments)) @@ -141,7 +141,7 @@ public func convertToSupergroupController(account: Account, peerId: PeerId) -> V actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) replaceControllerImpl = { [weak controller] c in if let controller = controller { (controller.navigationController as? NavigationController)?.replaceAllButRootController(c, animated: true) diff --git a/TelegramUI/CounterContollerTitleView.swift b/TelegramUI/CounterContollerTitleView.swift index 48e96b879e..0cc902d1ab 100644 --- a/TelegramUI/CounterContollerTitleView.swift +++ b/TelegramUI/CounterContollerTitleView.swift @@ -12,14 +12,15 @@ struct CounterContollerTitle: Equatable { } final class CounterContollerTitleView: UIView { + private var theme: PresentationTheme private let titleNode: ASTextNode var title: CounterContollerTitle = CounterContollerTitle(title: "", counter: "") { didSet { if self.title != oldValue { let string = NSMutableAttributedString() - string.append(NSAttributedString(string: title.title, font: Font.medium(17.0), textColor: .black)) - string.append(NSAttributedString(string: " " + title.counter, font: Font.regular(15.0), textColor: .gray)) + string.append(NSAttributedString(string: title.title, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)) + string.append(NSAttributedString(string: " " + title.counter, font: Font.regular(15.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)) self.titleNode.attributedText = string self.setNeedsLayout() @@ -27,14 +28,16 @@ final class CounterContollerTitleView: UIView { } } - override init(frame: CGRect) { + init(theme: PresentationTheme) { + self.theme = theme + self.titleNode = ASTextNode() self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 1 self.titleNode.truncationMode = .byTruncatingTail self.titleNode.isOpaque = false - super.init(frame: frame) + super.init(frame: CGRect()) self.addSubnode(self.titleNode) } diff --git a/TelegramUI/CreateChannelController.swift b/TelegramUI/CreateChannelController.swift index e4a43d60ee..aed5db3f8e 100644 --- a/TelegramUI/CreateChannelController.swift +++ b/TelegramUI/CreateChannelController.swift @@ -18,11 +18,11 @@ private enum CreateChannelSection: Int32 { } private enum CreateChannelEntry: ItemListNodeEntry { - case channelInfo(Peer?, ItemListAvatarAndNameInfoItemState) - case setProfilePhoto + case channelInfo(PresentationTheme, PresentationStrings, Peer?, ItemListAvatarAndNameInfoItemState) + case setProfilePhoto(PresentationTheme, String) - case descriptionSetup(text: String) - case descriptionInfo + case descriptionSetup(PresentationTheme, String, String) + case descriptionInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -48,8 +48,14 @@ private enum CreateChannelEntry: ItemListNodeEntry { static func ==(lhs: CreateChannelEntry, rhs: CreateChannelEntry) -> Bool { switch lhs { - case let .channelInfo(lhsPeer, lhsEditingState): - if case let .channelInfo(rhsPeer, rhsEditingState) = rhs { + case let .channelInfo(lhsTheme, lhsStrings, lhsPeer, lhsEditingState): + if case let .channelInfo(rhsTheme, rhsStrings, rhsPeer, rhsEditingState) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false @@ -64,20 +70,20 @@ private enum CreateChannelEntry: ItemListNodeEntry { } else { return false } - case .setProfilePhoto: - if case .setProfilePhoto = rhs { + case let .setProfilePhoto(lhsTheme, lhsText): + if case let .setProfilePhoto(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .descriptionSetup(text): - if case .descriptionSetup(text) = rhs { + case let .descriptionSetup(lhsTheme, lhsText, lhsValue): + if case let .descriptionSetup(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case .descriptionInfo: - if case .descriptionInfo = rhs { + case let .descriptionInfo(lhsTheme, lhsText): + if case let .descriptionInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -91,23 +97,23 @@ private enum CreateChannelEntry: ItemListNodeEntry { func item(_ arguments: CreateChannelArguments) -> ListViewItem { switch self { - case let .channelInfo(peer, state): - return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks, editingNameUpdated: { editingName in + case let .channelInfo(theme, strings, peer, state): + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { }) - case .setProfilePhoto: - return ItemListActionItem(title: "Set Profile Photo", kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .setProfilePhoto(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { }) - case let .descriptionSetup(text): - return ItemListMultilineInputItem(text: text, placeholder: "Description", sectionId: self.section, style: .blocks, textUpdated: { updatedText in + case let .descriptionSetup(theme, text, value): + return ItemListMultilineInputItem(theme: theme, text: value, placeholder: text, sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }, action: { }) - case .descriptionInfo: - return ItemListTextItem(text: .plain("You can provide an optional description for your channel."), sectionId: self.section) + case let .descriptionInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } @@ -143,18 +149,18 @@ private struct CreateChannelState: Equatable { } } -private func CreateChannelEntries(state: CreateChannelState) -> [CreateChannelEntry] { +private func CreateChannelEntries(presentationData: PresentationData, state: CreateChannelState) -> [CreateChannelEntry] { var entries: [CreateChannelEntry] = [] let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil) let peer = TelegramGroup(id: PeerId(namespace: 100, id: 0), title: state.editingName.composedTitle, photo: [], participantCount: 0, role: .creator, membership: .Member, flags: [], migrationReference: nil, creationDate: 0, version: 0) - entries.append(.channelInfo(peer, groupInfoState)) - entries.append(.setProfilePhoto) + entries.append(.channelInfo(presentationData.theme, presentationData.strings, peer, groupInfoState)) + entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.Settings_SetProfilePhoto)) - entries.append(.descriptionSetup(text: state.editingDescriptionText)) - entries.append(.descriptionInfo) + entries.append(.descriptionSetup(presentationData.theme, presentationData.strings.Channel_Edit_AboutItem, state.editingDescriptionText)) + entries.append(.descriptionInfo(presentationData.theme, presentationData.strings.Channel_About_Help)) return entries } @@ -206,8 +212,8 @@ public func createChannelController(account: Account) -> ViewController { } }) - let signal = statePromise.get() - |> map { state -> (ItemListControllerState, (ItemListNodeState, CreateChannelEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, CreateChannelEntry.ItemGenerationArguments)) in let rightNavigationButton: ItemListNavigationButton if state.creating { @@ -218,16 +224,15 @@ public func createChannelController(account: Account) -> ViewController { }) } - let controllerState = ItemListControllerState(title: .text("Create Channel"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton) - let listState = ItemListNodeState(entries: CreateChannelEntries(state: state), style: .blocks) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Create Channel"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back")) + let listState = ItemListNodeState(entries: CreateChannelEntries(presentationData: presentationData, state: state), style: .blocks) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + let controller = ItemListController(account: account, state: signal) replaceControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true) } diff --git a/TelegramUI/CreateGroupController.swift b/TelegramUI/CreateGroupController.swift index b0a9f3e7ce..2585bd13d4 100644 --- a/TelegramUI/CreateGroupController.swift +++ b/TelegramUI/CreateGroupController.swift @@ -17,10 +17,10 @@ private enum CreateGroupSection: Int32 { } private enum CreateGroupEntry: ItemListNodeEntry { - case groupInfo(Peer?, ItemListAvatarAndNameInfoItemState) - case setProfilePhoto + case groupInfo(PresentationTheme, PresentationStrings, Peer?, ItemListAvatarAndNameInfoItemState) + case setProfilePhoto(PresentationTheme, String) - case member(Int32, Peer, PeerPresence?) + case member(Int32, PresentationTheme, PresentationStrings, Peer, PeerPresence?) var section: ItemListSectionId { switch self { @@ -37,15 +37,21 @@ private enum CreateGroupEntry: ItemListNodeEntry { return 0 case .setProfilePhoto: return 1 - case let .member(index, _, _): + case let .member(index, _, _, _, _): return 2 + index } } static func ==(lhs: CreateGroupEntry, rhs: CreateGroupEntry) -> Bool { switch lhs { - case let .groupInfo(lhsPeer, lhsEditingState): - if case let .groupInfo(rhsPeer, rhsEditingState) = rhs { + case let .groupInfo(lhsTheme, lhsStrings, lhsPeer, lhsEditingState): + if case let .groupInfo(rhsTheme, rhsStrings, rhsPeer, rhsEditingState) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false @@ -60,21 +66,26 @@ private enum CreateGroupEntry: ItemListNodeEntry { } else { return false } - case .setProfilePhoto: - if case .setProfilePhoto = rhs { + case let .setProfilePhoto(lhsTheme, lhsText): + if case let .setProfilePhoto(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .member(lhsIndex, lhsPeer, lhsPresence): - if case let .member(rhsIndex, rhsPeer, rhsPresence) = rhs { + case let .member(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsPresence): + if case let .member(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsPresence) = rhs { if lhsIndex != rhsIndex { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if !lhsPeer.isEqual(rhsPeer) { return false } - if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { if !lhsPresence.isEqual(to: rhsPresence) { return false @@ -95,17 +106,17 @@ private enum CreateGroupEntry: ItemListNodeEntry { func item(_ arguments: CreateGroupArguments) -> ListViewItem { switch self { - case let .groupInfo(peer, state): - return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks, editingNameUpdated: { editingName in + case let .groupInfo(theme, strings, peer, state): + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: nil, cachedData: nil, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { }) - case .setProfilePhoto: - return ItemListActionItem(title: "Set Profile Photo", kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .setProfilePhoto(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { }) - case let .member(_, peer, presence): - return ItemListPeerItem(account: arguments.account, peer: peer, presence: presence, text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _ in }, removePeer: { _ in }) + case let .member(_, theme, strings, peer, presence): + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: presence, text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _ in }, removePeer: { _ in }) } } } @@ -126,15 +137,15 @@ private struct CreateGroupState: Equatable { } } -private func createGroupEntries(state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView) -> [CreateGroupEntry] { +private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView) -> [CreateGroupEntry] { var entries: [CreateGroupEntry] = [] let groupInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingName, updatingName: nil) let peer = TelegramGroup(id: PeerId(namespace: 100, id: 0), title: state.editingName.composedTitle, photo: [], participantCount: 0, role: .creator, membership: .Member, flags: [], migrationReference: nil, creationDate: 0, version: 0) - entries.append(.groupInfo(peer, groupInfoState)) - entries.append(.setProfilePhoto) + entries.append(.groupInfo(presentationData.theme, presentationData.strings, peer, groupInfoState)) + entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.Settings_SetProfilePhoto)) var peers: [Peer] = [] for peerId in peerIds { @@ -164,7 +175,7 @@ private func createGroupEntries(state: CreateGroupState, peerIds: [PeerId], view }) for i in 0 ..< peers.count { - entries.append(.member(Int32(i), peers[i], view.presences[peers[i].id])) + entries.append(.member(Int32(i), presentationData.theme, presentationData.strings, peers[i], view.presences[peers[i].id])) } return entries @@ -210,8 +221,8 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo } }) - let signal = combineLatest(statePromise.get(), account.postbox.multiplePeersView(peerIds)) - |> map { state, view -> (ItemListControllerState, (ItemListNodeState, CreateGroupEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.postbox.multiplePeersView(peerIds)) + |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, CreateGroupEntry.ItemGenerationArguments)) in let rightNavigationButton: ItemListNavigationButton if state.creating { @@ -222,16 +233,15 @@ public func createGroupController(account: Account, peerIds: [PeerId]) -> ViewCo }) } - let controllerState = ItemListControllerState(title: .text("Create Group"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton) - let listState = ItemListNodeState(entries: createGroupEntries(state: state, peerIds: peerIds, view: view), style: .blocks) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Create Group"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back")) + let listState = ItemListNodeState(entries: createGroupEntries(presentationData: presentationData, state: state, peerIds: peerIds, view: view), style: .blocks) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + let controller = ItemListController(account: account, state: signal) replaceControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.replaceAllButRootController(value, animated: true) } diff --git a/TelegramUI/DataAndStorageSettingsController.swift b/TelegramUI/DataAndStorageSettingsController.swift index a64017f3fd..ed78e67363 100644 --- a/TelegramUI/DataAndStorageSettingsController.swift +++ b/TelegramUI/DataAndStorageSettingsController.swift @@ -45,22 +45,22 @@ private enum DataAndStorageSection: Int32 { } private enum DataAndStorageEntry: ItemListNodeEntry { - case storageUsage(String) - case networkUsage(String) - case automaticPhotoDownloadHeader(String) - case automaticPhotoDownloadPrivateChats(String, Bool) - case automaticPhotoDownloadGroupsAndChannels(String, Bool) - case automaticVoiceDownloadHeader(String) - case automaticVoiceDownloadPrivateChats(String, Bool) - case automaticVoiceDownloadGroupsAndChannels(String, Bool) - case automaticInstantVideoDownloadHeader(String) - case automaticInstantVideoDownloadPrivateChats(String, Bool) - case automaticInstantVideoDownloadGroupsAndChannels(String, Bool) - case voiceCallsHeader(String) - case useLessVoiceData(String, String) - case otherHeader(String) - case saveIncomingPhotos(String, Bool) - case saveEditedPhotos(String, Bool) + case storageUsage(PresentationTheme, String) + case networkUsage(PresentationTheme, String) + case automaticPhotoDownloadHeader(PresentationTheme, String) + case automaticPhotoDownloadPrivateChats(PresentationTheme, String, Bool) + case automaticPhotoDownloadGroupsAndChannels(PresentationTheme, String, Bool) + case automaticVoiceDownloadHeader(PresentationTheme, String) + case automaticVoiceDownloadPrivateChats(PresentationTheme, String, Bool) + case automaticVoiceDownloadGroupsAndChannels(PresentationTheme, String, Bool) + case automaticInstantVideoDownloadHeader(PresentationTheme, String) + case automaticInstantVideoDownloadPrivateChats(PresentationTheme, String, Bool) + case automaticInstantVideoDownloadGroupsAndChannels(PresentationTheme, String, Bool) + case voiceCallsHeader(PresentationTheme, String) + case useLessVoiceData(PresentationTheme, String, String) + case otherHeader(PresentationTheme, String) + case saveIncomingPhotos(PresentationTheme, String, Bool) + case saveEditedPhotos(PresentationTheme, String, Bool) var section: ItemListSectionId { switch self { @@ -118,98 +118,98 @@ private enum DataAndStorageEntry: ItemListNodeEntry { static func ==(lhs: DataAndStorageEntry, rhs: DataAndStorageEntry) -> Bool { switch lhs { - case let .storageUsage(text): - if case .storageUsage(text) = rhs { + case let .storageUsage(lhsTheme, lhsText): + if case let .storageUsage(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .networkUsage(text): - if case .networkUsage(text) = rhs { + case let .networkUsage(lhsTheme, lhsText): + if case let .networkUsage(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .automaticPhotoDownloadHeader(text): - if case .automaticPhotoDownloadHeader(text) = rhs { + case let .automaticPhotoDownloadHeader(lhsTheme, lhsText): + if case let .automaticPhotoDownloadHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .automaticPhotoDownloadPrivateChats(text, value): - if case .automaticPhotoDownloadPrivateChats(text, value) = rhs { + case let .automaticPhotoDownloadPrivateChats(lhsTheme, lhsText, lhsValue): + if case let .automaticPhotoDownloadPrivateChats(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .automaticPhotoDownloadGroupsAndChannels(text, value): - if case .automaticPhotoDownloadGroupsAndChannels(text, value) = rhs { + case let .automaticPhotoDownloadGroupsAndChannels(lhsTheme, lhsText, lhsValue): + if case let .automaticPhotoDownloadGroupsAndChannels(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .automaticVoiceDownloadHeader(text): - if case .automaticVoiceDownloadHeader(text) = rhs { + case let .automaticVoiceDownloadHeader(lhsTheme, lhsText): + if case let .automaticVoiceDownloadHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .automaticVoiceDownloadPrivateChats(text, value): - if case .automaticVoiceDownloadPrivateChats(text, value) = rhs { + case let .automaticVoiceDownloadPrivateChats(lhsTheme, lhsText, lhsValue): + if case let .automaticVoiceDownloadPrivateChats(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .automaticVoiceDownloadGroupsAndChannels(text, value): - if case .automaticVoiceDownloadGroupsAndChannels(text, value) = rhs { + case let .automaticVoiceDownloadGroupsAndChannels(lhsTheme, lhsText, lhsValue): + if case let .automaticVoiceDownloadGroupsAndChannels(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .automaticInstantVideoDownloadHeader(text): - if case .automaticInstantVideoDownloadHeader(text) = rhs { + case let .automaticInstantVideoDownloadHeader(lhsTheme, lhsText): + if case let .automaticInstantVideoDownloadHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .automaticInstantVideoDownloadPrivateChats(text, value): - if case .automaticInstantVideoDownloadPrivateChats(text, value) = rhs { + case let .automaticInstantVideoDownloadPrivateChats(lhsTheme, lhsText, lhsValue): + if case let .automaticInstantVideoDownloadPrivateChats(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .automaticInstantVideoDownloadGroupsAndChannels(text, value): - if case .automaticInstantVideoDownloadGroupsAndChannels(text, value) = rhs { + case let .automaticInstantVideoDownloadGroupsAndChannels(lhsTheme, lhsText, lhsValue): + if case let .automaticInstantVideoDownloadGroupsAndChannels(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .voiceCallsHeader(text): - if case .voiceCallsHeader(text) = rhs { + case let .voiceCallsHeader(lhsTheme, lhsText): + if case let .voiceCallsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .useLessVoiceData(text, value): - if case .useLessVoiceData(text, value) = rhs { + case let .useLessVoiceData(lhsTheme, lhsText, lhsValue): + if case let .useLessVoiceData(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .otherHeader(text): - if case .otherHeader(text) = rhs { + case let .otherHeader(lhsTheme, lhsText): + if case let .otherHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .saveIncomingPhotos(text, value): - if case .saveIncomingPhotos(text, value) = rhs { + case let .saveIncomingPhotos(lhsTheme, lhsText, lhsValue): + if case let .saveIncomingPhotos(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .saveEditedPhotos(text, value): - if case .saveEditedPhotos(text, value) = rhs { + case let .saveEditedPhotos(lhsTheme, lhsText, lhsValue): + if case let .saveEditedPhotos(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false @@ -223,58 +223,58 @@ private enum DataAndStorageEntry: ItemListNodeEntry { func item(_ arguments: DataAndStorageControllerArguments) -> ListViewItem { switch self { - case let .storageUsage(text): - return ItemListDisclosureItem(title: text, label: "", sectionId: self.section, style: .blocks, action: { + case let .storageUsage(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openStorageUsage() }) - case let .networkUsage(text): - return ItemListDisclosureItem(title: text, label: "", sectionId: self.section, style: .blocks, action: { + case let .networkUsage(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openNetworkUsage() }) - case let .automaticPhotoDownloadHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .automaticPhotoDownloadPrivateChats(text, value): - return ItemListSwitchItem(title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .automaticPhotoDownloadHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .automaticPhotoDownloadPrivateChats(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleAutomaticDownload(.photo, .privateChats, value) }) - case let .automaticPhotoDownloadGroupsAndChannels(text, value): - return ItemListSwitchItem(title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .automaticPhotoDownloadGroupsAndChannels(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleAutomaticDownload(.photo, .groupsAndChannels, value) }) - case let .automaticVoiceDownloadHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .automaticVoiceDownloadPrivateChats(text, value): - return ItemListSwitchItem(title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .automaticVoiceDownloadHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .automaticVoiceDownloadPrivateChats(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleAutomaticDownload(.voice, .privateChats, value) }) - case let .automaticVoiceDownloadGroupsAndChannels(text, value): - return ItemListSwitchItem(title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .automaticVoiceDownloadGroupsAndChannels(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleAutomaticDownload(.voice, .groupsAndChannels, value) }) - case let .automaticInstantVideoDownloadHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .automaticInstantVideoDownloadPrivateChats(text, value): - return ItemListSwitchItem(title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .automaticInstantVideoDownloadHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .automaticInstantVideoDownloadPrivateChats(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleAutomaticDownload(.instantVideo, .privateChats, value) }) - case let .automaticInstantVideoDownloadGroupsAndChannels(text, value): - return ItemListSwitchItem(title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .automaticInstantVideoDownloadGroupsAndChannels(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleAutomaticDownload(.instantVideo, .groupsAndChannels, value) }) - case let .voiceCallsHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .useLessVoiceData(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, action: { + case let .voiceCallsHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .useLessVoiceData(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openVoiceUseLessData() }) - case let .otherHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .saveIncomingPhotos(text, value): - return ItemListSwitchItem(title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .otherHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .saveIncomingPhotos(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleSaveIncomingPhotos(value) }) - case let .saveEditedPhotos(text, value): - return ItemListSwitchItem(title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .saveEditedPhotos(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleSaveEditedPhotos(value) }) } @@ -314,34 +314,34 @@ private func stringForUseLessDataSetting(_ settings: VoiceCallSettings) -> Strin } } -private func dataAndStorageControllerEntries(state: DataAndStorageControllerState, data: DataAndStorageData) -> [DataAndStorageEntry] { +private func dataAndStorageControllerEntries(state: DataAndStorageControllerState, data: DataAndStorageData, presentationData: PresentationData) -> [DataAndStorageEntry] { var entries: [DataAndStorageEntry] = [] - entries.append(.storageUsage("Storage Usage")) - entries.append(.networkUsage("Network Usage")) + entries.append(.storageUsage(presentationData.theme, "Storage Usage")) + entries.append(.networkUsage(presentationData.theme, "Network Usage")) - entries.append(.automaticPhotoDownloadHeader("AUTOMATIC PHOTO DOWNLOAD")) - entries.append(.automaticPhotoDownloadPrivateChats("Private Chats", data.automaticMediaDownloadSettings.categories.photo.privateChats)) - entries.append(.automaticPhotoDownloadGroupsAndChannels("Groups and Channels", data.automaticMediaDownloadSettings.categories.photo.groupsAndChannels)) + entries.append(.automaticPhotoDownloadHeader(presentationData.theme, "AUTOMATIC PHOTO DOWNLOAD")) + entries.append(.automaticPhotoDownloadPrivateChats(presentationData.theme, "Private Chats", data.automaticMediaDownloadSettings.categories.photo.privateChats)) + entries.append(.automaticPhotoDownloadGroupsAndChannels(presentationData.theme, "Groups and Channels", data.automaticMediaDownloadSettings.categories.photo.groupsAndChannels)) - entries.append(.automaticVoiceDownloadHeader("AUTOMATIC AUDIO DOWNLOAD")) - entries.append(.automaticVoiceDownloadPrivateChats("Private Chats", data.automaticMediaDownloadSettings.categories.voice.privateChats)) - entries.append(.automaticVoiceDownloadGroupsAndChannels("Groups and Channels", data.automaticMediaDownloadSettings.categories.voice.groupsAndChannels)) + entries.append(.automaticVoiceDownloadHeader(presentationData.theme, "AUTOMATIC AUDIO DOWNLOAD")) + entries.append(.automaticVoiceDownloadPrivateChats(presentationData.theme, "Private Chats", data.automaticMediaDownloadSettings.categories.voice.privateChats)) + entries.append(.automaticVoiceDownloadGroupsAndChannels(presentationData.theme, "Groups and Channels", data.automaticMediaDownloadSettings.categories.voice.groupsAndChannels)) - entries.append(.automaticInstantVideoDownloadHeader("AUTOMATIC VIDEO MESSAGE DOWNLOAD")) - entries.append(.automaticInstantVideoDownloadPrivateChats("Private Chats", data.automaticMediaDownloadSettings.categories.instantVideo.privateChats)) - entries.append(.automaticInstantVideoDownloadGroupsAndChannels("Groups and Channels", data.automaticMediaDownloadSettings.categories.instantVideo.groupsAndChannels)) + entries.append(.automaticInstantVideoDownloadHeader(presentationData.theme, "AUTOMATIC VIDEO MESSAGE DOWNLOAD")) + entries.append(.automaticInstantVideoDownloadPrivateChats(presentationData.theme, "Private Chats", data.automaticMediaDownloadSettings.categories.instantVideo.privateChats)) + entries.append(.automaticInstantVideoDownloadGroupsAndChannels(presentationData.theme, "Groups and Channels", data.automaticMediaDownloadSettings.categories.instantVideo.groupsAndChannels)) /*entries.append(.automaticGifDownloadHeader("AUTOMATIC GIF DOWNLOAD")) entries.append(.automaticGifDownloadPrivateChats("Private Chats", data.automaticMediaDownloadSettings.categories.gif.privateChats)) entries.append(.automaticGifDownloadGroupsAndChannels("Groups and Channels", data.automaticMediaDownloadSettings.categories.gif.groupsAndChannels))*/ - entries.append(.voiceCallsHeader("VOICE CALLS")) - entries.append(.useLessVoiceData("Use Less Data", stringForUseLessDataSetting(data.voiceCallSettings))) + entries.append(.voiceCallsHeader(presentationData.theme, "VOICE CALLS")) + entries.append(.useLessVoiceData(presentationData.theme, "Use Less Data", stringForUseLessDataSetting(data.voiceCallSettings))) - entries.append(.otherHeader("OTHER")) - entries.append(.saveIncomingPhotos("Save Incoming Photos", data.automaticMediaDownloadSettings.saveIncomingPhotos)) - entries.append(.saveEditedPhotos("Save Edited Photos", data.generatedMediaStoreSettings.storeEditedPhotos)) + entries.append(.otherHeader(presentationData.theme, "OTHER")) + entries.append(.saveIncomingPhotos(presentationData.theme, "Save Incoming Photos", data.automaticMediaDownloadSettings.saveIncomingPhotos)) + entries.append(.saveEditedPhotos(presentationData.theme, "Save Edited Photos", data.generatedMediaStoreSettings.storeEditedPhotos)) return entries } @@ -350,10 +350,6 @@ func dataAndStorageController(account: Account) -> ViewController { let initialState = DataAndStorageControllerState() let statePromise = ValuePromise(initialState, ignoreRepeated: true) - let stateValue = Atomic(value: initialState) - let updateState: ((DataAndStorageControllerState) -> DataAndStorageControllerState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } var pushControllerImpl: ((ViewController) -> Void)? @@ -435,19 +431,18 @@ func dataAndStorageController(account: Account) -> ViewController { }).start() }) - let signal = combineLatest(statePromise.get(), dataAndStorageDataPromise.get()) |> deliverOnMainQueue - |> map { state, dataAndStorageData -> (ItemListControllerState, (ItemListNodeState, DataAndStorageEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), dataAndStorageDataPromise.get()) |> deliverOnMainQueue + |> map { presentationData, state, dataAndStorageData -> (ItemListControllerState, (ItemListNodeState, DataAndStorageEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(title: .text("Data and Storage"), leftNavigationButton: nil, rightNavigationButton: nil, animateChanges: false) - let listState = ItemListNodeState(entries: dataAndStorageControllerEntries(state: state, data: dataAndStorageData), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Data and Storage"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: false) + let listState = ItemListNodeState(entries: dataAndStorageControllerEntries(state: state, data: dataAndStorageData, presentationData: presentationData), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + let controller = ItemListController(account: account, state: signal) pushControllerImpl = { [weak controller] c in if let controller = controller { diff --git a/TelegramUI/DebugAccountsController.swift b/TelegramUI/DebugAccountsController.swift index 1d51406874..da67d2ef65 100644 --- a/TelegramUI/DebugAccountsController.swift +++ b/TelegramUI/DebugAccountsController.swift @@ -111,15 +111,15 @@ public func debugAccountsController(account: Account, accountManager: AccountMan }).start() }) - let signal = accountManager.accountRecords() - |> map { view -> (ItemListControllerState, (ItemListNodeState, DebugAccountsControllerEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(title: .text("Accounts"), leftNavigationButton: nil, rightNavigationButton: nil) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, accountManager.accountRecords()) + |> map { presentationData, view -> (ItemListControllerState, (ItemListNodeState, DebugAccountsControllerEntry.ItemGenerationArguments)) in + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Accounts"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back")) let listState = ItemListNodeState(entries: debugAccountsControllerEntries(view: view), style: .blocks) return (controllerState, (listState, arguments)) } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window, with: a) } diff --git a/TelegramUI/DebugController.swift b/TelegramUI/DebugController.swift index 7ee47e4449..e41e926cc4 100644 --- a/TelegramUI/DebugController.swift +++ b/TelegramUI/DebugController.swift @@ -23,8 +23,8 @@ private enum DebugControllerSection: Int32 { } private enum DebugControllerEntry: ItemListNodeEntry { - case sendLogs - case accounts + case sendLogs(PresentationTheme) + case accounts(PresentationTheme) var section: ItemListSectionId { switch self { @@ -46,8 +46,18 @@ private enum DebugControllerEntry: ItemListNodeEntry { static func ==(lhs: DebugControllerEntry, rhs: DebugControllerEntry) -> Bool { switch lhs { - case .sendLogs, .accounts: - return lhs.stableId == rhs.stableId + case let .sendLogs(lhsTheme): + if case let .sendLogs(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } + case let .accounts(lhsTheme): + if case let .accounts(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } } } @@ -57,8 +67,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { func item(_ arguments: DebugControllerArguments) -> ListViewItem { switch self { - case .sendLogs: - return ItemListDisclosureItem(title: "Seng Logs", label: "", sectionId: self.section, style: .blocks, action: { + case let .sendLogs(theme): + return ItemListDisclosureItem(theme: theme, title: "Seng Logs", label: "", sectionId: self.section, style: .blocks, action: { let _ = (Logger.shared.collectLogs() |> deliverOnMainQueue).start(next: { logs in let controller = PeerSelectionController(account: arguments.account) @@ -77,19 +87,19 @@ private enum DebugControllerEntry: ItemListNodeEntry { arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) }) }) - case .accounts: - return ItemListDisclosureItem(title: "Accounts", label: "", sectionId: self.section, style: .blocks, action: { + case let .accounts(theme): + return ItemListDisclosureItem(theme: theme, title: "Accounts", label: "", sectionId: self.section, style: .blocks, action: { arguments.pushController(debugAccountsController(account: arguments.account, accountManager: arguments.accountManager)) }) } } } -private func debugControllerEntries() -> [DebugControllerEntry] { +private func debugControllerEntries(presentationData: PresentationData) -> [DebugControllerEntry] { var entries: [DebugControllerEntry] = [] - entries.append(.sendLogs) - entries.append(.accounts) + entries.append(.sendLogs(presentationData.theme)) + entries.append(.accounts(presentationData.theme)) return entries } @@ -104,15 +114,15 @@ public func debugController(account: Account, accountManager: AccountManager) -> pushControllerImpl?(controller) }) - let signal = Signal.single(Void()) - |> map { _ -> (ItemListControllerState, (ItemListNodeState, DebugControllerEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(title: .text("Debug"), leftNavigationButton: nil, rightNavigationButton: nil) - let listState = ItemListNodeState(entries: debugControllerEntries(), style: .blocks) + let signal = (account.applicationContext as! TelegramApplicationContext).presentationData + |> map { presentationData -> (ItemListControllerState, (ItemListNodeState, DebugControllerEntry.ItemGenerationArguments)) in + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Debug"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back")) + let listState = ItemListNodeState(entries: debugControllerEntries(presentationData: presentationData), style: .blocks) return (controllerState, (listState, arguments)) } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window, with: a) } diff --git a/TelegramUI/DeclareEncodables.swift b/TelegramUI/DeclareEncodables.swift index 47c8048457..3fee9e5e00 100644 --- a/TelegramUI/DeclareEncodables.swift +++ b/TelegramUI/DeclareEncodables.swift @@ -9,6 +9,8 @@ private var telegramUIDeclaredEncodables: Void = { declareEncodable(PresentationPasscodeSettings.self, f: { PresentationPasscodeSettings(decoder: $0) }) declareEncodable(AutomaticMediaDownloadSettings.self, f: { AutomaticMediaDownloadSettings(decoder: $0) }) declareEncodable(GeneratedMediaStoreSettings.self, f: { GeneratedMediaStoreSettings(decoder: $0) }) + declareEncodable(PresentationThemeSettings.self, f: { PresentationThemeSettings(decoder: $0) }) + declareEncodable(TelegramWallpaper.self, f: { TelegramWallpaper(decoder: $0) }) return }() diff --git a/TelegramUI/DefaultDarkPresentationTheme.swift b/TelegramUI/DefaultDarkPresentationTheme.swift new file mode 100644 index 0000000000..a32b5b1501 --- /dev/null +++ b/TelegramUI/DefaultDarkPresentationTheme.swift @@ -0,0 +1,210 @@ +import Foundation + +private let accentColor: UIColor = UIColor(rgb: 0xb2b2b2) +private let destructiveColor: UIColor = .red + +private let rootStatusBar = PresentationThemeRootNavigationStatusBar( + style: .white +) + +private let rootTabBar = PresentationThemeRootTabBar( + backgroundColor: UIColor(rgb: 0x121212), + separatorColor: UIColor(rgb: 0x1f1f1f), + iconColor: UIColor(rgb: 0x5e5e5e), + selectedIconColor: accentColor, + textColor: UIColor(rgb: 0x5e5e5e), + selectedTextColor: accentColor, + badgeBackgroundColor: UIColor(rgb: 0xff3600), + badgeTextColor: .white) + +private let rootNavigationBar = PresentationThemeRootNavigationBar( + buttonColor: accentColor, + primaryTextColor: accentColor, + secondaryTextColor: UIColor(rgb: 0x5e5e5e), + controlColor: UIColor(rgb: 0x5e5e5e), + accentTextColor: accentColor, + backgroundColor: UIColor(rgb: 0x121212), + separatorColor: UIColor(rgb: 0x1a1a1a) +) + +private let activeNavigationSearchBar = PresentationThemeActiveNavigationSearchBar( + backgroundColor: UIColor(rgb: 0x121212), + accentColor: accentColor, + inputFillColor: UIColor(rgb: 0x545454), + inputTextColor: accentColor, + inputPlaceholderTextColor: UIColor(rgb: 0x5e5e5e), + inputIconColor: UIColor(rgb: 0x5e5e5e), + separatorColor: UIColor(rgb: 0x1a1a1a) +) + +private let rootController = PresentationThemeRootController( + statusBar: rootStatusBar, + tabBar: rootTabBar, + navigationBar: rootNavigationBar, + activeNavigationSearchBar: activeNavigationSearchBar +) + +private let switchColors = PresentationThemeSwitch( + frameColor: UIColor(rgb: 0x545454), + handleColor: UIColor(rgb: 0x121212), + contentColor: UIColor(rgb: 0xb2b2b2) +) + +private let list = PresentationThemeList( + blocksBackgroundColor: UIColor(rgb: 0x121212), + plainBackgroundColor: UIColor(rgb: 0x121212), + itemPrimaryTextColor: UIColor(rgb: 0xb2b2b2), + itemSecondaryTextColor: UIColor(rgb: 0x545454), + itemDisabledTextColor: UIColor(rgb: 0x4d4d4d), + itemAccentColor: accentColor, + itemDestructiveColor: destructiveColor, + itemPlaceholderTextColor: UIColor(rgb: 0x4d4d4d), + itemBackgroundColor: UIColor(rgb: 0x121212), + itemHighlightedBackgroundColor: UIColor(rgb: 0x1b1b1b), + itemSeparatorColor: UIColor(rgb: 0x1a1a1a), + disclosureArrowColor: UIColor(rgb: 0x545454), + sectionHeaderTextColor: UIColor(rgb: 0x545454), + freeTextColor: UIColor(rgb: 0x545454), + freeTextErrorColor: UIColor(rgb: 0xcf3030), + freeTextSuccessColor: UIColor(rgb: 0x30cf30), + itemSwitchColors: switchColors +) + +private let chatList = PresentationThemeChatList( + backgroundColor: UIColor(rgb: 0x121212), + itemSeparatorColor: UIColor(rgb: 0x1a1a1a), + itemBackgroundColor: UIColor(rgb: 0x121212), + pinnedItemBackgroundColor: UIColor(rgb: 0x121212), + itemHighlightedBackgroundColor: UIColor(rgb: 0x1b1b1b), + titleColor: UIColor(rgb: 0xb2b2b2), + secretTitleColor: UIColor(rgb: 0xb2b2b2), + dateTextColor: UIColor(rgb: 0x545454), + authorNameColor: UIColor(rgb: 0xb2b2b2), + messageTextColor: UIColor(rgb: 0x545454), + messageDraftTextColor: UIColor(rgb: 0xdd4b39), + checkmarkColor: UIColor(rgb: 0x545454), + pendingIndicatorColor: UIColor(rgb: 0x545454), + unreadBadgeActiveBackgroundColor: UIColor(rgb: 0xb2b2b2), + unreadBadgeActiveTextColor: UIColor(rgb: 0x121212), + unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0x626262), + unreadBadgeInactiveTextColor:UIColor(rgb: 0x121212), + pinnedSearchBarColor: UIColor(rgb: 0x545454), + regularSearchBarColor: UIColor(rgb: 0x545454), + sectionHeaderFillColor: UIColor(rgb: 0x000000), + sectionHeaderTextColor: UIColor(rgb: 0x545454), + searchBarKeyboardColor: .dark +) + +private let bubble = PresentationThemeChatBubble( + incomingFillColor: UIColor(rgb: 0x1b1b1b), + incomingFillHighlightedColor: UIColor(rgb: 0x4b4b4b), + incomingStrokeColor: UIColor(rgb: 0x000000), + outgoingFillColor: UIColor(rgb: 0x1b1b1b), + outgoingFillHighlightedColor: UIColor(rgb: 0x4b4b4b), + outgoingStrokeColor: UIColor(rgb: 0x000000), + freeformFillColor: UIColor(rgb: 0x1b1b1b), + freeformFillHighlightedColor: UIColor(rgb: 0x4b4b4b), + freeformStrokeColor: UIColor(rgb: 0x000000), + infoFillColor: UIColor(rgb: 0x1b1b1b), + infoStrokeColor: UIColor(rgb: 0x000000), + incomingPrimaryTextColor: UIColor(rgb: 0xb2b2b2), + incomingSecondaryTextColor: UIColor(rgb: 0x545454), + incomingLinkTextColor: accentColor, + incomingLinkHighlightColor: accentColor.withAlphaComponent(0.5), + outgoingPrimaryTextColor: UIColor(rgb: 0xb2b2b2), + outgoingSecondaryTextColor: UIColor(rgb: 0x545454), + outgoingLinkTextColor: accentColor, + outgoingLinkHighlightColor: accentColor.withAlphaComponent(0.5), + infoPrimaryTextColor: UIColor(rgb: 0xb2b2b2), + infoLinkTextColor: accentColor, + incomingAccentColor: accentColor, + outgoingAccentColor: accentColor, + outgoingCheckColor: UIColor(rgb: 0x545454), + incomingPendingActivityColor: UIColor(rgb: 0x545454), + outgoingPendingActivityColor: UIColor(rgb: 0x545454), + mediaDateAndStatusFillColor: UIColor(white: 0.0, alpha: 0.5), + mediaDateAndStatusTextColor: .white, + incomingFileTitleColor: UIColor(rgb: 0xb2b2b2), + outgoingFileTitleColor: UIColor(rgb: 0xb2b2b2), + incomingFileDescriptionColor: UIColor(rgb: 0x545454), + outgoingFileDescriptionColor: UIColor(rgb: 0x545454), + incomingFileDurationColor: UIColor(rgb: 0x545454), + outgoingFileDurationColor: UIColor(rgb: 0x545454), + shareButtonFillColor: UIColor(rgb: 0xffffff, alpha: 0.2), + shareButtonForegroundColor: UIColor(rgb: 0xb2b2b2), + mediaOverlayControlBackgroundColor: UIColor(white: 0.0, alpha: 0.6), + mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 0.6), + actionButtonsFillColor: UIColor(rgb: 0x1b1b1b), + actionButtonsTextColor: UIColor(rgb: 0xb2b2b2) +) + +private let serviceMessage = PresentationThemeServiceMessage( + serviceMessageFillColor: UIColor(rgb: 0xffffff, alpha: 0.2), + serviceMessagePrimaryTextColor: UIColor(rgb: 0xb2b2b2), + unreadBarFillColor: UIColor(rgb: 0x1b1b1b), + unreadBarStrokeColor: UIColor(rgb: 0x000000), + unreadBarTextColor: UIColor(rgb: 0xb2b2b2), + dateFillStaticColor: UIColor(rgb: 0xffffff, alpha: 0.2), + dateFillFloatingColor: UIColor(rgb: 0xffffff, alpha: 0.2), + dateTextColor: UIColor(rgb: 0xb2b2b2) +) + +private let inputPanel = PresentationThemeChatInputPanel( + panelBackgroundColor: UIColor(rgb: 0x1b1b1b), + panelStrokeColor: UIColor(rgb: 0x000000), + panelControlAccentColor: accentColor, + panelControlColor: UIColor(rgb: 0x545454), + panelControlDisabledColor: UIColor(rgb: 0x545454, alpha: 0.5), + panelControlDestructiveColor: UIColor(rgb: 0xff3b30), + inputBackgroundColor: UIColor(rgb: 0x121212), + inputStrokeColor: UIColor(rgb: 0x000000), + inputPlaceholderColor: UIColor(rgb: 0xb2b2b2, alpha: 0.5), + inputTextColor: UIColor(rgb: 0xb2b2b2), + inputControlColor: UIColor(rgb: 0xb2b2b2, alpha: 0.5), + primaryTextColor: UIColor(rgb: 0xb2b2b2), + mediaRecordingDotColor: .white, + keyboardColor: .dark +) + +private let inputMediaPanel = PresentationThemeInputMediaPanel( + panelSerapatorColor: UIColor(rgb: 0x000000), + panelIconColor: UIColor(rgb: 0x545454), + panelHighlightedIconBackgroundColor: UIColor(rgb: 0x4b4b4b), + stickersBackgroundColor: UIColor(rgb: 0x1b1b1b), + stickersSectionTextColor: UIColor(rgb: 0xb2b2b2), + gifsBackgroundColor: UIColor(rgb: 0x1b1b1b) +) + +private let inputButtonPanel = PresentationThemeInputButtonPanel( + panelSerapatorColor: UIColor(rgb: 0x000000), + panelBackgroundColor: UIColor(rgb: 0x1b1b1b), + buttonFillColor: UIColor(rgb: 0x1b1b1b), + buttonStrokeColor: UIColor(rgb: 0x000000), + buttonHighlightedFillColor: UIColor(rgb: 0x4b4b4b), + buttonHighlightedStrokeColor: UIColor(rgb: 0x000000), + buttonTextColor: UIColor(rgb: 0xb2b2b2) +) + +private let historyNavigation = PresentationThemeChatHistoryNavigation( + fillColor: .white, + strokeColor: UIColor(rgb: 0x000000), + foregroundColor: UIColor(rgb: 0x4b4b4b), + badgeBackgroundColor: accentColor, + badgeTextColor: .black +) + +private let chat = PresentationThemeChat( + bubble: bubble, + serviceMessage: serviceMessage, + inputPanel: inputPanel, + inputMediaPanel: inputMediaPanel, + inputButtonPanel: inputButtonPanel, + historyNavigation: historyNavigation +) + +let defaultDarkPresentationTheme = PresentationTheme( + rootController: rootController, + list: list, + chatList: chatList, + chat: chat +) diff --git a/TelegramUI/DefaultPresentationStrings.swift b/TelegramUI/DefaultPresentationStrings.swift new file mode 100644 index 0000000000..7b16d84eba --- /dev/null +++ b/TelegramUI/DefaultPresentationStrings.swift @@ -0,0 +1,3 @@ +import Foundation + +let defaultPresentationStrings = PresentationStrings(languageCode: "en", dict: NSDictionary(contentsOf: URL(fileURLWithPath: Bundle.main.path(forResource: "Localizable", ofType: "strings", inDirectory: nil, forLocalization: "en")!)) as! [String : String]) diff --git a/TelegramUI/DefaultPresentationTheme.swift b/TelegramUI/DefaultPresentationTheme.swift new file mode 100644 index 0000000000..63a07ce59f --- /dev/null +++ b/TelegramUI/DefaultPresentationTheme.swift @@ -0,0 +1,210 @@ +import Foundation + +private let accentColor: UIColor = UIColor(rgb: 0x007ee5) +private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) + +private let rootStatusBar = PresentationThemeRootNavigationStatusBar( + style: .black +) + +private let rootTabBar = PresentationThemeRootTabBar( + backgroundColor: UIColor(rgb: 0xf7f7f7), + separatorColor: UIColor(rgb: 0xa3a3a3), + iconColor: UIColor(rgb: 0x929292), + selectedIconColor: accentColor, + textColor: UIColor(rgb: 0x929292), + selectedTextColor: accentColor, + badgeBackgroundColor: UIColor(rgb: 0xff3b30), + badgeTextColor: .white) + +private let rootNavigationBar = PresentationThemeRootNavigationBar( + buttonColor: accentColor, + primaryTextColor: .black, + secondaryTextColor: UIColor(rgb: 0x787878), + controlColor: UIColor(rgb: 0x7e8791), + accentTextColor: accentColor, + backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), + separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) +) + +private let activeNavigationSearchBar = PresentationThemeActiveNavigationSearchBar( + backgroundColor: .white, + accentColor: accentColor, + inputFillColor: UIColor(rgb: 0xe9e9e9), + inputTextColor: .black, + inputPlaceholderTextColor: UIColor(rgb: 0x8e8e93), + inputIconColor: UIColor(rgb: 0x8e8e93), + separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) +) + +private let rootController = PresentationThemeRootController( + statusBar: rootStatusBar, + tabBar: rootTabBar, + navigationBar: rootNavigationBar, + activeNavigationSearchBar: activeNavigationSearchBar +) + +private let switchColors = PresentationThemeSwitch( + frameColor: UIColor(rgb: 0xe0e0e0), + handleColor: UIColor(rgb: 0xffffff), + contentColor: UIColor(rgb: 0x42d451) +) + +private let list = PresentationThemeList( + blocksBackgroundColor: UIColor(rgb: 0xefeff4), + plainBackgroundColor: .white, + itemPrimaryTextColor: .black, + itemSecondaryTextColor: UIColor(rgb: 0x8e8e93), + itemDisabledTextColor: UIColor(rgb: 0x8e8e93), + itemAccentColor: accentColor, + itemDestructiveColor: destructiveColor, + itemPlaceholderTextColor: UIColor(rgb: 0xc8c8ce), + itemBackgroundColor: .white, + itemHighlightedBackgroundColor: UIColor(rgb: 0xd9d9d9), + itemSeparatorColor: UIColor(rgb: 0xc8c7cc), + disclosureArrowColor: UIColor(rgb: 0xbab9be), + sectionHeaderTextColor: UIColor(rgb: 0x6d6d72), + freeTextColor: UIColor(rgb: 0x6d6d72), + freeTextErrorColor: UIColor(rgb: 0xcf3030), + freeTextSuccessColor: UIColor(rgb: 0x26972c), + itemSwitchColors: switchColors +) + +private let chatList = PresentationThemeChatList( + backgroundColor: .white, + itemSeparatorColor: UIColor(rgb: 0xc8c7cc), + itemBackgroundColor: .white, + pinnedItemBackgroundColor: UIColor(rgb: 0xf7f7f7), + itemHighlightedBackgroundColor: UIColor(rgb: 0xd9d9d9), + titleColor: .black, + secretTitleColor: UIColor(rgb: 0x00a629), + dateTextColor: UIColor(rgb: 0x8e8e93), + authorNameColor: .black, + messageTextColor: UIColor(rgb: 0x8e8e93), + messageDraftTextColor: UIColor(rgb: 0xdd4b39), + checkmarkColor: UIColor(rgb: 0x21c004), + pendingIndicatorColor: UIColor(rgb: 0x8e8e93), + unreadBadgeActiveBackgroundColor: UIColor(rgb: 0x007ee5), + unreadBadgeActiveTextColor: .white, + unreadBadgeInactiveBackgroundColor: UIColor(rgb: 0xadb3bb), + unreadBadgeInactiveTextColor: .white, + pinnedSearchBarColor: UIColor(rgb: 0xdfdfdf), + regularSearchBarColor: UIColor(rgb: 0xe9e9e9), + sectionHeaderFillColor: UIColor(rgb: 0xf7f7f7), + sectionHeaderTextColor: UIColor(rgb: 0x8e8e93), + searchBarKeyboardColor: .light +) + +private let bubble = PresentationThemeChatBubble( + incomingFillColor: UIColor(rgb: 0xffffff), + incomingFillHighlightedColor: UIColor(rgb: 0xd9f4ff), + incomingStrokeColor: UIColor(rgb: 0x86A9C9, alpha: 0.5), + outgoingFillColor: UIColor(rgb: 0xE1FFC7), + outgoingFillHighlightedColor: UIColor(rgb: 0xc8ffa6), + outgoingStrokeColor: UIColor(rgb: 0x86A9C9, alpha: 0.5), + freeformFillColor: UIColor(rgb: 0xffffff), + freeformFillHighlightedColor: UIColor(rgb: 0xd9f4ff), + freeformStrokeColor: UIColor(rgb: 0x86A9C9, alpha: 0.5), + infoFillColor: UIColor(rgb: 0xffffff), + infoStrokeColor: UIColor(rgb: 0x86A9C9, alpha: 0.5), + incomingPrimaryTextColor: UIColor(rgb: 0x000000), + incomingSecondaryTextColor: UIColor(rgb: 0x525252, alpha: 0.6), + incomingLinkTextColor: UIColor(rgb: 0x004bad), + incomingLinkHighlightColor: accentColor.withAlphaComponent(0.3), + outgoingPrimaryTextColor: UIColor(rgb: 0x000000), + outgoingSecondaryTextColor: UIColor(rgb: 0x008c09, alpha: 0.8), + outgoingLinkTextColor: UIColor(rgb: 0x004bad), + outgoingLinkHighlightColor: accentColor.withAlphaComponent(0.3), + infoPrimaryTextColor: UIColor(rgb: 0x000000), + infoLinkTextColor: UIColor(rgb: 0x004bad), + incomingAccentColor: UIColor(rgb: 0x3ca7fe), + outgoingAccentColor: UIColor(rgb: 0x00a700), + outgoingCheckColor: UIColor(rgb: 0x19C700), + incomingPendingActivityColor: UIColor(rgb: 0x525252, alpha: 0.6), + outgoingPendingActivityColor: UIColor(rgb: 0x42b649), + mediaDateAndStatusFillColor: UIColor(white: 0.0, alpha: 0.5), + mediaDateAndStatusTextColor: .white, + incomingFileTitleColor: UIColor(rgb: 0x0b8bed), + outgoingFileTitleColor: UIColor(rgb: 0x3faa3c), + incomingFileDescriptionColor: UIColor(rgb: 0x999999), + outgoingFileDescriptionColor: UIColor(rgb: 0x6fb26a), + incomingFileDurationColor: UIColor(rgb: 0x525252, alpha: 0.6), + outgoingFileDurationColor: UIColor(rgb: 0x008c09, alpha: 0.8), + shareButtonFillColor: UIColor(rgb: 0x748391, alpha: 0.45), + shareButtonForegroundColor: .white, + mediaOverlayControlBackgroundColor: UIColor(white: 0.0, alpha: 0.6), + mediaOverlayControlForegroundColor: UIColor(white: 1.0, alpha: 0.6), + actionButtonsFillColor: UIColor(rgb: 0x596E89), + actionButtonsTextColor: .white +) + +private let serviceMessage = PresentationThemeServiceMessage( + serviceMessageFillColor: UIColor(rgb: 0x748391, alpha: 0.45), + serviceMessagePrimaryTextColor: .white, + unreadBarFillColor: UIColor(white: 1.0, alpha: 0.9), + unreadBarStrokeColor: UIColor(white: 0.0, alpha: 0.2), + unreadBarTextColor: UIColor(rgb: 0x86868d), + dateFillStaticColor: UIColor(rgb: 0x748391, alpha: 0.45), + dateFillFloatingColor: UIColor(rgb: 0x939fab, alpha: 0.5), + dateTextColor: .white +) + +private let inputPanel = PresentationThemeChatInputPanel( + panelBackgroundColor: UIColor(rgb: 0xf2f4f6), + panelStrokeColor: UIColor(rgb: 0xbdc2c7), + panelControlAccentColor: accentColor, + panelControlColor: UIColor(rgb: 0x727b87), + panelControlDisabledColor: UIColor(rgb: 0x727b87, alpha: 0.5), + panelControlDestructiveColor: UIColor(rgb: 0xff3b30), + inputBackgroundColor: UIColor(rgb: 0xffffff), + inputStrokeColor: UIColor(rgb: 0xd3d6da), + inputPlaceholderColor: UIColor(rgb: 0xbebec0), + inputTextColor: .black, + inputControlColor: UIColor(rgb: 0x9099A2, alpha: 0.6), + primaryTextColor: .black, + mediaRecordingDotColor: UIColor(rgb: 0xed2521), + keyboardColor: .light +) + +private let inputMediaPanel = PresentationThemeInputMediaPanel( + panelSerapatorColor: UIColor(rgb: 0xBEC2C6), + panelIconColor: UIColor(rgb: 0x9099A2), + panelHighlightedIconBackgroundColor: UIColor(rgb: 0x9099A2, alpha: 0.2), + stickersBackgroundColor: UIColor(rgb: 0xE8EBF0), + stickersSectionTextColor: UIColor(rgb: 0x9099A2), + gifsBackgroundColor: .white +) + +private let inputButtonPanel = PresentationThemeInputButtonPanel( + panelSerapatorColor: UIColor(rgb: 0xBEC2C6), + panelBackgroundColor: UIColor(rgb: 0x9099A2), + buttonFillColor: .white, + buttonStrokeColor: UIColor(rgb: 0xc3c7c9), + buttonHighlightedFillColor: UIColor(rgb: 0xa8b3c0), + buttonHighlightedStrokeColor: UIColor(rgb: 0xc3c7c9), + buttonTextColor: .black +) + +private let historyNavigation = PresentationThemeChatHistoryNavigation( + fillColor: .white, + strokeColor: UIColor(rgb: 0x000000, alpha: 0.15), + foregroundColor: UIColor(rgb: 0x88888D), + badgeBackgroundColor: accentColor, + badgeTextColor: .white +) + +private let chat = PresentationThemeChat( + bubble: bubble, + serviceMessage: serviceMessage, + inputPanel: inputPanel, + inputMediaPanel: inputMediaPanel, + inputButtonPanel: inputButtonPanel, + historyNavigation: historyNavigation +) + +let defaultPresentationTheme = PresentationTheme( + rootController: rootController, + list: list, + chatList: chatList, + chat: chat +) diff --git a/TelegramUI/DeleteChatInputPanelNode.swift b/TelegramUI/DeleteChatInputPanelNode.swift index 61197c0f78..ba7b94446b 100644 --- a/TelegramUI/DeleteChatInputPanelNode.swift +++ b/TelegramUI/DeleteChatInputPanelNode.swift @@ -8,7 +8,7 @@ import SwiftSignalKit final class DeleteChatInputPanelNode: ChatInputPanelNode { private let button: HighlightableButtonNode - private var presentationInterfaceState = ChatPresentationInterfaceState() + private var presentationInterfaceState: ChatPresentationInterfaceState? override init() { self.button = HighlightableButtonNode() @@ -37,7 +37,7 @@ final class DeleteChatInputPanelNode: ChatInputPanelNode { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState - self.button.setAttributedTitle(NSAttributedString(string: "Delete and Exit", font: Font.regular(17.0), textColor: UIColor(0xff3b30)), for: []) + self.button.setAttributedTitle(NSAttributedString(string: interfaceState.strings.GroupInfo_DeleteAndExit, font: Font.regular(17.0), textColor: interfaceState.theme.chat.inputPanel.panelControlDestructiveColor), for: []) } let buttonSize = self.button.measure(CGSize(width: width - 10.0, height: 100.0)) diff --git a/TelegramUI/EditAccessoryPanelNode.swift b/TelegramUI/EditAccessoryPanelNode.swift index b1014e1450..a209dcaeef 100644 --- a/TelegramUI/EditAccessoryPanelNode.swift +++ b/TelegramUI/EditAccessoryPanelNode.swift @@ -5,20 +5,6 @@ import Postbox import SwiftSignalKit import Display -private let lineImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(0x007ee5)) -private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x9099A2).cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - context.move(to: CGPoint(x: 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) - context.strokePath() - context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) - context.strokePath() -}) - final class EditAccessoryPanelNode: AccessoryPanelNode { let messageId: MessageId @@ -49,18 +35,21 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { } } - init(account: Account, messageId: MessageId) { + var theme: PresentationTheme + + init(account: Account, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings) { self.messageId = messageId + self.theme = theme self.closeButton = ASButtonNode() - self.closeButton.setImage(closeButtonImage, for: []) + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.closeButton.displaysAsynchronously = false self.lineNode = ASImageNode() self.lineNode.displayWithoutProcessing = true self.lineNode.displaysAsynchronously = false - self.lineNode.image = lineImage + self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) self.titleNode = ASTextNode() self.titleNode.truncationMode = .byTruncatingTail @@ -89,8 +78,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { text = messageText } - strongSelf.titleNode.attributedText = NSAttributedString(string: "Edit Message", font: Font.medium(15.0), textColor: UIColor(0x007ee5)) - strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: UIColor.black) + strongSelf.titleNode.attributedText = NSAttributedString(string: "Edit Message", font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) + strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.primaryTextColor) strongSelf.setNeedsLayout() } @@ -102,6 +91,26 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.editingMessageDisposable.dispose() } + override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if self.theme !== theme { + self.theme = theme + + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) + + self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) + + if let text = self.titleNode.attributedText?.string { + self.titleNode.attributedText = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + } + + if let text = self.textNode.attributedText?.string { + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) + } + + self.setNeedsLayout() + } + } + override func didLoad() { super.didLoad() diff --git a/TelegramUI/EditableTokenListNode.swift b/TelegramUI/EditableTokenListNode.swift index 4dc745dc00..33eb3f2dca 100644 --- a/TelegramUI/EditableTokenListNode.swift +++ b/TelegramUI/EditableTokenListNode.swift @@ -7,7 +7,7 @@ struct EditableTokenListToken { let title: String } -private let caretIndicatorImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(0x3350ee)) +private let caretIndicatorImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(rgb: 0x3350ee)) private func caretAnimation() -> CAAnimation { let animation = CAKeyframeAnimation(keyPath: "opacity") @@ -24,18 +24,38 @@ private func caretAnimation() -> CAAnimation { return animation } +final class EditableTokenListNodeTheme { + let backgroundColor: UIColor + let separatorColor: UIColor + let placeholderTextColor: UIColor + let primaryTextColor: UIColor + let selectedTextColor: UIColor + let keyboardColor: PresentationThemeKeyboardColor + + init(backgroundColor: UIColor, separatorColor: UIColor, placeholderTextColor: UIColor, primaryTextColor: UIColor, selectedTextColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) { + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + self.placeholderTextColor = placeholderTextColor + self.primaryTextColor = primaryTextColor + self.selectedTextColor = selectedTextColor + self.keyboardColor = keyboardColor + } +} + private final class TokenNode: ASDisplayNode { + let theme: EditableTokenListNodeTheme let token: EditableTokenListToken let titleNode: ASTextNode var isSelected: Bool { didSet { if self.isSelected != oldValue { - self.titleNode.attributedText = NSAttributedString(string: token.title + ",", font: Font.regular(15.0), textColor: self.isSelected ? UIColor(0x007ee5) : UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: token.title + ",", font: Font.regular(15.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor) } } } - init(token: EditableTokenListToken, isSelected: Bool) { + init(theme: EditableTokenListNodeTheme, token: EditableTokenListToken, isSelected: Bool) { + self.theme = theme self.token = token self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true @@ -44,7 +64,7 @@ private final class TokenNode: ASDisplayNode { super.init() - self.titleNode.attributedText = NSAttributedString(string: token.title + ",", font: Font.regular(15.0), textColor: self.isSelected ? UIColor(0x007ee5) : UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: token.title + ",", font: Font.regular(15.0), textColor: self.isSelected ? self.theme.selectedTextColor : self.theme.primaryTextColor) self.addSubnode(self.titleNode) } @@ -73,6 +93,7 @@ private final class CaretIndicatorNode: ASImageNode { } final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { + private let theme: EditableTokenListNodeTheme private let placeholderNode: ASTextNode private var tokenNodes: [TokenNode] = [] private let separatorNode: ASDisplayNode @@ -83,14 +104,23 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { var textUpdated: ((String) -> Void)? var deleteToken: ((AnyHashable) -> Void)? - override init() { + init(theme: EditableTokenListNodeTheme, placeholder: String) { + self.theme = theme + self.placeholderNode = ASTextNode() self.placeholderNode.isLayerBacked = true self.placeholderNode.maximumNumberOfLines = 1 - self.placeholderNode.attributedText = NSAttributedString(string: "Whom would you like to message?", font: Font.regular(15.0), textColor: UIColor(0x8e8e92)) + self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(15.0), textColor: theme.placeholderTextColor) self.textFieldNode = TextFieldNode() self.textFieldNode.textField.font = Font.regular(15.0) + self.textFieldNode.textField.textColor = theme.primaryTextColor + switch theme.keyboardColor { + case .light: + self.textFieldNode.textField.keyboardAppearance = .default + case .dark: + self.textFieldNode.textField.keyboardAppearance = .dark + } self.caretIndicatorNode = CaretIndicatorNode() self.caretIndicatorNode.isLayerBacked = true @@ -100,11 +130,11 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = UIColor(0xc7c6cb) + self.separatorNode.backgroundColor = theme.separatorColor super.init() - self.backgroundColor = UIColor(0xf7f7f7) + self.backgroundColor = theme.backgroundColor self.addSubnode(self.placeholderNode) self.addSubnode(self.separatorNode) self.addSubnode(self.textFieldNode) @@ -170,7 +200,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { if let currentNode = currentNode { tokenNode = currentNode } else { - tokenNode = TokenNode(token: token, isSelected: self.selectedTokenId != nil && token.id == self.selectedTokenId!) + tokenNode = TokenNode(theme: self.theme, token: token, isSelected: self.selectedTokenId != nil && token.id == self.selectedTokenId!) self.tokenNodes.append(tokenNode) self.addSubnode(tokenNode) animateIn = true diff --git a/TelegramUI/FeaturedStickerPacksController.swift b/TelegramUI/FeaturedStickerPacksController.swift index 2a8d967d55..6b9029bacf 100644 --- a/TelegramUI/FeaturedStickerPacksController.swift +++ b/TelegramUI/FeaturedStickerPacksController.swift @@ -44,7 +44,7 @@ private enum FeaturedStickerPacksEntryId: Hashable { } private enum FeaturedStickerPacksEntry: ItemListNodeEntry { - case pack(Int32, StickerPackCollectionInfo, Bool, StickerPackItem?, Int32, Bool) + case pack(Int32, PresentationTheme, StickerPackCollectionInfo, Bool, StickerPackItem?, String, Bool) var section: ItemListSectionId { switch self { @@ -55,18 +55,21 @@ private enum FeaturedStickerPacksEntry: ItemListNodeEntry { var stableId: FeaturedStickerPacksEntryId { switch self { - case let .pack(_, info, _, _, _, _): + case let .pack(_, _, info, _, _, _, _): return .pack(info.id) } } static func ==(lhs: FeaturedStickerPacksEntry, rhs: FeaturedStickerPacksEntry) -> Bool { switch lhs { - case let .pack(lhsIndex, lhsInfo, lhsUnread, lhsTopItem, lhsCount, lhsInstalled): - if case let .pack(rhsIndex, rhsInfo, rhsUnread, rhsTopItem, rhsCount, rhsInstalled) = rhs { + case let .pack(lhsIndex, lhsTheme, lhsInfo, lhsUnread, lhsTopItem, lhsCount, lhsInstalled): + if case let .pack(rhsIndex, rhsTheme, rhsInfo, rhsUnread, rhsTopItem, rhsCount, rhsInstalled) = rhs { if lhsIndex != rhsIndex { return false } + if lhsTheme !== rhsTheme { + return false + } if lhsInfo != rhsInfo { return false } @@ -91,9 +94,9 @@ private enum FeaturedStickerPacksEntry: ItemListNodeEntry { static func <(lhs: FeaturedStickerPacksEntry, rhs: FeaturedStickerPacksEntry) -> Bool { switch lhs { - case let .pack(lhsIndex, _, _, _, _, _): + case let .pack(lhsIndex, _, _, _, _, _, _): switch rhs { - case let .pack(rhsIndex, _, _, _, _, _): + case let .pack(rhsIndex, _, _, _, _, _, _): return lhsIndex < rhsIndex } } @@ -101,8 +104,8 @@ private enum FeaturedStickerPacksEntry: ItemListNodeEntry { func item(_ arguments: FeaturedStickerPacksControllerArguments) -> ListViewItem { switch self { - case let .pack(_, info, unread, topItem, count, installed): - return ItemListStickerPackItem(account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false), enabled: true, sectionId: self.section, action: { _ in + case let .pack(_, theme, info, unread, topItem, count, installed): + return ItemListStickerPackItem(theme: theme, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false), enabled: true, sectionId: self.section, action: { _ in arguments.openStickerPack(info) }, setPackIdWithRevealedOptions: { _ in }, addPack: { @@ -122,7 +125,15 @@ private struct FeaturedStickerPacksControllerState: Equatable { } } -private func featuredStickerPacksControllerEntries(state: FeaturedStickerPacksControllerState, view: CombinedView, featured: [FeaturedStickerPackItem], unreadPacks: [ItemCollectionId: Bool]) -> [FeaturedStickerPacksEntry] { +private func stringForStickerCount(_ count: Int32) -> String { + if count == 1 { + return "1 sticker" + } else { + return "\(count) stickers" + } +} + +private func featuredStickerPacksControllerEntries(presentationData: PresentationData, state: FeaturedStickerPacksControllerState, view: CombinedView, featured: [FeaturedStickerPackItem], unreadPacks: [ItemCollectionId: Bool]) -> [FeaturedStickerPacksEntry] { var entries: [FeaturedStickerPacksEntry] = [] if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView, !featured.isEmpty { @@ -137,7 +148,7 @@ private func featuredStickerPacksControllerEntries(state: FeaturedStickerPacksCo if let value = unreadPacks[item.info.id] { unread = value } - entries.append(.pack(index, item.info, unread, item.topItems.first, item.info.count, installedPacks.contains(item.info.id))) + entries.append(.pack(index, presentationData.theme, item.info, unread, item.topItems.first, stringForStickerCount(item.info.count), installedPacks.contains(item.info.id))) index += 1 } } @@ -175,8 +186,9 @@ public func featuredStickerPacksController(account: Account) -> ViewController { var previousPackCount: Int? var initialUnreadPacks: [ItemCollectionId: Bool] = [:] - let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, featured.get() |> deliverOnMainQueue) - |> map { state, view, featured -> (ItemListControllerState, (ItemListNodeState, FeaturedStickerPacksEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, featured.get() |> deliverOnMainQueue) + |> deliverOnMainQueue + |> map { presentationData, state, view, featured -> (ItemListControllerState, (ItemListNodeState, FeaturedStickerPacksEntry.ItemGenerationArguments)) in let packCount: Int? = featured.count for item in featured { @@ -189,15 +201,15 @@ public func featuredStickerPacksController(account: Account) -> ViewController { let previous = previousPackCount previousPackCount = packCount - let controllerState = ItemListControllerState(title: .text("Trending Stickers"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.FeaturedStickerPacks_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: featuredStickerPacksControllerEntries(state: state, view: view, featured: featured, unreadPacks: initialUnreadPacks), style: .blocks, animateChanges: false) + let listState = ItemListNodeState(entries: featuredStickerPacksControllerEntries(presentationData: presentationData, state: state, view: view, featured: featured, unreadPacks: initialUnreadPacks), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) var alreadyReadIds = Set() @@ -205,7 +217,7 @@ public func featuredStickerPacksController(account: Account) -> ViewController { var unreadIds: [ItemCollectionId] = [] for entry in entries { switch entry { - case let .pack(_, info, unread, _, _, _): + case let .pack(_, _, info, unread, _, _, _): if unread && !alreadyReadIds.contains(info.id) { unreadIds.append(info.id) } @@ -218,7 +230,6 @@ public func featuredStickerPacksController(account: Account) -> ViewController { } } - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/ForwardAccessoryPanelNode.swift b/TelegramUI/ForwardAccessoryPanelNode.swift index 6c2f4f2ea5..d62b80d690 100644 --- a/TelegramUI/ForwardAccessoryPanelNode.swift +++ b/TelegramUI/ForwardAccessoryPanelNode.swift @@ -5,20 +5,6 @@ import Postbox import SwiftSignalKit import Display -private let lineImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(0x007ee5)) -private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x9099A2).cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - context.move(to: CGPoint(x: 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) - context.strokePath() - context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) - context.strokePath() -}) - func textStringForForwardedMessage(_ message: Message) -> (String, Bool) { if !message.text.isEmpty { return (message.text, false) @@ -85,18 +71,21 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { let titleNode: ASTextNode let textNode: ASTextNode - init(account: Account, messageIds: [MessageId]) { + var theme: PresentationTheme + + init(account: Account, messageIds: [MessageId], theme: PresentationTheme, strings: PresentationStrings) { self.messageIds = messageIds + self.theme = theme self.closeButton = ASButtonNode() - self.closeButton.setImage(closeButtonImage, for: []) + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.closeButton.displaysAsynchronously = false self.lineNode = ASImageNode() self.lineNode.displayWithoutProcessing = true self.lineNode.displaysAsynchronously = false - self.lineNode.image = lineImage + self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) self.titleNode = ASTextNode() self.titleNode.truncationMode = .byTruncatingTail @@ -139,8 +128,8 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { text = "\(messages.count) messages" } - strongSelf.titleNode.attributedText = NSAttributedString(string: authors, font: Font.medium(15.0), textColor: UIColor(0x007ee5)) - strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: UIColor.black) + strongSelf.titleNode.attributedText = NSAttributedString(string: authors, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) + strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.primaryTextColor) strongSelf.setNeedsLayout() } @@ -151,6 +140,26 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.messageDisposable.dispose() } + override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if self.theme !== theme { + self.theme = theme + + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) + + self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) + + if let text = self.titleNode.attributedText?.string { + self.titleNode.attributedText = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + } + + if let text = self.textNode.attributedText?.string { + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) + } + + self.setNeedsLayout() + } + } + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: constrainedSize.width, height: 45.0) } diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index a590abee91..d1c0ee4fb8 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -69,20 +69,20 @@ private func mediaForMessage(message: Message) -> Media? { return nil } -func galleryItemForEntry(account: Account, entry: MessageHistoryEntry) -> GalleryItem { +func galleryItemForEntry(account: Account, theme: PresentationTheme, strings: PresentationStrings, entry: MessageHistoryEntry) -> GalleryItem { switch entry { case let .MessageEntry(message, _, location, _): if let media = mediaForMessage(message: message) { if let _ = media as? TelegramMediaImage { - return ChatImageGalleryItem(account: account, message: message, location: location) + return ChatImageGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } else if let file = media as? TelegramMediaFile { if file.isVideo || file.mimeType.hasPrefix("video/") { - return ChatVideoGalleryItem(account: account, message: message, location: location) + return ChatVideoGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } else { if file.mimeType.hasPrefix("image/") { - return ChatImageGalleryItem(account: account, message: message, location: location) + return ChatImageGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } else { - return ChatDocumentGalleryItem(account: account, message: message, location: location) + return ChatDocumentGalleryItem(account: account, theme: theme, strings: strings, message: message, location: location) } } } @@ -128,11 +128,15 @@ private enum GalleryMessageHistoryView { } class GalleryController: ViewController { + static let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, primaryTextColor: .white, backgroundColor: UIColor(white: 0.0, alpha: 0.6), separatorColor: UIColor(white: 0.0, alpha: 0.8)) + static let lightNavigationTheme = NavigationBarTheme(buttonColor: UIColor(rgb: 0x007ee5), primaryTextColor: .black, backgroundColor: UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0), separatorColor: UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0)) + private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode } private let account: Account + private var presentationData: PresentationData private let _ready = Promise() override var ready: Promise { @@ -162,14 +166,11 @@ class GalleryController: ViewController { self.account = account self.replaceRootController = replaceRootController - super.init() + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.6) - self.navigationBar.stripeColor = UIColor.clear - self.navigationBar.foregroundColor = UIColor.white - self.navigationBar.accentColor = UIColor.white + super.init(navigationBarTheme: GalleryController.darkNavigationTheme) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.donePressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) self.statusBar.statusBarStyle = .White @@ -207,7 +208,7 @@ class GalleryController: ViewController { } } if strongSelf.isViewLoaded { - strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ galleryItemForEntry(account: account, entry: $0) }), centralItemIndex: strongSelf.centralEntryIndex) + strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ galleryItemForEntry(account: account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, entry: $0) }), centralItemIndex: strongSelf.centralEntryIndex) let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in strongSelf?.didSetReady = true @@ -237,19 +238,13 @@ class GalleryController: ViewController { switch style { case .dark: strongSelf.statusBar.statusBarStyle = .White - strongSelf.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - strongSelf.navigationBar.stripeColor = UIColor.clear - strongSelf.navigationBar.foregroundColor = UIColor.white - strongSelf.navigationBar.accentColor = UIColor.white + strongSelf.navigationBar?.updateTheme(GalleryController.darkNavigationTheme) strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor.black strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = true case .light: strongSelf.statusBar.statusBarStyle = .Black - strongSelf.navigationBar.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0) - strongSelf.navigationBar.foregroundColor = UIColor.black - strongSelf.navigationBar.accentColor = UIColor(0x007ee5) - strongSelf.navigationBar.stripeColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) - strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor(0xbdbdc2) + strongSelf.navigationBar?.updateTheme(GalleryController.lightNavigationTheme) + strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor(rgb: 0xbdbdc2) strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = false } } @@ -333,7 +328,7 @@ class GalleryController: ViewController { self?.presentingViewController?.dismiss(animated: false, completion: nil) } - self.galleryNode.pager.replaceItems(self.entries.map({ galleryItemForEntry(account: self.account, entry: $0) }), centralItemIndex: self.centralEntryIndex) + self.galleryNode.pager.replaceItems(self.entries.map({ galleryItemForEntry(account: self.account, theme: self.presentationData.theme, strings: self.presentationData.strings, entry: $0) }), centralItemIndex: self.centralEntryIndex) self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in if let strongSelf = self { diff --git a/TelegramUI/GalleryControllerNode.swift b/TelegramUI/GalleryControllerNode.swift index f71e8afe75..eed8bf5a61 100644 --- a/TelegramUI/GalleryControllerNode.swift +++ b/TelegramUI/GalleryControllerNode.swift @@ -6,6 +6,7 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { var statusBar: StatusBar? var navigationBar: NavigationBar? let footerNode: GalleryFooterNode + var toolbarNode: ASDisplayNode? var transitionNodeForCentralItem: (() -> ASDisplayNode?)? var dismiss: (() -> Void)? @@ -25,11 +26,11 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - init(controllerInteraction: GalleryControllerInteraction) { + init(controllerInteraction: GalleryControllerInteraction, pageGap: CGFloat = 20.0) { self.backgroundNode = ASDisplayNode() self.backgroundNode.backgroundColor = UIColor.black self.scrollView = UIScrollView() - self.pager = GalleryPagerNode() + self.pager = GalleryPagerNode(pageGap: pageGap) self.footerNode = GalleryFooterNode(controllerInteraction: controllerInteraction) super.init(viewBlock: { @@ -99,6 +100,10 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { self.footerNode.alpha = 1.0 }) + if let toolbarNode = self.toolbarNode { + toolbarNode.layer.animatePosition(from: CGPoint(x: 0.0, y: self.bounds.size.height), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + if animateContent { self.scrollView.layer.animateBounds(from: self.scrollView.layer.bounds.offsetBy(dx: 0.0, dy: -self.scrollView.layer.bounds.size.height), to: self.scrollView.layer.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) } @@ -124,6 +129,10 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { intermediateCompletion() }) + if let toolbarNode = self.toolbarNode { + toolbarNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.bounds.size.height), duration: 0.25, timingFunction: kCAMediaTimingFunctionLinear, removeOnCompletion: false, additive: true) + } + if animateContent { contentAnimationCompleted = false self.scrollView.layer.animateBounds(from: self.scrollView.layer.bounds, to: self.scrollView.layer.bounds.offsetBy(dx: 0.0, dy: -self.scrollView.layer.bounds.size.height), duration: 0.25, timingFunction: kCAMediaTimingFunctionLinear, removeOnCompletion: false, completion: { _ in @@ -145,13 +154,19 @@ class GalleryControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigationBar?.alpha = transition self.footerNode.alpha = transition } + + if let toolbarNode = toolbarNode { + toolbarNode.layer.position = CGPoint(x: toolbarNode.layer.position.x, y: self.bounds.size.height - toolbarNode.bounds.size.height / 2.0 + (1.0 - transition) * toolbarNode.bounds.size.height) + } } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { targetContentOffset.pointee = scrollView.contentOffset if abs(velocity.y) > 1.0 { - self.backgroundNode.layer.animate(from: self.backgroundNode.backgroundColor!, to: UIColor(white: 0.0, alpha: 0.0).cgColor, keyPath: "backgroundColor", timingFunction: kCAMediaTimingFunctionLinear, duration: 0.2, removeOnCompletion: false) + if let backgroundColor = self.backgroundNode.backgroundColor { + self.backgroundNode.layer.animate(from: backgroundColor, to: UIColor(white: 0.0, alpha: 0.0).cgColor, keyPath: "backgroundColor", timingFunction: kCAMediaTimingFunctionLinear, duration: 0.2, removeOnCompletion: false) + } var interfaceAnimationCompleted = false var contentAnimationCompleted = true diff --git a/TelegramUI/GalleryPagerNode.swift b/TelegramUI/GalleryPagerNode.swift index 5ea20e2aa0..f24192a674 100644 --- a/TelegramUI/GalleryPagerNode.swift +++ b/TelegramUI/GalleryPagerNode.swift @@ -4,7 +4,7 @@ import Display import SwiftSignalKit final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { - private let pageGap: CGFloat = 20.0 + private let pageGap: CGFloat private let scrollView: UIScrollView @@ -24,14 +24,16 @@ final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate { var centralItemIndexUpdated: (Int?) -> Void = { _ in } var toggleControlsVisibility: () -> Void = { } - override init() { + init(pageGap: CGFloat) { + self.pageGap = pageGap self.scrollView = UIScrollView() super.init() self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.alwaysBounceHorizontal = true + self.scrollView.alwaysBounceHorizontal = !pageGap.isZero + self.scrollView.bounces = !pageGap.isZero self.scrollView.isPagingEnabled = true self.scrollView.delegate = self self.scrollView.clipsToBounds = false diff --git a/TelegramUI/GeneratedMediaStoreSettings.swift b/TelegramUI/GeneratedMediaStoreSettings.swift index 774dc5ba8d..8f3b65b74a 100644 --- a/TelegramUI/GeneratedMediaStoreSettings.swift +++ b/TelegramUI/GeneratedMediaStoreSettings.swift @@ -14,7 +14,7 @@ public struct GeneratedMediaStoreSettings: PreferencesEntry, Equatable { } public init(decoder: Decoder) { - self.storeEditedPhotos = (decoder.decodeInt32ForKey("eph") as Int32) != 0 + self.storeEditedPhotos = decoder.decodeInt32ForKey("eph", orElse: 0) != 0 } public func encode(_ encoder: Encoder) { diff --git a/TelegramUI/GridMessageItem.swift b/TelegramUI/GridMessageItem.swift index 6998a784b7..8fd60ce747 100644 --- a/TelegramUI/GridMessageItem.swift +++ b/TelegramUI/GridMessageItem.swift @@ -33,6 +33,9 @@ private let timezoneOffset: Int32 = { final class GridMessageItemSection: GridSection { let height: CGFloat = 44.0 + private let theme: PresentationTheme + private let strings: PresentationStrings + private let roundedTimestamp: Int32 private let month: Int32 private let year: Int32 @@ -41,7 +44,10 @@ final class GridMessageItemSection: GridSection { return self.roundedTimestamp.hashValue } - init(timestamp: Int32) { + init(timestamp: Int32, theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + var now = time_t(timestamp) var timeinfoNow: tm = tm() localtime_r(&now, &timeinfoNow) @@ -60,26 +66,31 @@ final class GridMessageItemSection: GridSection { } func node() -> ASDisplayNode { - return GridMessageItemSectionNode(roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year) + return GridMessageItemSectionNode(theme: self.theme, strings: self.strings, roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year) } } private let sectionTitleFont = Font.regular(17.0) final class GridMessageItemSectionNode: ASDisplayNode { + var theme: PresentationTheme + var strings: PresentationStrings let titleNode: ASTextNode - init(roundedTimestamp: Int32, month: Int32, year: Int32) { + init(theme: PresentationTheme, strings: PresentationStrings, roundedTimestamp: Int32, month: Int32, year: Int32) { + self.theme = theme + self.strings = strings + self.titleNode = ASTextNode() self.titleNode.isLayerBacked = true super.init() - self.backgroundColor = UIColor(white: 1.0, alpha: 0.9) + self.backgroundColor = theme.list.plainBackgroundColor.withAlphaComponent(0.9) - let dateText = stringForMonth(month, ofYear: year) + let dateText = stringForMonth(strings: strings, month: month, ofYear: year) self.addSubnode(self.titleNode) - self.titleNode.attributedText = NSAttributedString(string: dateText, font: sectionTitleFont, textColor: .black) + self.titleNode.attributedText = NSAttributedString(string: dateText, font: sectionTitleFont, textColor: theme.list.itemPrimaryTextColor) self.titleNode.maximumNumberOfLines = 1 self.titleNode.truncationMode = .byTruncatingTail } @@ -95,17 +106,21 @@ final class GridMessageItemSectionNode: ASDisplayNode { } final class GridMessageItem: GridItem { + private let theme: PresentationTheme + private let strings: PresentationStrings private let account: Account private let message: Message private let controllerInteraction: ChatControllerInteraction let section: GridSection? - init(account: Account, message: Message, controllerInteraction: ChatControllerInteraction) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, message: Message, controllerInteraction: ChatControllerInteraction) { + self.theme = theme + self.strings = strings self.account = account self.message = message self.controllerInteraction = controllerInteraction - self.section = GridMessageItemSection(timestamp: message.timestamp) + self.section = GridMessageItemSection(timestamp: message.timestamp, theme: theme, strings: strings) } func node(layout: GridNodeLayout) -> GridItemNode { diff --git a/TelegramUI/GroupAdminsController.swift b/TelegramUI/GroupAdminsController.swift index 2acba0ae6a..bc7a0e3df6 100644 --- a/TelegramUI/GroupAdminsController.swift +++ b/TelegramUI/GroupAdminsController.swift @@ -347,9 +347,9 @@ public func groupAdminsController(account: Account, peerId: PeerId) -> ViewContr let peerView = account.viewTracker.peerView(peerId) - let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, peerView |> deliverOnMainQueue) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, peerView |> deliverOnMainQueue) |> deliverOnMainQueue - |> map { state, view -> (ItemListControllerState, (ItemListNodeState, GroupAdminsEntry.ItemGenerationArguments)) in + |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, GroupAdminsEntry.ItemGenerationArguments)) in var emptyStateItem: ItemListControllerEmptyStateItem? if view.cachedData == nil { @@ -361,7 +361,7 @@ public func groupAdminsController(account: Account, peerId: PeerId) -> ViewContr rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } - let controllerState = ItemListControllerState(title: .text("Admins"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Admins"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: true) let listState = ItemListNodeState(entries: groupAdminsControllerEntries(account: account, view: view, state: state), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: true) return (controllerState, (listState, arguments)) @@ -369,6 +369,6 @@ public func groupAdminsController(account: Account, peerId: PeerId) -> ViewContr actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) return controller } diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 06b0c53b11..ce9e3a6288 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -93,22 +93,22 @@ private enum GroupEntryStableId: Hashable, Equatable { } private enum GroupInfoEntry: ItemListNodeEntry { - case info(peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: TelegramMediaImageRepresentation?) - case setGroupPhoto - case about(String) - case link(String) - case sharedMedia - case notifications(settings: PeerNotificationSettings?) - case adminManagement - case groupTypeSetup(isPublic: Bool) - case groupDescriptionSetup(text: String) - case groupManagementInfoLabel(text: String) - case membersAdmins(count: Int) - case membersBlacklist(count: Int) - case addMember(editing: Bool) - case member(index: Int, peerId: PeerId, peer: Peer, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus, editing: ItemListPeerItemEditing, enabled: Bool) - case convertToSupergroup - case leave + case info(PresentationTheme, PresentationStrings, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: TelegramMediaImageRepresentation?) + case setGroupPhoto(PresentationTheme) + case about(PresentationTheme, String) + case link(PresentationTheme, String) + case sharedMedia(PresentationTheme) + case notifications(PresentationTheme, settings: PeerNotificationSettings?) + case adminManagement(PresentationTheme) + case groupTypeSetup(PresentationTheme, isPublic: Bool) + case groupDescriptionSetup(PresentationTheme, text: String) + case groupManagementInfoLabel(PresentationTheme, text: String) + case membersAdmins(PresentationTheme, count: Int) + case membersBlacklist(PresentationTheme, count: Int) + case addMember(PresentationTheme, editing: Bool) + case member(PresentationTheme, index: Int, peerId: PeerId, peer: Peer, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus, editing: ItemListPeerItemEditing, enabled: Bool) + case convertToSupergroup(PresentationTheme) + case leave(PresentationTheme) var section: ItemListSectionId { switch self { @@ -131,8 +131,14 @@ private enum GroupInfoEntry: ItemListNodeEntry { static func ==(lhs: GroupInfoEntry, rhs: GroupInfoEntry) -> Bool { switch lhs { - case let .info(lhsPeer, lhsCachedData, lhsState, lhsUpdatingAvatar): - if case let .info(rhsPeer, rhsCachedData, rhsState, rhsUpdatingAvatar) = rhs { + case let .info(lhsTheme, lhsStrings, lhsPeer, lhsCachedData, lhsState, lhsUpdatingAvatar): + if case let .info(rhsTheme, rhsStrings, rhsPeer, rhsCachedData, rhsState, rhsUpdatingAvatar) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false @@ -157,22 +163,53 @@ private enum GroupInfoEntry: ItemListNodeEntry { } else { return false } - case .setGroupPhoto, .sharedMedia, .leave, .convertToSupergroup, .adminManagement: - return lhs.sortIndex == rhs.sortIndex - case let .about(text): - if case .about(text) = rhs { + case let .setGroupPhoto(lhsTheme): + if case let .setGroupPhoto(rhsTheme) = rhs, lhsTheme === rhsTheme { return true } else { return false } - case let .link(text): - if case .link(text) = rhs { + case let .sharedMedia(lhsTheme): + if case let .sharedMedia(rhsTheme) = rhs, lhsTheme === rhsTheme { return true } else { return false } - case let .notifications(lhsSettings): - if case let .notifications(rhsSettings) = rhs { + case let .leave(lhsTheme): + if case let .leave(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } + case let .convertToSupergroup(lhsTheme): + if case let .convertToSupergroup(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } + case let .adminManagement(lhsTheme): + if case let .adminManagement(rhsTheme) = rhs, lhsTheme === rhsTheme { + return true + } else { + return false + } + case let .about(lhsTheme, lhsText): + if case let .about(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .link(lhsTheme, lhsText): + if case let .link(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .notifications(lhsTheme, lhsSettings): + if case let .notifications(rhsTheme, rhsSettings) = rhs { + if lhsTheme !== rhsTheme { + return false + } if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { return lhsSettings.isEqual(to: rhsSettings) } else if (lhsSettings != nil) != (rhsSettings != nil) { @@ -182,44 +219,47 @@ private enum GroupInfoEntry: ItemListNodeEntry { } else { return false } - case let .groupTypeSetup(isPublic): - if case .groupTypeSetup(isPublic) = rhs { + case let .groupTypeSetup(lhsTheme, lhsIsPublic): + if case let .groupTypeSetup(rhsTheme, rhsIsPublic) = rhs, lhsTheme == rhsTheme, lhsIsPublic == rhsIsPublic { return true } else { return false } - case let .groupDescriptionSetup(text): - if case .groupDescriptionSetup(text) = rhs { + case let .groupDescriptionSetup(lhsTheme, lhsText): + if case let .groupDescriptionSetup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .groupManagementInfoLabel(text): - if case .groupManagementInfoLabel(text) = rhs { + case let .groupManagementInfoLabel(lhsTheme, lhsText): + if case let .groupManagementInfoLabel(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .membersAdmins(lhsCount): - if case let .membersAdmins(rhsCount) = rhs, lhsCount == rhsCount { + case let .membersAdmins(lhsTheme, lhsCount): + if case let .membersAdmins(rhsTheme, rhsCount) = rhs, lhsTheme === rhsTheme, lhsCount == rhsCount { return true } else { return false } - case let .membersBlacklist(lhsCount): - if case let .membersBlacklist(rhsCount) = rhs, lhsCount == rhsCount { + case let .membersBlacklist(lhsTheme, lhsCount): + if case let .membersBlacklist(rhsTheme, rhsCount) = rhs, lhsTheme === rhsTheme, lhsCount == rhsCount { return true } else { return false } - case let .addMember(editing): - if case .addMember(editing) = rhs { + case let .addMember(lhsTheme, lhsEditing): + if case let .addMember(rhsTheme, rhsEditing) = rhs, lhsTheme === rhsTheme, lhsEditing == rhsEditing { return true } else { return false } - case let .member(lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsMemberStatus, lhsEditing, lhsEnabled): - if case let .member(rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsMemberStatus, rhsEditing, rhsEnabled) = rhs { + case let .member(lhsTheme, lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsMemberStatus, lhsEditing, lhsEnabled): + if case let .member(rhsTheme, rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsMemberStatus, rhsEditing, rhsEnabled) = rhs { + if lhsTheme !== rhsTheme { + return false + } if lhsIndex != rhsIndex { return false } @@ -254,7 +294,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { var stableId: GroupEntryStableId { switch self { - case let .member(_, peerId, _, _, _, _, _): + case let .member(_, _, peerId, _, _, _, _, _): return .peer(peerId) default: return .index(self.sortIndex) @@ -289,7 +329,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { return 11 case .addMember: return 12 - case let .member(index, _, _, _, _, _, _): + case let .member(_, index, _, _, _, _, _, _): return 20 + index case .convertToSupergroup: return 100000 @@ -304,62 +344,62 @@ private enum GroupInfoEntry: ItemListNodeEntry { func item(_ arguments: GroupInfoArguments) -> ListViewItem { switch self { - case let .info(peer, cachedData, state, updatingAvatar): - return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks, editingNameUpdated: { editingName in + case let .info(theme, strings, peer, cachedData, state, updatingAvatar): + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks(withTopInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { arguments.tapAvatarAction() }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar) - case .setGroupPhoto: - return ItemListActionItem(title: "Set Group Photo", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .setGroupPhoto(theme): + return ItemListActionItem(theme: theme, title: "Set Group Photo", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.changeProfilePhoto() }) - case let .about(text): - return ItemListMultilineTextItem(text: text, sectionId: self.section, style: .blocks) - case let .link(url): - return ItemListActionItem(title: url, kind: .neutral, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .about(theme, text): + return ItemListMultilineTextItem(theme: theme, text: text, sectionId: self.section, style: .blocks) + case let .link(theme, url): + return ItemListActionItem(theme: theme, title: url, kind: .neutral, alignment: .natural, sectionId: self.section, style: .blocks, action: { }) - case let .notifications(settings): + case let .notifications(theme, settings): let label: String if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState { label = "Disabled" } else { label = "Enabled" } - return ItemListDisclosureItem(title: "Notifications", label: label, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(theme: theme, title: "Notifications", label: label, sectionId: self.section, style: .blocks, action: { arguments.changeNotificationMuteSettings() }) - case .sharedMedia: - return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .blocks, action: { + case let .sharedMedia(theme): + return ItemListDisclosureItem(theme: theme, title: "Shared Media", label: "", sectionId: self.section, style: .blocks, action: { arguments.openSharedMedia() }) - case .adminManagement: - return ItemListDisclosureItem(title: "Add Admins", label: "", sectionId: self.section, style: .blocks, action: { + case let .adminManagement(theme): + return ItemListDisclosureItem(theme: theme, title: "Add Admins", label: "", sectionId: self.section, style: .blocks, action: { arguments.openAdminManagement() }) - case let .addMember(editing): - return ItemListPeerActionItem(icon: addMemberPlusIcon, title: "Add Member", sectionId: self.section, editing: editing, action: { + case let .addMember(theme, editing): + return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: "Add Member", sectionId: self.section, editing: editing, action: { arguments.addMember() }) - case let .groupTypeSetup(isPublic): - return ItemListDisclosureItem(title: "Group Type", label: isPublic ? "Public" : "Private", sectionId: self.section, style: .blocks, action: { + case let .groupTypeSetup(theme, isPublic): + return ItemListDisclosureItem(theme: theme, title: "Group Type", label: isPublic ? "Public" : "Private", sectionId: self.section, style: .blocks, action: { arguments.presentController(channelVisibilityController(account: arguments.account, peerId: arguments.peerId, mode: .generic), ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet)) }) - case let .groupDescriptionSetup(text): - return ItemListMultilineInputItem(text: text, placeholder: "Group Description", sectionId: self.section, style: .blocks, textUpdated: { updatedText in + case let .groupDescriptionSetup(theme, text): + return ItemListMultilineInputItem(theme: theme, text: text, placeholder: "Group Description", sectionId: self.section, style: .blocks, textUpdated: { updatedText in arguments.updateEditingDescriptionText(updatedText) }, action: { }) - case let .membersAdmins(count): - return ItemListDisclosureItem(title: "Admins", label: "\(count)", sectionId: self.section, style: .blocks, action: { + case let .membersAdmins(theme, count): + return ItemListDisclosureItem(theme: theme, title: "Admins", label: "\(count)", sectionId: self.section, style: .blocks, action: { arguments.pushController(channelAdminsController(account: arguments.account, peerId: arguments.peerId)) }) - case let .membersBlacklist(count): - return ItemListDisclosureItem(title: "Blacklist", label: "\(count)", sectionId: self.section, style: .blocks, action: { + case let .membersBlacklist(theme, count): + return ItemListDisclosureItem(theme: theme, title: "Blacklist", label: "\(count)", sectionId: self.section, style: .blocks, action: { arguments.pushController(channelBlacklistController(account: arguments.account, peerId: arguments.peerId)) }) - case let .member(_, _, peer, presence, memberStatus, editing, enabled): + case let .member(theme, _, _, peer, presence, memberStatus, editing, enabled): let label: String? switch memberStatus { case .admin: @@ -367,7 +407,7 @@ private enum GroupInfoEntry: ItemListNodeEntry { case .member: label = nil } - return ItemListPeerItem(account: arguments.account, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!), editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: { + return ItemListPeerItem(theme: theme, account: arguments.account, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!), editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: { if let infoController = peerInfoController(account: arguments.account, peer: peer) { arguments.pushController(infoController) } @@ -376,12 +416,12 @@ private enum GroupInfoEntry: ItemListNodeEntry { }, removePeer: { peerId in arguments.removePeer(peerId) }) - case .convertToSupergroup: - return ItemListActionItem(title: "Convert to Supergroup", kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { + case let .convertToSupergroup(theme): + return ItemListActionItem(theme: theme, title: "Convert to Supergroup", kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.convertToSupergroup() }) - case .leave: - return ItemListActionItem(title: "Delete and Exit", kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { + case let .leave(theme): + return ItemListActionItem(theme: theme, title: "Delete and Exit", kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { }) default: preconditionFailure() @@ -513,32 +553,39 @@ private func canRemoveParticipant(account: Account, isAdmin: Bool, participantId return isAdmin } -private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfoState) -> [GroupInfoEntry] { +private func groupInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: GroupInfoState) -> [GroupInfoEntry] { var entries: [GroupInfoEntry] = [] if let peer = peerViewMainPeer(view) { let infoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingState?.editingName, updatingName: state.updatingName) - entries.append(.info(peer: peer, cachedData: view.cachedData, state: infoState, updatingAvatar: state.updatingAvatar)) + entries.append(.info(presentationData.theme, presentationData.strings, peer: peer, cachedData: view.cachedData, state: infoState, updatingAvatar: state.updatingAvatar)) } var highlightAdmins = false - var canManageGroup = false - var canManageMembers = false + var canEditGroupInfo = false + var canEditMembers = false + var canAddMembers = false var isPublic = false + var isCreator = false if let group = view.peers[view.peerId] as? TelegramGroup { + if case .creator = group.role { + isCreator = true + } if group.flags.contains(.adminsEnabled) { highlightAdmins = true switch group.role { case .admin, .creator: - canManageGroup = true - canManageMembers = true + canEditGroupInfo = true + canEditMembers = true + canAddMembers = true case .member: break } } else { - canManageGroup = true + canEditGroupInfo = true + canAddMembers = true switch group.role { case .admin, .creator: - canManageMembers = true + canEditMembers = true case .member: break } @@ -546,68 +593,73 @@ private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfo } else if let channel = view.peers[view.peerId] as? TelegramChannel { highlightAdmins = true isPublic = channel.username != nil - switch channel.role { - case .creator: - canManageGroup = true - canManageMembers = true - case .moderator: - canManageMembers = true - case .editor, .member: - break + isCreator = channel.flags.contains(.isCreator) + if channel.hasAdminRights(.canChangeInfo) { + canEditGroupInfo = true + } + if channel.hasAdminRights(.canBanUsers) { + canEditMembers = true + } + if channel.hasAdminRights(.canInviteUsers) { + canAddMembers = true } } - if canManageGroup { - entries.append(GroupInfoEntry.setGroupPhoto) + if canEditGroupInfo { + entries.append(GroupInfoEntry.setGroupPhoto(presentationData.theme)) } if let editingState = state.editingState { if let group = view.peers[view.peerId] as? TelegramGroup, case .creator = group.role { - entries.append(.adminManagement) + entries.append(.adminManagement(presentationData.theme)) } else if let cachedChannelData = view.cachedData as? CachedChannelData { - entries.append(GroupInfoEntry.groupTypeSetup(isPublic: isPublic)) - entries.append(GroupInfoEntry.groupDescriptionSetup(text: editingState.editingDescriptionText)) + if isCreator { + entries.append(GroupInfoEntry.groupTypeSetup(presentationData.theme, isPublic: isPublic)) + } + if canEditGroupInfo { + entries.append(GroupInfoEntry.groupDescriptionSetup(presentationData.theme, text: editingState.editingDescriptionText)) + } if let adminCount = cachedChannelData.participantsSummary.adminCount { - entries.append(GroupInfoEntry.membersAdmins(count: Int(adminCount))) + entries.append(GroupInfoEntry.membersAdmins(presentationData.theme, count: Int(adminCount))) } if let bannedCount = cachedChannelData.participantsSummary.bannedCount { - entries.append(GroupInfoEntry.membersBlacklist(count: Int(bannedCount))) + entries.append(GroupInfoEntry.membersBlacklist(presentationData.theme, count: Int(bannedCount))) } } } else { if let cachedChannelData = view.cachedData as? CachedChannelData { if let about = cachedChannelData.about, !about.isEmpty { - entries.append(.about(about)) + entries.append(.about(presentationData.theme, about)) } if let peer = view.peers[view.peerId] as? TelegramChannel, let username = peer.username, !username.isEmpty { - entries.append(.link("t.me/" + username)) + entries.append(.link(presentationData.theme, "t.me/" + username)) } } - entries.append(GroupInfoEntry.notifications(settings: view.notificationSettings)) - entries.append(GroupInfoEntry.sharedMedia) + entries.append(GroupInfoEntry.notifications(presentationData.theme, settings: view.notificationSettings)) + entries.append(GroupInfoEntry.sharedMedia(presentationData.theme)) } var canRemoveAnyMember = false if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { for participant in participants.participants { - if canRemoveParticipant(account: account, isAdmin: canManageMembers, participantId: participant.peerId, invitedBy: participant.invitedBy) { + if canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: participant.peerId, invitedBy: participant.invitedBy) { canRemoveAnyMember = true break } } } else if let cachedChannelData = view.cachedData as? CachedChannelData, let participants = cachedChannelData.topParticipants { for participant in participants.participants { - if canRemoveParticipant(account: account, isAdmin: canManageMembers, participantId: participant.peerId, invitedBy: nil) { + if canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: participant.peerId, invitedBy: nil) { canRemoveAnyMember = true break } } } - if canManageGroup { - entries.append(GroupInfoEntry.addMember(editing: state.editingState != nil && canRemoveAnyMember)) + if canAddMembers { + entries.append(GroupInfoEntry.addMember(presentationData.theme, editing: state.editingState != nil && canRemoveAnyMember)) } if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { @@ -697,7 +749,7 @@ private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfo } else { memberStatus = .member } - entries.append(GroupInfoEntry.member(index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canManageMembers, participantId: peer.id, invitedBy: sortedParticipants[i].invitedBy), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) + entries.append(GroupInfoEntry.member(presentationData.theme, index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: sortedParticipants[i].invitedBy), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) } } } else if let cachedChannelData = view.cachedData as? CachedChannelData, let participants = cachedChannelData.topParticipants { @@ -710,7 +762,7 @@ private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfo if !state.temporaryParticipants.isEmpty { for participant in state.temporaryParticipants { if !existingParticipantIds.contains(participant.peer.id) { - updatedParticipants.append(.member(id: participant.peer.id, invitedAt: participant.timestamp)) + updatedParticipants.append(.member(id: participant.peer.id, invitedAt: participant.timestamp, adminInfo: nil, banInfo: nil)) if let presence = participant.presence, peerPresences[participant.peer.id] == nil { peerPresences[participant.peer.id] = presence } @@ -740,65 +792,15 @@ private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfo switch lhs { case .creator: return false - case let .moderator(lhsId, _, lhsInvitedAt): + case let .member(lhsId, lhsInvitedAt, _, _): switch rhs { - case .creator: - return true - case let .moderator(rhsId, _, rhsInvitedAt): - if lhsInvitedAt == rhsInvitedAt { - return lhsId.id < rhsId.id - } - return lhsInvitedAt > rhsInvitedAt - case let .editor(rhsId, _, rhsInvitedAt): - if lhsInvitedAt == rhsInvitedAt { - return lhsId.id < rhsId.id - } - return lhsInvitedAt > rhsInvitedAt - case let .member(rhsId, rhsInvitedAt): - if lhsInvitedAt == rhsInvitedAt { - return lhsId.id < rhsId.id - } - return lhsInvitedAt > rhsInvitedAt - } - case let .editor(lhsId, _, lhsInvitedAt): - switch rhs { - case .creator: - return true - case let .moderator(rhsId, _, rhsInvitedAt): - if lhsInvitedAt == rhsInvitedAt { - return lhsId.id < rhsId.id - } - return lhsInvitedAt > rhsInvitedAt - case let .editor(rhsId, _, rhsInvitedAt): - if lhsInvitedAt == rhsInvitedAt { - return lhsId.id < rhsId.id - } - return lhsInvitedAt > rhsInvitedAt - case let .member(rhsId, rhsInvitedAt): - if lhsInvitedAt == rhsInvitedAt { - return lhsId.id < rhsId.id - } - return lhsInvitedAt > rhsInvitedAt - } - case let .member(lhsId, lhsInvitedAt): - switch rhs { - case .creator: - return true - case let .moderator(rhsId, _, rhsInvitedAt): - if lhsInvitedAt == rhsInvitedAt { - return lhsId.id < rhsId.id - } - return lhsInvitedAt > rhsInvitedAt - case let .editor(rhsId, _, rhsInvitedAt): - if lhsInvitedAt == rhsInvitedAt { - return lhsId.id < rhsId.id - } - return lhsInvitedAt > rhsInvitedAt - case let .member(rhsId, rhsInvitedAt): - if lhsInvitedAt == rhsInvitedAt { - return lhsId.id < rhsId.id - } - return lhsInvitedAt > rhsInvitedAt + case .creator: + return true + case let .member(rhsId, rhsInvitedAt, _, _): + if lhsInvitedAt == rhsInvitedAt { + return lhsId.id < rhsId.id + } + return lhsInvitedAt > rhsInvitedAt } } }) @@ -808,15 +810,19 @@ private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfo let memberStatus: GroupInfoMemberStatus if highlightAdmins { switch sortedParticipants[i] { - case .moderator, .editor, .creator: - memberStatus = .admin - case .member: - memberStatus = .member + case .creator: + memberStatus = .admin + case let .member(_, _, adminInfo, _): + if adminInfo != nil { + memberStatus = .admin + } else { + memberStatus = .member + } } } else { memberStatus = .member } - entries.append(GroupInfoEntry.member(index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canManageMembers, participantId: peer.id, invitedBy: nil), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) + entries.append(GroupInfoEntry.member(presentationData.theme, index: i, peerId: peer.id, peer: peer, presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: nil), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), enabled: !disabledPeerIds.contains(peer.id))) } } } @@ -824,13 +830,13 @@ private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfo if let group = view.peers[view.peerId] as? TelegramGroup { if case .Member = group.membership { if case .creator = group.role, state.editingState != nil { - entries.append(.convertToSupergroup) + entries.append(.convertToSupergroup(presentationData.theme)) } - entries.append(.leave) + entries.append(.leave(presentationData.theme)) } } else if let channel = view.peers[view.peerId] as? TelegramChannel { if case .member = channel.participationStatus { - entries.append(.leave) + entries.append(.leave(presentationData.theme)) } } @@ -1048,7 +1054,7 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl } }, addMember: { var confirmationImpl: ((PeerId) -> Signal)? - let contactsController = ContactSelectionController(account: account, title: "Add Member", confirmation: { peerId in + let contactsController = ContactSelectionController(account: account, title: { $0.GroupInfo_AddParticipantTitle }, confirmation: { peerId in if let confirmationImpl = confirmationImpl { return confirmationImpl(peerId) } else { @@ -1196,8 +1202,8 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl pushControllerImpl?(convertToSupergroupController(account: account, peerId: peerId)) }) - let signal = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId)) - |> map { state, view -> (ItemListControllerState, (ItemListNodeState, GroupInfoEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId)) + |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, GroupInfoEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) let rightNavigationButton: ItemListNavigationButton if let editingState = state.editingState { @@ -1272,15 +1278,15 @@ public func groupInfoController(account: Account, peerId: PeerId) -> ViewControl }) } - let controllerState = ItemListControllerState(title: .text("Info"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton) - let listState = ItemListNodeState(entries: groupInfoEntries(account: account, view: view, state: state), style: .blocks) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Info"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back")) + let listState = ItemListNodeState(entries: groupInfoEntries(account: account, presentationData: presentationData, view: view, state: state), style: .blocks) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) pushControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.pushViewController(value) diff --git a/TelegramUI/GroupsInCommonController.swift b/TelegramUI/GroupsInCommonController.swift index bc8edc1d8c..fc729cd77e 100644 --- a/TelegramUI/GroupsInCommonController.swift +++ b/TelegramUI/GroupsInCommonController.swift @@ -42,7 +42,7 @@ private enum GroupsInCommonEntryStableId: Hashable { } private enum GroupsInCommonEntry: ItemListNodeEntry { - case peerItem(Int32, Peer) + case peerItem(Int32, PresentationTheme, PresentationStrings, Peer) var section: ItemListSectionId { switch self { @@ -53,18 +53,24 @@ private enum GroupsInCommonEntry: ItemListNodeEntry { var stableId: GroupsInCommonEntryStableId { switch self { - case let .peerItem(_, peer): + case let .peerItem(_, _, _, peer): return .peer(peer.id) } } static func ==(lhs: GroupsInCommonEntry, rhs: GroupsInCommonEntry) -> Bool { switch lhs { - case let .peerItem(lhsIndex, lhsPeer): - if case let .peerItem(rhsIndex, rhsPeer) = rhs { + case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsPeer): + if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsPeer) = rhs { if lhsIndex != rhsIndex { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if !lhsPeer.isEqual(rhsPeer) { return false } @@ -77,9 +83,9 @@ private enum GroupsInCommonEntry: ItemListNodeEntry { static func <(lhs: GroupsInCommonEntry, rhs: GroupsInCommonEntry) -> Bool { switch lhs { - case let .peerItem(index, _): + case let .peerItem(index, _, _, _): switch rhs { - case let .peerItem(rhsIndex, _): + case let .peerItem(rhsIndex, _, _, _): return index < rhsIndex } } @@ -87,8 +93,8 @@ private enum GroupsInCommonEntry: ItemListNodeEntry { func item(_ arguments: GroupsInCommonControllerArguments) -> ListViewItem { switch self { - case let .peerItem(_, peer): - return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: { + case let .peerItem(_, theme, strings, peer): + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: { arguments.openPeer(peer.id) }, setPeerIdWithRevealedOptions: { _ in }, removePeer: { _ in @@ -103,13 +109,13 @@ private struct GroupsInCommonControllerState: Equatable { } } -private func groupsInCommonControllerEntries(state: GroupsInCommonControllerState, peers: [Peer]?) -> [GroupsInCommonEntry] { +private func groupsInCommonControllerEntries(presentationData: PresentationData, state: GroupsInCommonControllerState, peers: [Peer]?) -> [GroupsInCommonEntry] { var entries: [GroupsInCommonEntry] = [] if let peers = peers { var index: Int32 = 0 for peer in peers { - entries.append(.peerItem(index, peer)) + entries.append(.peerItem(index, presentationData.theme, presentationData.strings, peer)) index += 1 } } @@ -151,9 +157,9 @@ public func groupsInCommonController(account: Account, peerId: PeerId) -> ViewCo var previousPeers: [Peer]? - let signal = combineLatest(statePromise.get(), peersPromise.get()) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peersPromise.get()) |> deliverOnMainQueue - |> map { state, peers -> (ItemListControllerState, (ItemListNodeState, GroupsInCommonEntry.ItemGenerationArguments)) in + |> map { presentationData, state, peers -> (ItemListControllerState, (ItemListNodeState, GroupsInCommonEntry.ItemGenerationArguments)) in var emptyStateItem: ItemListControllerEmptyStateItem? if peers == nil { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() @@ -162,20 +168,19 @@ public func groupsInCommonController(account: Account, peerId: PeerId) -> ViewCo let previous = previousPeers previousPeers = peers - let controllerState = ItemListControllerState(title: .text("Groups in Common"), leftNavigationButton: nil, rightNavigationButton: nil, animateChanges: false) - let listState = ItemListNodeState(entries: groupsInCommonControllerEntries(state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_GroupsInCommon), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: groupsInCommonControllerEntries(presentationData: presentationData, state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) pushControllerImpl = { [weak controller] c in if let controller = controller { (controller.navigationController as? NavigationController)?.pushViewController(c) } } - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) return controller } diff --git a/TelegramUI/HashtagChatInputPanelItem.swift b/TelegramUI/HashtagChatInputPanelItem.swift index b2abfdce02..6520fe5b93 100644 --- a/TelegramUI/HashtagChatInputPanelItem.swift +++ b/TelegramUI/HashtagChatInputPanelItem.swift @@ -79,15 +79,15 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { self.textNode = TextNode() self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = UIColor(0xC9CDD1) + self.topSeparatorNode.backgroundColor = UIColor(rgb: 0xC9CDD1) self.topSeparatorNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xD6D6DA) + self.separatorNode.backgroundColor = UIColor(rgb: 0xD6D6DA) self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) + self.highlightedBackgroundNode.backgroundColor = UIColor(rgb: 0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) diff --git a/TelegramUI/HashtagSearchController.swift b/TelegramUI/HashtagSearchController.swift index 0e0feb2160..a609a1f18b 100644 --- a/TelegramUI/HashtagSearchController.swift +++ b/TelegramUI/HashtagSearchController.swift @@ -11,6 +11,8 @@ final class HashtagSearchController: TelegramController { private var transitionDisposable: Disposable? private let openMessageFromSearchDisposable = MetaDisposable() + private var presentationData: PresentationData + private var controllerNode: HashtagSearchControllerNode { return self.displayNode as! HashtagSearchControllerNode } @@ -18,6 +20,8 @@ final class HashtagSearchController: TelegramController { init(account: Account, peerName: String?, query: String) { self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + super.init(account: account) if let peerName = peerName { @@ -25,7 +29,7 @@ final class HashtagSearchController: TelegramController { } else { self.title = query } - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) let peerId: Signal if let peerName = peerName { @@ -38,7 +42,7 @@ final class HashtagSearchController: TelegramController { let foundMessages: Signal<[ChatListSearchEntry], NoError> = peerId |> mapToSignal { peerId -> Signal<[ChatListSearchEntry], NoError> in return searchMessages(account: account, peerId: peerId, query: query) - |> map { return $0.map({ .message($0) }) } + |> map { return $0.map({ .message($0, defaultPresentationTheme, defaultPresentationStrings) }) } } let interaction = ChatListNodeInteraction(activateSearch: { diff --git a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift index 7251487797..028e3d7dcc 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -65,7 +65,7 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont override init(account: Account) { self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = UIColor(0xbdc2c7) + self.separatorNode.backgroundColor = UIColor(rgb: 0xbdc2c7) self.separatorNode.isHidden = true self.listView = ListView() diff --git a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift index 75c56f1feb..e398c04958 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift @@ -71,7 +71,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { private let titleFont = Font.medium(16.0) private let textFont = Font.regular(15.0) private let iconFont = Font.medium(25.0) -private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 2.0, color: UIColor(0xdfdfdf)) +private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 2.0, color: UIColor(rgb: 0xdfdfdf)) final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode { private let imageNodeBackground: ASDisplayNode @@ -230,7 +230,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode if updatedVideoResource { if let videoResource = videoResource { if let applicationContext = item.account.applicationContext as? TelegramApplicationContext { - strongSelf.videoNode.acquireContext(account: item.account, mediaManager: applicationContext.mediaManager, id: ChatContextResultManagedMediaId(result: item.result), resource: videoResource) + strongSelf.videoNode.acquireContext(account: item.account, mediaManager: applicationContext.mediaManager, id: ChatContextResultManagedMediaId(result: item.result), resource: videoResource, priority: 1) } } else { strongSelf.videoNode.clearContext() diff --git a/TelegramUI/HorizontalPeerItem.swift b/TelegramUI/HorizontalPeerItem.swift index 23ba1ea694..d5a34cd75a 100644 --- a/TelegramUI/HorizontalPeerItem.swift +++ b/TelegramUI/HorizontalPeerItem.swift @@ -6,11 +6,15 @@ import TelegramCore import SwiftSignalKit final class HorizontalPeerItem: ListViewItem { + let theme: PresentationTheme + let strings: PresentationStrings let account: Account let peer: Peer let action: (Peer) -> Void - init(account: Account, peer: Peer, action: @escaping (Peer) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peer: Peer, action: @escaping (Peer) -> Void) { + self.theme = theme + self.strings = strings self.account = account self.peer = peer self.action = action @@ -21,7 +25,7 @@ final class HorizontalPeerItem: ListViewItem { let node = HorizontalPeerItemNode() node.contentSize = CGSize(width: 92.0, height: 80.0) node.insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0) - node.update(account: self.account, peer: self.peer) + node.update(account: self.account, peer: self.peer, theme: self.theme, strings: self.strings) node.action = self.action completion(node, { return (nil, {}) @@ -58,15 +62,15 @@ final class HorizontalPeerItemNode: ListViewItemNode { override func didLoad() { super.didLoad() - self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) + self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } - func update(account: Account, peer: Peer) { + func update(account: Account, peer: Peer, theme: PresentationTheme, strings: PresentationStrings) { self.peer = peer self.avatarNode.setPeer(account: account, peer: peer) - self.titleNode.attributedText = NSAttributedString(string: peer.compactDisplayTitle, font: Font.regular(11.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: peer.compactDisplayTitle, font: Font.regular(11.0), textColor: theme.list.itemPrimaryTextColor) let titleSize = self.titleNode.measure(CGSize(width: 84.0, height: CGFloat.infinity)) self.titleNode.frame = CGRect(origin: CGPoint(x: floor((92.0 - titleSize.width) / 2.0), y: 4.0 + 60.0 + 6.0), size: titleSize) } diff --git a/TelegramUI/HorizontalStickersChatContextPanelNode.swift b/TelegramUI/HorizontalStickersChatContextPanelNode.swift index d9f5d80514..1ffd47494c 100644 --- a/TelegramUI/HorizontalStickersChatContextPanelNode.swift +++ b/TelegramUI/HorizontalStickersChatContextPanelNode.swift @@ -6,7 +6,7 @@ import Display private let backgroundCenterImage = generateImage(CGSize(width: 30.0, height: 82.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0xbfbfc4).cgColor) + context.setStrokeColor(UIColor(rgb: 0xbfbfc4).cgColor) context.setFillColor(UIColor.white.cgColor) let lineWidth = UIScreenPixel context.setLineWidth(lineWidth) @@ -25,7 +25,7 @@ private let backgroundCenterImage = generateImage(CGSize(width: 30.0, height: 82 private let backgroundLeftImage = generateImage(CGSize(width: 8.0, height: 16.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0xbfbfc4).cgColor) + context.setStrokeColor(UIColor(rgb: 0xbfbfc4).cgColor) context.setFillColor(UIColor.white.cgColor) let lineWidth = UIScreenPixel context.setLineWidth(lineWidth) @@ -183,7 +183,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { self.gridNode.bounds = CGRect(x: gridBounds.minX, y: gridBounds.minY, width: gridFrame.size.height, height: gridFrame.size.width) self.gridNode.position = CGPoint(x: gridFrame.size.width / 2.0, y: gridFrame.size.height / 2.0) - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: gridFrame.size.height, height: gridFrame.size.width), insets: UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0), preloadSize: 100.0, type: .fixed(itemSize: CGSize(width: 66.0, height: 66.0))), transition: .immediate), stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: gridFrame.size.height, height: gridFrame.size.width), insets: UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0), preloadSize: 100.0, type: .fixed(itemSize: CGSize(width: 66.0, height: 66.0), lineSpacing: 0.0)), transition: .immediate), stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) let dequeue = self.validLayout == nil self.validLayout = (size, interfaceState) diff --git a/TelegramUI/InAppNotificationSettings.swift b/TelegramUI/InAppNotificationSettings.swift index 0019bbaaef..5aa146e72b 100644 --- a/TelegramUI/InAppNotificationSettings.swift +++ b/TelegramUI/InAppNotificationSettings.swift @@ -18,9 +18,9 @@ struct InAppNotificationSettings: PreferencesEntry, Equatable { } init(decoder: Decoder) { - self.playSounds = (decoder.decodeInt32ForKey("s") as Int32) != 0 - self.vibrate = (decoder.decodeInt32ForKey("v") as Int32) != 0 - self.displayPreviews = (decoder.decodeInt32ForKey("p") as Int32) != 0 + self.playSounds = decoder.decodeInt32ForKey("s", orElse: 0) != 0 + self.vibrate = decoder.decodeInt32ForKey("v", orElse: 0) != 0 + self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0 } func encode(_ encoder: Encoder) { diff --git a/TelegramUI/InstalledStickerPacksController.swift b/TelegramUI/InstalledStickerPacksController.swift index 4ebab76530..47694ae4bd 100644 --- a/TelegramUI/InstalledStickerPacksController.swift +++ b/TelegramUI/InstalledStickerPacksController.swift @@ -64,12 +64,12 @@ private enum InstalledStickerPacksEntryId: Hashable { } private enum InstalledStickerPacksEntry: ItemListNodeEntry { - case trending(Int32) - case archived - case masks - case packsTitle(String) - case pack(Int32, StickerPackCollectionInfo, StickerPackItem?, Int32, Bool, ItemListStickerPackItemEditing) - case packsInfo(String) + case trending(PresentationTheme, String, Int32) + case archived(PresentationTheme, String) + case masks(PresentationTheme, String) + case packsTitle(PresentationTheme, String) + case pack(Int32, PresentationTheme, StickerPackCollectionInfo, StickerPackItem?, String, Bool, ItemListStickerPackItemEditing) + case packsInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -90,7 +90,7 @@ private enum InstalledStickerPacksEntry: ItemListNodeEntry { return .index(2) case .packsTitle: return .index(3) - case let .pack(_, info, _, _, _, _): + case let .pack(_, _, info, _, _, _, _): return .pack(info.id) case .packsInfo: return .index(4) @@ -99,25 +99,38 @@ private enum InstalledStickerPacksEntry: ItemListNodeEntry { static func ==(lhs: InstalledStickerPacksEntry, rhs: InstalledStickerPacksEntry) -> Bool { switch lhs { - case let .trending(count): - if case .trending(count) = rhs { + case let .trending(lhsTheme, lhsText, lhsCount): + if case let .trending(rhsTheme, rhsText, rhsCount) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsCount == rhsCount { return true } else { return false } - case .masks, .archived: - return lhs.stableId == rhs.stableId - case let .packsTitle(text): - if case .packsTitle(text) = rhs { + case let .masks(lhsTheme, lhsCount): + if case let .masks(rhsTheme, rhsCount) = rhs, lhsTheme === rhsTheme, lhsCount == rhsCount { return true } else { return false } - case let .pack(lhsIndex, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing): - if case let .pack(rhsIndex, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs { + case let .archived(lhsTheme, lhsCount): + if case let .archived(rhsTheme, rhsCount) = rhs, lhsTheme === rhsTheme, lhsCount == rhsCount { + return true + } else { + return false + } + case let .packsTitle(lhsTheme, lhsText): + if case let .packsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .pack(lhsIndex, lhsTheme, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing): + if case let .pack(rhsIndex, rhsTheme, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs { if lhsIndex != rhsIndex { return false } + if lhsTheme !== rhsTheme { + return false + } if lhsInfo != rhsInfo { return false } @@ -137,8 +150,8 @@ private enum InstalledStickerPacksEntry: ItemListNodeEntry { } else { return false } - case let .packsInfo(text): - if case .packsInfo(text) = rhs { + case let .packsInfo(lhsTheme, lhsText): + if case let .packsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -176,9 +189,9 @@ private enum InstalledStickerPacksEntry: ItemListNodeEntry { default: return true } - case let .pack(lhsIndex, _, _, _, _, _): + case let .pack(lhsIndex, _, _, _, _, _, _): switch rhs { - case let .pack(rhsIndex, _, _, _, _, _): + case let .pack(rhsIndex, _, _, _, _, _, _): return lhsIndex < rhsIndex case .packsInfo: return true @@ -197,22 +210,22 @@ private enum InstalledStickerPacksEntry: ItemListNodeEntry { func item(_ arguments: InstalledStickerPacksControllerArguments) -> ListViewItem { switch self { - case let .trending(count): - return ItemListDisclosureItem(title: "Trending", label: count == 0 ? "" : "\(count)", sectionId: self.section, style: .blocks, action: { + case let .trending(theme, text, count): + return ItemListDisclosureItem(theme: theme, title: text, label: count == 0 ? "" : "\(count)", sectionId: self.section, style: .blocks, action: { arguments.openFeatured() }) - case .masks: - return ItemListDisclosureItem(title: "Masks", label: "", sectionId: self.section, style: .blocks, action: { + case let .masks(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openMasks() }) - case .archived: - return ItemListDisclosureItem(title: "Archived", label: "", sectionId: self.section, style: .blocks, action: { + case let .archived(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openArchived() }) - case let .packsTitle(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .pack(_, info, topItem, count, enabled, editing): - return ItemListStickerPackItem(account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, sectionId: self.section, action: { _ in + case let .packsTitle(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .pack(_, theme, info, topItem, count, enabled, editing): + return ItemListStickerPackItem(theme: theme, account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: false, control: .none, editing: editing, enabled: enabled, sectionId: self.section, action: { _ in arguments.openStickerPack(info) }, setPackIdWithRevealedOptions: { current, previous in arguments.setPackIdWithRevealedOptions(current, previous) @@ -220,8 +233,8 @@ private enum InstalledStickerPacksEntry: ItemListNodeEntry { }, removePack: { arguments.removePack(info.id) }) - case let .packsInfo(text): - return ItemListTextItem(text: .markdown(text), sectionId: self.section, linkAction: { _ in + case let .packsInfo(theme, text): + return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { _ in arguments.openStickersBot() }) } @@ -271,7 +284,15 @@ private func namespaceForMode(_ mode: InstalledStickerPacksControllerMode) -> It } } -private func installedStickerPacksControllerEntries(state: InstalledStickerPacksControllerState, mode: InstalledStickerPacksControllerMode, view: CombinedView, featured: [FeaturedStickerPackItem]) -> [InstalledStickerPacksEntry] { +private func stringForStickerCount(_ count: Int32) -> String { + if count == 1 { + return "1 sticker" + } else { + return "\(count) stickers" + } +} + +private func installedStickerPacksControllerEntries(presentationData: PresentationData, state: InstalledStickerPacksControllerState, mode: InstalledStickerPacksControllerMode, view: CombinedView, featured: [FeaturedStickerPackItem]) -> [InstalledStickerPacksEntry] { var entries: [InstalledStickerPacksEntry] = [] switch mode { @@ -283,11 +304,11 @@ private func installedStickerPacksControllerEntries(state: InstalledStickerPacks unreadCount += 1 } } - entries.append(.trending(unreadCount)) + entries.append(.trending(presentationData.theme, presentationData.strings.StickerPacksSettings_FeaturedPacks, unreadCount)) } - entries.append(.archived) - entries.append(.masks) - entries.append(.packsTitle("STICKER SETS")) + entries.append(.archived(presentationData.theme, presentationData.strings.StickerPacksSettings_ArchivedPacks)) + entries.append(.masks(presentationData.theme, presentationData.strings.MaskStickerSettings_Title)) + entries.append(.packsTitle(presentationData.theme, presentationData.strings.StickerPacksSettings_StickerPacksSection)) case .masks: break } @@ -297,7 +318,7 @@ private func installedStickerPacksControllerEntries(state: InstalledStickerPacks var index: Int32 = 0 for entry in packsEntries { if let info = entry.info as? StickerPackCollectionInfo { - entries.append(.pack(index, info, entry.firstItem as? StickerPackItem, info.count == 0 ? entry.count : info.count, true, ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == entry.id))) + entries.append(.pack(index, presentationData.theme, info, entry.firstItem as? StickerPackItem, stringForStickerCount(info.count == 0 ? entry.count : info.count), true, ItemListStickerPackItemEditing(editable: true, editing: state.editing, revealed: state.packIdWithRevealedOptions == entry.id))) index += 1 } } @@ -306,9 +327,9 @@ private func installedStickerPacksControllerEntries(state: InstalledStickerPacks switch mode { case .general: - entries.append(.packsInfo("Artists are welcome to add their own sticker sets using our [@stickers]() bot.\n\nTap on a sticker to view and add the whole set.")) + entries.append(.packsInfo(presentationData.theme, presentationData.strings.StickerPacksSettings_ManagingHelp)) case .masks: - entries.append(.packsInfo("You can add masks to photos and videos you send. To do this, open the photo editor before sending a photo or video.")) + entries.append(.packsInfo(presentationData.theme, presentationData.strings.MaskStickerSettings_Info)) } return entries @@ -386,8 +407,9 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti var previousPackCount: Int? - let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, featured.get() |> deliverOnMainQueue) - |> map { state, view, featured -> (ItemListControllerState, (ItemListNodeState, InstalledStickerPacksEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, featured.get() |> deliverOnMainQueue) + |> deliverOnMainQueue + |> map { presentationData, state, view, featured -> (ItemListControllerState, (ItemListNodeState, InstalledStickerPacksEntry.ItemGenerationArguments)) in var packCount: Int? = nil if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [namespaceForMode(mode)])] as? ItemCollectionInfosView, let entries = stickerPacksView.entriesByNamespace[namespaceForMode(mode)] { packCount = entries.count @@ -396,13 +418,13 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti var rightNavigationButton: ItemListNavigationButton? if let packCount = packCount, packCount != 0 { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { updateState { $0.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { updateState { $0.withUpdatedEditing(true) } @@ -413,16 +435,15 @@ public func installedStickerPacksController(account: Account, mode: InstalledSti let previous = previousPackCount previousPackCount = packCount - let controllerState = ItemListControllerState(title: .text(mode == .general ? "Stickers" : "Masks"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(mode == .general ? "Stickers" : "Masks"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(entries: installedStickerPacksControllerEntries(state: state, mode: mode, view: view, featured: featured), style: .blocks, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10)) + let listState = ItemListNodeState(entries: installedStickerPacksControllerEntries(presentationData: presentationData, state: state, mode: mode, view: view, featured: featured), style: .blocks, animateChanges: previous != nil && packCount != nil && (previous! != 0 && previous! >= packCount! - 10)) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/InstantPageController.swift b/TelegramUI/InstantPageController.swift index e60dfa194c..0e0b9b0015 100644 --- a/TelegramUI/InstantPageController.swift +++ b/TelegramUI/InstantPageController.swift @@ -14,7 +14,7 @@ final class InstantPageController: ViewController { self.account = account self.webPage = webPage - super.init() + super.init(navigationBarTheme: nil) } required init(coder aDecoder: NSCoder) { @@ -24,7 +24,6 @@ final class InstantPageController: ViewController { override public func loadDisplayNode() { self.displayNode = InstantPageControllerNode(account: self.account) - self.navigationBar.isHidden = true self.statusBar.alpha = 0.0 self.displayNodeDidLoad() diff --git a/TelegramUI/InstantPageLayout.swift b/TelegramUI/InstantPageLayout.swift index 210741e1b0..b32a99291c 100644 --- a/TelegramUI/InstantPageLayout.swift +++ b/TelegramUI/InstantPageLayout.swift @@ -71,12 +71,12 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h let backgroundInset: CGFloat = 14.0 let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0) item.frame = item.frame.offsetBy(dx: horizontalInset, dy: backgroundInset) - let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shape: .rect, color: UIColor(0xF5F8FC)) + let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.size.height + backgroundInset * 2.0)), shape: .rect, color: UIColor(rgb: 0xF5F8FC)) return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [backgroundItem, item]) case let .authorDate(author: author, date: date): let styleStack = InstantPageTextStyleStack() styleStack.push(.fontSize(15.0)) - styleStack.push(.textColor(UIColor(0x79828b))) + styleStack.push(.textColor(UIColor(rgb: 0x79828b))) var text: RichText? if case .empty = author { if date != 0 { diff --git a/TelegramUI/InstantPageTextItem.swift b/TelegramUI/InstantPageTextItem.swift index 5c12cccafc..3aea61dcbd 100644 --- a/TelegramUI/InstantPageTextItem.swift +++ b/TelegramUI/InstantPageTextItem.swift @@ -201,14 +201,14 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt styleStack.pop() return result case let .url(text, url, _): - styleStack.push(.textColor(UIColor(0x007BE8))) + styleStack.push(.textColor(UIColor(rgb: 0x007BE8))) let result = attributedStringForRichText(text, styleStack: styleStack) styleStack.pop() styleStack.pop() return result case let .email(text, _): styleStack.push(.bold) - styleStack.push(.textColor(UIColor(0x007BE8))) + styleStack.push(.textColor(UIColor(rgb: 0x007BE8))) let result = attributedStringForRichText(text, styleStack: styleStack) styleStack.pop() styleStack.pop() diff --git a/TelegramUI/ItemListActionItem.swift b/TelegramUI/ItemListActionItem.swift index 1028c7f912..473a7f6a5f 100644 --- a/TelegramUI/ItemListActionItem.swift +++ b/TelegramUI/ItemListActionItem.swift @@ -16,6 +16,7 @@ enum ItemListActionAlignment { } class ItemListActionItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let title: String let kind: ItemListActionKind let alignment: ItemListActionAlignment @@ -24,7 +25,12 @@ class ItemListActionItem: ListViewItem, ItemListItem { let action: () -> Void let tag: Any? - init(title: String, kind: ItemListActionKind, alignment: ItemListActionAlignment, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void, tag: Any? = nil) { + init(theme: PresentationTheme? = nil, title: String, kind: ItemListActionKind, alignment: ItemListActionAlignment, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void, tag: Any? = nil) { + if let theme = theme { + self.theme = theme + } else { + self.theme = defaultPresentationTheme + } self.title = title self.kind = kind self.alignment = alignment @@ -95,11 +101,9 @@ class ItemListActionItemNode: ListViewItemNode { self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.titleNode = TextNode() @@ -108,7 +112,6 @@ class ItemListActionItemNode: ListViewItemNode { self.titleNode.contentsScale = UIScreen.main.scale self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) @@ -119,17 +122,25 @@ class ItemListActionItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ItemListActionItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let currentItem = self.item + return { item, width, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + let textColor: UIColor switch item.kind { case .destructive: - textColor = UIColor(0xff3b30) + textColor = item.theme.list.itemDestructiveColor case .generic: - textColor = UIColor(0x007ee5) + textColor = item.theme.list.itemAccentColor case .neutral: - textColor = .black + textColor = item.theme.list.itemPrimaryTextColor case .disabled: - textColor = UIColor(0x8e8e93) + textColor = item.theme.list.itemDisabledTextColor } let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) @@ -154,6 +165,13 @@ class ItemListActionItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + let _ = titleApply() let leftInset: CGFloat diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index 2264c47b13..fafb475172 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -66,6 +66,11 @@ struct ItemListAvatarAndNameInfoItemState: Equatable { let editingName: ItemListAvatarAndNameInfoItemName? let updatingName: ItemListAvatarAndNameInfoItemName? + init(editingName: ItemListAvatarAndNameInfoItemName? = nil, updatingName: ItemListAvatarAndNameInfoItemName? = nil) { + self.editingName = editingName + self.updatingName = updatingName + } + static func ==(lhs: ItemListAvatarAndNameInfoItemState, rhs: ItemListAvatarAndNameInfoItemState) -> Bool { if lhs.editingName != rhs.editingName { return false @@ -81,21 +86,31 @@ final class ItemListAvatarAndNameInfoItemContext { var hiddenAvatarRepresentation: TelegramMediaImageRepresentation? } +enum ItemListAvatarAndNameInfoItemStyle { + case plain + case blocks(withTopInset: Bool) +} + class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { let account: Account + let theme: PresentationTheme + let strings: PresentationStrings let peer: Peer? let presence: PeerPresence? let cachedData: CachedPeerData? let state: ItemListAvatarAndNameInfoItemState let sectionId: ItemListSectionId - let style: ItemListStyle + let style: ItemListAvatarAndNameInfoItemStyle let editingNameUpdated: (ItemListAvatarAndNameInfoItemName) -> Void let avatarTapped: () -> Void let context: ItemListAvatarAndNameInfoItemContext? let updatingImage: TelegramMediaImageRepresentation? - - init(account: Account, peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, sectionId: ItemListSectionId, style: ItemListStyle, editingNameUpdated: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, avatarTapped: @escaping () -> Void, context: ItemListAvatarAndNameInfoItemContext? = nil, updatingImage: TelegramMediaImageRepresentation? = nil) { + let call: (() -> Void)? + + init(account: Account, theme: PresentationTheme, strings: PresentationStrings, peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, sectionId: ItemListSectionId, style: ItemListAvatarAndNameInfoItemStyle, editingNameUpdated: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, avatarTapped: @escaping () -> Void, context: ItemListAvatarAndNameInfoItemContext? = nil, updatingImage: TelegramMediaImageRepresentation? = nil, call: (() -> Void)? = nil) { self.account = account + self.theme = theme + self.strings = strings self.peer = peer self.presence = presence self.cachedData = cachedData @@ -106,6 +121,7 @@ class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem { self.avatarTapped = avatarTapped self.context = context self.updatingImage = updatingImage + self.call = call } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { @@ -155,6 +171,8 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { private let avatarNode: AvatarNode private let updatingAvatarOverlay: ASImageNode + private let callButton: HighlightableButtonNode + private let nameNode: TextNode private let statusNode: TextNode @@ -174,11 +192,9 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.avatarNode = AvatarNode(font: Font.regular(28.0)) @@ -197,6 +213,8 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { self.statusNode.contentMode = .left self.statusNode.contentsScale = UIScreen.main.scale + self.callButton = HighlightableButtonNode() + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.avatarNode) @@ -209,6 +227,8 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { apply(true) } }) + + self.callButton.addTarget(self, action: #selector(callButtonPressed), forControlEvents: .touchUpInside) } deinit { @@ -226,7 +246,15 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { let layoutStatusNode = TextNode.asyncLayout(self.statusNode) let currentOverlayImage = self.updatingAvatarOverlay.image + let currentItem = self.item + return { item, width, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + let displayTitle: ItemListAvatarAndNameInfoItemName if let updatingName = item.state.updatingName { displayTitle = updatingName @@ -236,39 +264,39 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { displayTitle = .title(title: "") } - let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: displayTitle.composedTitle, font: nameFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: displayTitle.composedTitle, font: nameFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let statusText: String let statusColor: UIColor if let presence = item.presence as? TelegramUserPresence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) + let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, presence: presence, relativeTo: Int32(timestamp)) statusText = string if activity { - statusColor = UIColor(0x007ee5) + statusColor = item.theme.list.itemAccentColor } else { - statusColor = UIColor(0xb3b3b3) + statusColor = item.theme.list.itemSecondaryTextColor } } else if let channel = item.peer as? TelegramChannel { if let cachedChannelData = item.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { statusText = "\(memberCount) members" - statusColor = UIColor(0xb3b3b3) + statusColor = item.theme.list.itemSecondaryTextColor } else { switch channel.info { case .broadcast: statusText = "channel" - statusColor = UIColor(0xb3b3b3) + statusColor = item.theme.list.itemSecondaryTextColor case .group: statusText = "group" - statusColor = UIColor(0xb3b3b3) + statusColor = item.theme.list.itemSecondaryTextColor } } } else if let group = item.peer as? TelegramGroup { statusText = "\(group.participantCount) members" - statusColor = UIColor(0xb3b3b3) + statusColor = item.theme.list.itemSecondaryTextColor } else { statusText = "" - statusColor = UIColor.black + statusColor = item.theme.list.itemPrimaryTextColor } let (statusNodeLayout, statusNodeApply) = layoutStatusNode(NSAttributedString(string: statusText, font: statusFont, textColor: statusColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) @@ -281,16 +309,20 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { case .plain: contentSize = CGSize(width: width, height: 96.0) insets = itemListNeighborsPlainInsets(neighbors) - case .blocks: + case let .blocks(withTopInset): contentSize = CGSize(width: width, height: 92.0) - let topInset: CGFloat - switch neighbors.top { - case .sameSection, .none: - topInset = 0.0 - case .otherSection: - topInset = separatorHeight + 35.0 + if withTopInset { + insets = itemListNeighborsGroupedInsets(neighbors) + } else { + let topInset: CGFloat + switch neighbors.top { + case .sameSection, .none: + topInset = 0.0 + case .otherSection: + topInset = separatorHeight + 35.0 + } + insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: separatorHeight, right: 0.0) } - insets = UIEdgeInsets(top: topInset, left: 0.0, bottom: separatorHeight, right: 0.0) } var updateAvatarOverlayImage: UIImage? @@ -307,6 +339,15 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { strongSelf.layoutWidthAndNeighbors = (width, neighbors) + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + + strongSelf.inputSeparator?.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.callButton.setImage(PresentationResourcesChat.chatInfoCallButtonImage(item.theme), for: []) + } + if item.updatingImage != nil { if let updateAvatarOverlayImage = updateAvatarOverlayImage { strongSelf.updatingAvatarOverlay.image = updateAvatarOverlayImage @@ -328,6 +369,14 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { } } + if item.call != nil { + strongSelf.addSubnode(strongSelf.callButton) + + strongSelf.callButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - 10.0, y: floor((contentSize.height - 44.0) / 2.0) - 2.0), size: CGSize(width: 44.0, height: 44.0)) + } else if strongSelf.callButton.supernode != nil { + strongSelf.callButton.removeFromSupernode() + } + let avatarOriginY: CGFloat switch item.style { case .plain: @@ -369,8 +418,8 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { bottomStripeInset = 0.0 } - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: CGSize(width: width, height: layoutSize.height)) - strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: contentSize.height)) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: layoutSize.height - insets.top - separatorHeight), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) } @@ -414,7 +463,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { case let .personName(firstName, lastName): if strongSelf.inputSeparator == nil { let inputSeparator = ASDisplayNode() - inputSeparator.backgroundColor = UIColor(0xc8c7cc) + inputSeparator.backgroundColor = item.theme.list.itemSeparatorColor inputSeparator.isLayerBacked = true strongSelf.addSubnode(inputSeparator) strongSelf.inputSeparator = inputSeparator @@ -422,11 +471,12 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { if strongSelf.inputFirstField == nil { let inputFirstField = TextFieldNodeView() - inputFirstField.typingAttributes = [NSFontAttributeName: Font.regular(17.0)] - //inputFirstField.backgroundColor = UIColor.lightGray + inputFirstField.font = Font.regular(17.0) + inputFirstField.textColor = item.theme.list.itemPrimaryTextColor + inputFirstField.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance inputFirstField.autocorrectionType = .no - inputFirstField.attributedPlaceholder = NSAttributedString(string: "First Name", font: Font.regular(17.0), textColor: UIColor(0xc8c8ce)) - inputFirstField.attributedText = NSAttributedString(string: firstName, font: Font.regular(17.0), textColor: UIColor.black) + inputFirstField.attributedPlaceholder = NSAttributedString(string: "First Name", font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) + inputFirstField.attributedText = NSAttributedString(string: firstName, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) strongSelf.inputFirstField = inputFirstField strongSelf.view.addSubview(inputFirstField) inputFirstField.addTarget(self, action: #selector(strongSelf.textFieldDidChange(_:)), for: .editingChanged) @@ -436,10 +486,12 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { if strongSelf.inputSecondField == nil { let inputSecondField = TextFieldNodeView() - inputSecondField.typingAttributes = [NSFontAttributeName: Font.regular(17.0)] + inputSecondField.font = Font.regular(17.0) + inputSecondField.textColor = item.theme.list.itemPrimaryTextColor + inputSecondField.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance inputSecondField.autocorrectionType = .no - inputSecondField.attributedPlaceholder = NSAttributedString(string: "Last Name", font: Font.regular(17.0), textColor: UIColor(0xc8c8ce)) - inputSecondField.attributedText = NSAttributedString(string: lastName, font: Font.regular(17.0), textColor: UIColor.black) + inputSecondField.attributedPlaceholder = NSAttributedString(string: "Last Name", font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) + inputSecondField.attributedText = NSAttributedString(string: lastName, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) strongSelf.inputSecondField = inputSecondField strongSelf.view.addSubview(inputSecondField) inputSecondField.addTarget(self, action: #selector(strongSelf.textFieldDidChange(_:)), for: .editingChanged) @@ -459,7 +511,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { case let .title(title): if strongSelf.inputSeparator == nil { let inputSeparator = ASDisplayNode() - inputSeparator.backgroundColor = UIColor(0xc8c7cc) + inputSeparator.backgroundColor = item.theme.list.itemSeparatorColor inputSeparator.isLayerBacked = true strongSelf.addSubnode(inputSeparator) strongSelf.inputSeparator = inputSeparator @@ -467,11 +519,12 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { if strongSelf.inputFirstField == nil { let inputFirstField = TextFieldNodeView() - inputFirstField.typingAttributes = [NSFontAttributeName: Font.regular(19.0)] - //inputFirstField.backgroundColor = UIColor.lightGray + inputFirstField.font = Font.regular(17.0) + inputFirstField.textColor = item.theme.list.itemPrimaryTextColor + inputFirstField.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance inputFirstField.autocorrectionType = .no - inputFirstField.attributedPlaceholder = NSAttributedString(string: "Title", font: Font.regular(19.0), textColor: UIColor(0xc8c8ce)) - inputFirstField.attributedText = NSAttributedString(string: title, font: Font.regular(19.0), textColor: UIColor.black) + inputFirstField.attributedPlaceholder = NSAttributedString(string: "Title", font: Font.regular(19.0), textColor: item.theme.list.itemPlaceholderTextColor) + inputFirstField.attributedText = NSAttributedString(string: title, font: Font.regular(19.0), textColor: item.theme.list.itemPrimaryTextColor) strongSelf.inputFirstField = inputFirstField strongSelf.view.addSubview(inputFirstField) inputFirstField.addTarget(self, action: #selector(strongSelf.textFieldDidChange(_:)), for: .editingChanged) @@ -586,4 +639,8 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { self.avatarNode.isHidden = hidden } } + + @objc func callButtonPressed() { + self.item?.call?() + } } diff --git a/TelegramUI/ItemListCheckboxItem.swift b/TelegramUI/ItemListCheckboxItem.swift index 04e922c82f..0f126d3618 100644 --- a/TelegramUI/ItemListCheckboxItem.swift +++ b/TelegramUI/ItemListCheckboxItem.swift @@ -3,24 +3,20 @@ import Display import AsyncDisplayKit import SwiftSignalKit -private let checkIcon = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x007ee5).cgColor) - context.setLineWidth(2.0) - context.move(to: CGPoint(x: 12.0, y: 1.0)) - context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) - context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) - context.strokePath() -}) - class ItemListCheckboxItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let title: String let checked: Bool let zeroSeparatorInsets: Bool let sectionId: ItemListSectionId let action: () -> Void - init(title: String, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + init(theme: PresentationTheme? = nil, title: String, checked: Bool, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void) { + if let theme = theme { + self.theme = theme + } else { + self.theme = defaultPresentationTheme + } self.title = title self.checked = checked self.zeroSeparatorInsets = zeroSeparatorInsets @@ -78,17 +74,16 @@ class ItemListCheckboxItemNode: ListViewItemNode { private let iconNode: ASImageNode private let titleNode: TextNode + private var item: ItemListCheckboxItem? + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.iconNode = ASImageNode() @@ -102,7 +97,6 @@ class ItemListCheckboxItemNode: ListViewItemNode { self.titleNode.contentsScale = UIScreen.main.scale self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) @@ -114,10 +108,12 @@ class ItemListCheckboxItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ItemListCheckboxItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let currentItem = self.item + return { item, width, neighbors in let leftInset: CGFloat = 44.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let separatorHeight = UIScreenPixel @@ -127,14 +123,32 @@ class ItemListCheckboxItemNode: ListViewItemNode { let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size + var updateCheckImage: UIImage? + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + updateCheckImage = PresentationResourcesItemList.checkIconImage(item.theme) + } + return (layout, { [weak self] in - let image = checkIcon - if let strongSelf = self { + strongSelf.item = item + + if let updateCheckImage = updateCheckImage { + strongSelf.iconNode.image = updateCheckImage + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + let _ = titleApply() - strongSelf.iconNode.image = image - if let image = image { + if let image = strongSelf.iconNode.image { strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: floor((leftInset - image.size.width) / 2.0), y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size) } strongSelf.iconNode.isHidden = !item.checked diff --git a/TelegramUI/ItemListController.swift b/TelegramUI/ItemListController.swift index 6d9432dc33..72d5990a39 100644 --- a/TelegramUI/ItemListController.swift +++ b/TelegramUI/ItemListController.swift @@ -1,6 +1,7 @@ import Foundation import Display import SwiftSignalKit +import TelegramCore enum ItemListNavigationButtonStyle { case regular @@ -24,6 +25,14 @@ struct ItemListNavigationButton { let action: () -> Void } +struct ItemListBackButton: Equatable { + let title: String + + static func ==(lhs: ItemListBackButton, rhs: ItemListBackButton) -> Bool { + return lhs.title == rhs.title + } +} + enum ItemListControllerTitle: Equatable { case text(String) case sectionControl([String], Int) @@ -37,7 +46,7 @@ enum ItemListControllerTitle: Equatable { return false } case let .sectionControl(lhsSection, lhsIndex): - if case let .sectionControl(rhsSection, rhsIndex) = rhs { + if case let .sectionControl(rhsSection, rhsIndex) = rhs, lhsSection == rhsSection, lhsIndex == rhsIndex { return true } else { return false @@ -46,16 +55,38 @@ enum ItemListControllerTitle: Equatable { } } +final class ItemListControllerTabBarItem: Equatable { + let title: String + let image: UIImage? + let selectedImage: UIImage? + + init(title: String, image: UIImage?, selectedImage: UIImage?) { + self.title = title + self.image = image + self.selectedImage = selectedImage + } + + static func ==(lhs: ItemListControllerTabBarItem, rhs: ItemListControllerTabBarItem) -> Bool { + return lhs.title == rhs.title && lhs.image === rhs.image && lhs.selectedImage === rhs.selectedImage + } +} + struct ItemListControllerState { + let theme: PresentationTheme let title: ItemListControllerTitle let leftNavigationButton: ItemListNavigationButton? let rightNavigationButton: ItemListNavigationButton? + let backNavigationButton: ItemListBackButton? + let tabBarItem: ItemListControllerTabBarItem? let animateChanges: Bool - init(title: ItemListControllerTitle, leftNavigationButton: ItemListNavigationButton?, rightNavigationButton: ItemListNavigationButton?, animateChanges: Bool = true) { + init(theme: PresentationTheme, title: ItemListControllerTitle, leftNavigationButton: ItemListNavigationButton?, rightNavigationButton: ItemListNavigationButton?, backNavigationButton: ItemListBackButton?, tabBarItem: ItemListControllerTabBarItem? = nil, animateChanges: Bool = true) { + self.theme = theme self.title = title self.leftNavigationButton = leftNavigationButton self.rightNavigationButton = rightNavigationButton + self.backNavigationButton = backNavigationButton + self.tabBarItem = tabBarItem self.animateChanges = animateChanges } } @@ -65,13 +96,19 @@ final class ItemListController: ViewController { private var leftNavigationButtonTitleAndStyle: (String, ItemListNavigationButtonStyle)? private var rightNavigationButtonTitleAndStyle: (String, ItemListNavigationButtonStyle)? + private var backNavigationButton: ItemListBackButton? + private var tabBarItemInfo: ItemListControllerTabBarItem? private var navigationButtonActions: (left: (() -> Void)?, right: (() -> Void)?) = (nil, nil) private var segmentedTitleView: ItemListControllerSegmentedTitleView? + private var theme: PresentationTheme + private var didPlayPresentationAnimation = false var titleControlValueChanged: ((Int) -> Void)? + private var tabBarItemDisposable: Disposable? + private let _ready = Promise() override var ready: Promise { return self._ready @@ -83,20 +120,42 @@ final class ItemListController: ViewController { } } - init(_ state: Signal<(ItemListControllerState, (ItemListNodeState, Entry.ItemGenerationArguments)), NoError>) { + init(account: Account, state: Signal<(ItemListControllerState, (ItemListNodeState, Entry.ItemGenerationArguments)), NoError>, tabBarItem: Signal? = nil) { self.state = state - super.init() + self.theme = (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme + + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.theme)) + + self.statusBar.statusBarStyle = (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme.rootController.statusBar.style.style self.scrollToTop = { [weak self] in (self?.displayNode as! ItemListNode).scrollToTop() } + + if let tabBarItem = tabBarItem { + self.tabBarItemDisposable = (tabBarItem |> deliverOnMainQueue).start(next: { [weak self] tabBarItemInfo in + if let strongSelf = self { + if strongSelf.tabBarItemInfo != tabBarItemInfo { + strongSelf.tabBarItemInfo = tabBarItemInfo + + strongSelf.tabBarItem.title = tabBarItemInfo.title + strongSelf.tabBarItem.image = tabBarItemInfo.image + strongSelf.tabBarItem.selectedImage = tabBarItemInfo.selectedImage + } + } + }) + } } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + self.tabBarItemDisposable?.dispose() + } + override func loadDisplayNode() { let previousControllerState = Atomic(value: nil) let nodeState = self.state |> deliverOnMainQueue |> afterNext { [weak self] controllerState, state in @@ -114,7 +173,7 @@ final class ItemListController: ViewController { if let segmentedTitleView = strongSelf.segmentedTitleView, segmentedTitleView.segments == sections { segmentedTitleView.index = index } else { - let segmentedTitleView = ItemListControllerSegmentedTitleView(segments: sections, index: index) + let segmentedTitleView = ItemListControllerSegmentedTitleView(segments: sections, index: index, color: controllerState.theme.rootController.navigationBar.accentTextColor) strongSelf.segmentedTitleView = segmentedTitleView strongSelf.navigationItem.titleView = strongSelf.segmentedTitleView segmentedTitleView.indexUpdated = { index in @@ -145,7 +204,7 @@ final class ItemListController: ViewController { if let rightNavigationButton = controllerState.rightNavigationButton { let item: UIBarButtonItem if case .activity = rightNavigationButton.style { - item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode()) + item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: controllerState.theme)) } else { item = UIBarButtonItem(title: rightNavigationButton.title, style: rightNavigationButton.style.barButtonItemStyle, target: strongSelf, action: #selector(strongSelf.rightNavigationButtonPressed)) } @@ -159,9 +218,38 @@ final class ItemListController: ViewController { } else if let barButtonItem = strongSelf.navigationItem.rightBarButtonItem, let rightNavigationButton = controllerState.rightNavigationButton, rightNavigationButton.enabled != barButtonItem.isEnabled { barButtonItem.isEnabled = rightNavigationButton.enabled } + + if strongSelf.backNavigationButton != controllerState.backNavigationButton { + strongSelf.backNavigationButton = controllerState.backNavigationButton + + if let backNavigationButton = strongSelf.backNavigationButton { + strongSelf.navigationItem.backBarButtonItem = UIBarButtonItem(title: backNavigationButton.title, style: .plain, target: nil, action: nil) + } else { + strongSelf.navigationItem.backBarButtonItem = nil + } + } + + if strongSelf.theme !== controllerState.theme { + strongSelf.theme = controllerState.theme + + strongSelf.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: strongSelf.theme)) + strongSelf.statusBar.statusBarStyle = strongSelf.theme.rootController.statusBar.style.style + + strongSelf.segmentedTitleView?.color = controllerState.theme.rootController.navigationBar.accentTextColor + + if let rightNavigationButton = controllerState.rightNavigationButton { + if case .activity = rightNavigationButton.style { + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: controllerState.theme))! + + strongSelf.rightNavigationButtonTitleAndStyle = (rightNavigationButton.title, rightNavigationButton.style) + strongSelf.navigationItem.setRightBarButton(item, animated: false) + item.isEnabled = rightNavigationButton.enabled + } + } + } } } - } |> map { $1 } + } |> map { ($0.theme, $1) } let displayNode = ItemListNode(state: nodeState) displayNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: true, completion: nil) diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index 8a45bf638b..6b1f127a0d 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -35,6 +35,7 @@ enum ItemListStyle { } private struct ItemListNodeTransition { + let theme: PresentationTheme let entries: ItemListNodeEntryTransition let updateStyle: ItemListStyle? let emptyStateItem: ItemListControllerEmptyStateItem? @@ -99,11 +100,14 @@ final class ItemListNode: ASDisplayNode { private var enqueuedTransitions: [ItemListNodeTransition] = [] private var validLayout: (ContainerViewLayout, CGFloat)? + private var theme: PresentationTheme? + private var listStyle: ItemListStyle? + var dismiss: (() -> Void)? var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)? - init(state: Signal<(ItemListNodeState, Entry.ItemGenerationArguments), NoError>) { + init(state: Signal<(PresentationTheme, (ItemListNodeState, Entry.ItemGenerationArguments)), NoError>) { self.listNode = ListView() super.init(viewBlock: { @@ -112,7 +116,7 @@ final class ItemListNode: ASDisplayNode { self.addSubnode(self.listNode) - self.backgroundColor = UIColor(0xefeff4) + self.backgroundColor = UIColor(rgb: 0xefeff4) self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in if let strongSelf = self, let visibleEntriesUpdated = strongSelf.visibleEntriesUpdated, let mergedEntries = (opaqueTransactionState as? ItemListNodeOpaqueState)?.mergedEntries { @@ -134,7 +138,8 @@ final class ItemListNode: ASDisplayNode { } let previousState = Atomic?>(value: nil) - self.transitionDisposable.set(((state |> map { state, arguments -> ItemListNodeTransition in + self.transitionDisposable.set(((state |> map { theme, stateAndArguments -> ItemListNodeTransition in + let (state, arguments) = stateAndArguments assert(state.entries == state.entries.sorted()) let previous = previousState.swap(state) let transition = preparedItemListNodeEntryTransition(from: previous?.entries ?? [], to: state.entries, arguments: arguments) @@ -142,7 +147,7 @@ final class ItemListNode: ASDisplayNode { if previous?.style != state.style { updatedStyle = state.style } - return ItemListNodeTransition(entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, focusItemTag: state.focusItemTag, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && !state.animateChanges, mergedEntries: state.entries) + return ItemListNodeTransition(theme: theme, entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, focusItemTag: state.focusItemTag, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && !state.animateChanges, mergedEntries: state.entries) }) |> deliverOnMainQueue).start(next: { [weak self] transition in if let strongSelf = self { strongSelf.enqueueTransition(transition) @@ -220,14 +225,32 @@ final class ItemListNode: ASDisplayNode { while !self.enqueuedTransitions.isEmpty { let transition = self.enqueuedTransitions.removeFirst() - if let updateStyle = transition.updateStyle { - switch updateStyle { - case .plain: - self.backgroundColor = .white - case .blocks: - self.backgroundColor = UIColor(0xefeff4) + if transition.theme !== self.theme { + self.theme = transition.theme + + if let listStyle = self.listStyle { + switch listStyle { + case .plain: + self.backgroundColor = transition.theme.list.plainBackgroundColor + case .blocks: + self.backgroundColor = transition.theme.list.blocksBackgroundColor + } } } + + if let updateStyle = transition.updateStyle { + self.listStyle = updateStyle + + if let theme = self.theme { + switch updateStyle { + case .plain: + self.backgroundColor = transition.theme.list.plainBackgroundColor + case .blocks: + self.backgroundColor = transition.theme.list.blocksBackgroundColor + } + } + } + var options = ListViewDeleteAndInsertOptions() if transition.firstTime { options.insert(.Synchronous) diff --git a/TelegramUI/ItemListControllerSegmentedTitleView.swift b/TelegramUI/ItemListControllerSegmentedTitleView.swift index eb743924b3..c0aaad0c4a 100644 --- a/TelegramUI/ItemListControllerSegmentedTitleView.swift +++ b/TelegramUI/ItemListControllerSegmentedTitleView.swift @@ -2,7 +2,20 @@ import Foundation import UIKit final class ItemListControllerSegmentedTitleView: UIView { - let segments: [String] + var segments: [String] { + didSet { + if self.segments != oldValue { + self.control.removeAllSegments() + var index = 0 + for segment in self.segments { + self.control.insertSegment(withTitle: segment, at: index, animated: false) + index += 1 + } + self.setNeedsLayout() + } + } + } + var index: Int { didSet { self.control.selectedSegmentIndex = self.index @@ -13,12 +26,20 @@ final class ItemListControllerSegmentedTitleView: UIView { var indexUpdated: ((Int) -> Void)? - init(segments: [String], index: Int) { + var color: UIColor { + didSet { + self.control.tintColor = self.color + } + } + + init(segments: [String], index: Int, color: UIColor) { self.segments = segments self.index = index + self.color = color self.control = UISegmentedControl(items: segments) self.control.selectedSegmentIndex = index + self.control.tintColor = color super.init(frame: CGRect()) diff --git a/TelegramUI/ItemListDisclosureItem.swift b/TelegramUI/ItemListDisclosureItem.swift index 3db0538178..b42a2baa6d 100644 --- a/TelegramUI/ItemListDisclosureItem.swift +++ b/TelegramUI/ItemListDisclosureItem.swift @@ -9,6 +9,7 @@ enum ItemListDisclosureStyle { } class ItemListDisclosureItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let title: String let label: String let sectionId: ItemListSectionId @@ -16,7 +17,12 @@ class ItemListDisclosureItem: ListViewItem, ItemListItem { let disclosureStyle: ItemListDisclosureStyle let action: (() -> Void)? - init(title: String, label: String, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?) { + init(theme: PresentationTheme? = nil, title: String, label: String, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, action: (() -> Void)?) { + if let theme = theme { + self.theme = theme + } else { + self.theme = defaultPresentationTheme + } self.title = title self.label = label self.sectionId = sectionId @@ -65,7 +71,6 @@ class ItemListDisclosureItem: ListViewItem, ItemListItem { } private let titleFont = Font.regular(17.0) -private let arrowImage = UIImage(bundleImageName: "Peer Info/DisclosureArrow")?.precomposed() class ItemListDisclosureItemNode: ListViewItemNode { private let backgroundNode: ASDisplayNode @@ -93,11 +98,9 @@ class ItemListDisclosureItemNode: ListViewItemNode { self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.titleNode = TextNode() @@ -110,10 +113,8 @@ class ItemListDisclosureItemNode: ListViewItemNode { self.arrowNode.displayWithoutProcessing = true self.arrowNode.displaysAsynchronously = false self.arrowNode.isLayerBacked = true - self.arrowNode.image = arrowImage self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) @@ -127,6 +128,8 @@ class ItemListDisclosureItemNode: ListViewItemNode { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeLabelLayout = TextNode.asyncLayout(self.labelNode) + let currentItem = self.item + return { item, width, neighbors in var rightInset: CGFloat = 34.0 @@ -137,6 +140,14 @@ class ItemListDisclosureItemNode: ListViewItemNode { rightInset = 34.0 } + var updateArrowImage: UIImage? + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) + } + let contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel @@ -150,8 +161,8 @@ class ItemListDisclosureItemNode: ListViewItemNode { insets = itemListNeighborsGroupedInsets(neighbors) } - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: titleFont, textColor: UIColor(0x8e8e93)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: titleFont, textColor: item.theme.list.itemSecondaryTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size @@ -160,6 +171,17 @@ class ItemListDisclosureItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + if let updateArrowImage = updateArrowImage { + strongSelf.arrowNode.image = updateArrowImage + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + let _ = titleApply() let _ = labelApply() @@ -214,7 +236,7 @@ class ItemListDisclosureItemNode: ListViewItemNode { strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 12.0), size: titleLayout.size) strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: width - rightInset - labelLayout.size.width, y: 12.0), size: labelLayout.size) - if let arrowImage = arrowImage { + if let arrowImage = strongSelf.arrowNode.image { strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: width - 15.0 - arrowImage.size.width, y: 18.0), size: arrowImage.size) } diff --git a/TelegramUI/ItemListEditableDeleteControlNode.swift b/TelegramUI/ItemListEditableDeleteControlNode.swift index b57c67ff37..6c068a4fa3 100644 --- a/TelegramUI/ItemListEditableDeleteControlNode.swift +++ b/TelegramUI/ItemListEditableDeleteControlNode.swift @@ -6,7 +6,7 @@ private let deleteIndicator = generateImage(CGSize(width: 22.0, height: 26.0), c context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor(white: 0.0, alpha: 0.06).cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 22.0, height: 22.0))) - context.setFillColor(UIColor(0xfc2125).cgColor) + context.setFillColor(UIColor(rgb: 0xfc2125).cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: CGSize(width: 22.0, height: 22.0))) context.setFillColor(UIColor.white.cgColor) context.fill(CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - 11.0) / 2.0), y: 2.0 + floorToScreenPixels((size.width - 1.0) / 2.0)), size: CGSize(width: 11.0, height: 1.0))) diff --git a/TelegramUI/ItemListEditableItem.swift b/TelegramUI/ItemListEditableItem.swift index ad732bf6a7..b724e780ff 100644 --- a/TelegramUI/ItemListEditableItem.swift +++ b/TelegramUI/ItemListEditableItem.swift @@ -72,8 +72,8 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { return !self.isDisplayingRevealedOptions } - override init(layerBacked: Bool, dynamicBounce: Bool, rotated: Bool) { - super.init(layerBacked: layerBacked, dynamicBounce: dynamicBounce, rotated: rotated) + override init(layerBacked: Bool, dynamicBounce: Bool, rotated: Bool, seeThrough: Bool) { + super.init(layerBacked: layerBacked, dynamicBounce: dynamicBounce, rotated: rotated, seeThrough: seeThrough) } override func didLoad() { diff --git a/TelegramUI/ItemListMultilineInputItem.swift b/TelegramUI/ItemListMultilineInputItem.swift index e5eda41435..9e14f4f8c7 100644 --- a/TelegramUI/ItemListMultilineInputItem.swift +++ b/TelegramUI/ItemListMultilineInputItem.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit import SwiftSignalKit class ItemListMultilineInputItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let text: String let placeholder: String let sectionId: ItemListSectionId @@ -11,7 +12,8 @@ class ItemListMultilineInputItem: ListViewItem, ItemListItem { let action: () -> Void let textUpdated: (String) -> Void - init(text: String, placeholder: String, sectionId: ItemListSectionId, style: ItemListStyle, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { + init(theme: PresentationTheme, text: String, placeholder: String, sectionId: ItemListSectionId, style: ItemListStyle, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { + self.theme = theme self.text = text self.placeholder = placeholder self.sectionId = sectionId @@ -71,11 +73,11 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.topStripeNode.backgroundColor = UIColor(rgb: 0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.bottomStripeNode.backgroundColor = UIColor(rgb: 0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.textClippingNode = ASDisplayNode() @@ -93,7 +95,11 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega override func didLoad() { super.didLoad() - self.textNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0)] + var textColor: UIColor = .black + if let item = self.item { + textColor = item.theme.list.itemPrimaryTextColor + } + self.textNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0), NSForegroundColorAttributeName: textColor] self.textNode.clipsToBounds = true self.textNode.delegate = self self.textNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) @@ -102,7 +108,14 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega func asyncLayout() -> (_ item: ItemListMultilineInputItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTextLayout = TextNode.asyncLayout(self.measureTextNode) + let currentItem = self.item + return { item, width, neighbors in + var updatedTheme: PresentationTheme? + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + let leftInset: CGFloat switch item.style { case .blocks: @@ -116,7 +129,7 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega measureText += "|" } let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black) - let attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: .black) + let attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: item.theme.list.itemPrimaryTextColor) let (textLayout, textApply) = makeTextLayout(attributedMeasureText, nil, 0, .end, CGSize(width: width - 8 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let separatorHeight = UIScreenPixel @@ -130,12 +143,22 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: UIColor(0x878787)) + let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) return (layout, { [weak self] in if let strongSelf = self { strongSelf.item = item + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + + if strongSelf.isNodeLoaded { + strongSelf.textNode.typingAttributes = [NSFontAttributeName: Font.regular(17.0), NSForegroundColorAttributeName: item.theme.list.itemPrimaryTextColor] + } + } + let _ = textApply() if let currentText = strongSelf.textNode.attributedText { if !currentText.isEqual(to: attributedText) { @@ -197,7 +220,7 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega let separatorHeight = UIScreenPixel let insets = self.insets let width = self.bounds.size.width - let contentSize = CGSize(width: width, height: currentValue - insets.top - insets.bottom) + let contentSize = CGSize(width: width, height: max(1.0, currentValue - insets.top - insets.bottom)) if let item = self.item { let leftInset: CGFloat diff --git a/TelegramUI/ItemListMultilineTextItem.swift b/TelegramUI/ItemListMultilineTextItem.swift index d208890b60..31079ea66c 100644 --- a/TelegramUI/ItemListMultilineTextItem.swift +++ b/TelegramUI/ItemListMultilineTextItem.swift @@ -4,11 +4,13 @@ import AsyncDisplayKit import SwiftSignalKit class ItemListMultilineTextItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let text: String let sectionId: ItemListSectionId let style: ItemListStyle - init(text: String, sectionId: ItemListSectionId, style: ItemListStyle) { + init(theme: PresentationTheme, text: String, sectionId: ItemListSectionId, style: ItemListStyle) { + self.theme = theme self.text = text self.sectionId = sectionId self.style = style @@ -56,17 +58,19 @@ class ItemListMultilineTextItemNode: ListViewItemNode { private let textNode: TextNode + private var item: ItemListMultilineTextItem? + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.topStripeNode.backgroundColor = UIColor(rgb: 0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.bottomStripeNode.backgroundColor = UIColor(rgb: 0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.textNode = TextNode() @@ -75,7 +79,7 @@ class ItemListMultilineTextItemNode: ListViewItemNode { self.textNode.contentsScale = UIScreen.main.scale self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) + self.highlightedBackgroundNode.backgroundColor = UIColor(rgb: 0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) @@ -86,8 +90,16 @@ class ItemListMultilineTextItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ItemListMultilineTextItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) + let currentItem = self.item + return { item, width, neighbors in - let textColor: UIColor = .black + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + + let textColor: UIColor = item.theme.list.itemPrimaryTextColor let leftInset: CGFloat @@ -118,6 +130,15 @@ class ItemListMultilineTextItemNode: ListViewItemNode { return (layout, { [weak self] in if let strongSelf = self { + strongSelf.item = item + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + let _ = titleApply() switch item.style { diff --git a/TelegramUI/ItemListPeerActionItem.swift b/TelegramUI/ItemListPeerActionItem.swift index 1d7f2a75e3..1d2b6b336e 100644 --- a/TelegramUI/ItemListPeerActionItem.swift +++ b/TelegramUI/ItemListPeerActionItem.swift @@ -4,13 +4,15 @@ import AsyncDisplayKit import SwiftSignalKit class ItemListPeerActionItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let icon: UIImage? let title: String let editing: Bool let sectionId: ItemListSectionId let action: () -> Void - init(icon: UIImage?, title: String, sectionId: ItemListSectionId, editing: Bool, action: @escaping () -> Void) { + init(theme: PresentationTheme, icon: UIImage?, title: String, sectionId: ItemListSectionId, editing: Bool, action: @escaping () -> Void) { + self.theme = theme self.icon = icon self.title = title self.editing = editing @@ -73,17 +75,16 @@ class ItemListPeerActionItemNode: ListViewItemNode { private let iconNode: ASImageNode private let titleNode: TextNode + private var item: ItemListPeerActionItem? + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.iconNode = ASImageNode() @@ -97,7 +98,6 @@ class ItemListPeerActionItemNode: ListViewItemNode { self.titleNode.contentsScale = UIScreen.main.scale self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) @@ -109,12 +109,19 @@ class ItemListPeerActionItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ItemListPeerActionItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let currentItem = self.item + return { item, width, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } let leftInset: CGFloat = 65.0 let editingOffset: CGFloat = (item.editing ? 38.0 : 0.0) - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemAccentColor), nil, 1, .end, CGSize(width: width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let separatorHeight = UIScreenPixel @@ -126,8 +133,18 @@ class ItemListPeerActionItemNode: ListViewItemNode { return (layout, { [weak self] animated in if let strongSelf = self { + strongSelf.item = item + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + let _ = titleApply() + let transition: ContainedViewLayoutTransition if animated { transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index ac0f1e5b70..ccb7674249 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -37,6 +37,8 @@ enum ItemListPeerItemLabel { } final class ItemListPeerItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let strings: PresentationStrings let account: Account let peer: Peer let presence: PeerPresence? @@ -51,7 +53,17 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { let removePeer: (PeerId) -> Void let toggleUpdated: ((Bool) -> Void)? - init(account: Account, peer: Peer, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, switchValue: Bool?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil) { + init(theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, account: Account, peer: Peer, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, switchValue: Bool?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil) { + if let theme = theme { + self.theme = theme + } else { + self.theme = defaultPresentationTheme + } + if let strings = strings { + self.strings = strings + } else { + self.strings = defaultPresentationStrings + } self.account = account self.peer = peer self.presence = presence @@ -118,8 +130,6 @@ private let labelFont = Font.regular(13.0) private let labelDisclosureFont = Font.regular(17.0) private let avatarFont = Font.regular(17.0) -private let arrowImage = UIImage(bundleImageName: "Peer Info/DisclosureArrow")?.precomposed() - class ItemListPeerItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -153,14 +163,11 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.avatarNode = AvatarNode(font: avatarFont) @@ -182,10 +189,9 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { self.labelNode.contentsScale = UIScreen.main.scale self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true - super.init(layerBacked: false, dynamicBounce: false, rotated: false) + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.addSubnode(self.avatarNode) self.addSubnode(self.titleNode) @@ -212,14 +218,24 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { var currentLabelArrowNode = self.labelArrowNode + let currentItem = self.layoutParams?.0 + return { item, width, neighbors in + var updateArrowImage: UIImage? + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) + } + var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? var labelAttributedString: NSAttributedString? let peerRevealOptions: [ItemListRevealOption] if item.editing.editable && item.enabled { - peerRevealOptions = [ItemListRevealOption(key: 0, title: "Remove", icon: nil, color: UIColor(0xff3824))] + peerRevealOptions = [ItemListRevealOption(key: 0, title: "Remove", icon: nil, color: UIColor(rgb: 0xff3824))] } else { peerRevealOptions = [] } @@ -239,34 +255,34 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { if let user = item.peer as? TelegramUser { if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { let string = NSMutableAttributedString() - string.append(NSAttributedString(string: firstName, font: titleFont, textColor: .black)) - string.append(NSAttributedString(string: " ", font: titleFont, textColor: .black)) - string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: .black)) + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)) + string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor)) titleAttributedString = string } else if let firstName = user.firstName, !firstName.isEmpty { - titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: UIColor.black) + titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) } else if let lastName = user.lastName, !lastName.isEmpty { - titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: UIColor.black) + titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) } else { - titleAttributedString = NSAttributedString(string: "Deleted User", font: titleBoldFont, textColor: UIColor(0xa6a6a6)) + titleAttributedString = NSAttributedString(string: "Deleted User", font: titleBoldFont, textColor: item.theme.list.itemDisabledTextColor) } } else if let group = item.peer as? TelegramGroup { - titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black) + titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) } else if let channel = item.peer as? TelegramChannel { - titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black) + titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: item.theme.list.itemPrimaryTextColor) } switch item.text { case .presence: if let presence = item.presence as? TelegramUserPresence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) - statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? UIColor(0x007ee5) : UIColor(0xa6a6a6)) + let (string, activity) = stringAndActivityForUserPresence(strings: item.strings, presence: presence, relativeTo: Int32(timestamp)) + statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? item.theme.list.itemAccentColor : item.theme.list.itemSecondaryTextColor) } else { - statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6)) + statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) } case let .text(text): - statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: UIColor(0xa6a6a6)) + statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) case .none: break } @@ -290,7 +306,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { case .none: break case let .text(text): - labelAttributedString = NSAttributedString(string: text, font: labelFont, textColor: UIColor(0xa6a6a6)) + labelAttributedString = NSAttributedString(string: text, font: labelFont, textColor: item.theme.list.itemSecondaryTextColor) rightInset += 10.0 case let .disclosure(text): if let currentLabelArrowNode = currentLabelArrowNode { @@ -300,11 +316,11 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { arrowNode.isLayerBacked = true arrowNode.displayWithoutProcessing = true arrowNode.displaysAsynchronously = false - arrowNode.image = arrowImage + arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.theme) updatedLabelArrowNode = arrowNode } labelInset += 30.0 - labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: UIColor(0x8e8e93)) + labelAttributedString = NSAttributedString(string: text, font: labelDisclosureFont, textColor: item.theme.list.itemSecondaryTextColor) } let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) @@ -322,7 +338,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { if !item.enabled { if currentDisabledOverlayNode == nil { currentDisabledOverlayNode = ASDisplayNode() - currentDisabledOverlayNode?.backgroundColor = UIColor(white: 1.0, alpha: 0.5) + currentDisabledOverlayNode?.backgroundColor = item.theme.list.itemBackgroundColor.withAlphaComponent(0.5) } } else { currentDisabledOverlayNode = nil @@ -332,6 +348,17 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { if let strongSelf = self { strongSelf.layoutParams = (item, width, neighbors) + if let updateArrowImage = updateArrowImage { + strongSelf.labelArrowNode?.image = updateArrowImage + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + let revealOffset = strongSelf.revealOffset let transition: ContainedViewLayoutTransition diff --git a/TelegramUI/ItemListRecentSessionItem.swift b/TelegramUI/ItemListRecentSessionItem.swift index 9e89597430..89240fb48d 100644 --- a/TelegramUI/ItemListRecentSessionItem.swift +++ b/TelegramUI/ItemListRecentSessionItem.swift @@ -31,6 +31,8 @@ enum ItemListRecentSessionItemText { } final class ItemListRecentSessionItem: ListViewItem, ItemListItem { + let theme: PresentationTheme + let strings: PresentationStrings let session: RecentAccountSession let enabled: Bool let editable: Bool @@ -40,7 +42,9 @@ final class ItemListRecentSessionItem: ListViewItem, ItemListItem { let setSessionIdWithRevealedOptions: (Int64?, Int64?) -> Void let removeSession: (Int64) -> Void - init(session: RecentAccountSession, enabled: Bool, editable: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, session: RecentAccountSession, enabled: Bool, editable: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void) { + self.theme = theme + self.strings = strings self.session = session self.enabled = enabled self.editable = editable @@ -110,14 +114,11 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.titleNode = TextNode() @@ -141,10 +142,9 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { self.labelNode.contentsScale = UIScreen.main.scale self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true - super.init(layerBacked: false, dynamicBounce: false, rotated: false) + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.addSubnode(self.titleNode) self.addSubnode(self.appNode) @@ -161,7 +161,15 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { var currentDisabledOverlayNode = self.disabledOverlayNode + let currentItem = self.layoutParams?.0 + return { item, width, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + var titleAttributedString: NSAttributedString? var appAttributedString: NSAttributedString? var locationAttributedString: NSAttributedString? @@ -169,14 +177,14 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { let peerRevealOptions: [ItemListRevealOption] if item.editable && item.enabled { - peerRevealOptions = [ItemListRevealOption(key: 0, title: "Terminate", icon: nil, color: UIColor(0xff3824))] + peerRevealOptions = [ItemListRevealOption(key: 0, title: item.strings.AuthSessions_TerminateSession, icon: nil, color: UIColor(rgb: 0xff3824))] } else { peerRevealOptions = [] } let rightInset: CGFloat = 0.0 - titleAttributedString = NSAttributedString(string: "\(item.session.appName) \(item.session.appVersion)", font: titleFont, textColor: UIColor.black) + titleAttributedString = NSAttributedString(string: "\(item.session.appName) \(item.session.appVersion)", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) var appString = "" if !item.session.deviceModel.isEmpty { @@ -197,14 +205,14 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { appString += item.session.systemVersion } - appAttributedString = NSAttributedString(string: appString, font: textFont, textColor: UIColor.black) - locationAttributedString = NSAttributedString(string: "\(item.session.ip) — \(item.session.country)", font: textFont, textColor: UIColor(0x6d6d6d)) + appAttributedString = NSAttributedString(string: appString, font: textFont, textColor: item.theme.list.itemPrimaryTextColor) + locationAttributedString = NSAttributedString(string: "\(item.session.ip) — \(item.session.country)", font: textFont, textColor: item.theme.list.itemSecondaryTextColor) if item.session.isCurrent { - labelAttributedString = NSAttributedString(string: "online", font: textFont, textColor: UIColor(0x007ee5)) + labelAttributedString = NSAttributedString(string: item.strings.Presence_online, font: textFont, textColor: item.theme.list.itemAccentColor) } else { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - let dateText = stringForRelativeTimestamp(item.session.activityDate, relativeTo: timestamp) - labelAttributedString = NSAttributedString(string: dateText, font: textFont, textColor: UIColor(0x6d6d6d)) + let dateText = stringForRelativeTimestamp(strings: item.strings, relativeTimestamp: item.session.activityDate, relativeTo: timestamp) + labelAttributedString = NSAttributedString(string: dateText, font: textFont, textColor: item.theme.list.itemSecondaryTextColor) } let leftInset: CGFloat = 15.0 @@ -245,6 +253,13 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { if let strongSelf = self { strongSelf.layoutParams = (item, width, neighbors) + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + let revealOffset = strongSelf.revealOffset let transition: ContainedViewLayoutTransition diff --git a/TelegramUI/ItemListSectionHeaderItem.swift b/TelegramUI/ItemListSectionHeaderItem.swift index b439c02e74..5a2cee75ed 100644 --- a/TelegramUI/ItemListSectionHeaderItem.swift +++ b/TelegramUI/ItemListSectionHeaderItem.swift @@ -4,12 +4,18 @@ import AsyncDisplayKit import SwiftSignalKit class ItemListSectionHeaderItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let text: String let sectionId: ItemListSectionId let isAlwaysPlain: Bool = true - init(text: String, sectionId: ItemListSectionId) { + init(theme: PresentationTheme? = nil, text: String, sectionId: ItemListSectionId) { + if let theme = theme { + self.theme = theme + } else { + self.theme = defaultPresentationTheme + } self.text = text self.sectionId = sectionId } @@ -71,11 +77,10 @@ class ItemListSectionHeaderItemNode: ListViewItemNode { return { item, width, neighbors in let leftInset: CGFloat = 15.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.text, font: titleFont, textColor: UIColor(0x6d6d72)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.text, font: titleFont, textColor: item.theme.list.sectionHeaderTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let contentSize: CGSize var insets = UIEdgeInsets() - let separatorHeight = UIScreenPixel contentSize = CGSize(width: width, height: 30.0) switch neighbors.top { @@ -88,7 +93,6 @@ class ItemListSectionHeaderItemNode: ListViewItemNode { } let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) - let layoutSize = layout.size return (layout, { [weak self] in if let strongSelf = self { diff --git a/TelegramUI/ItemListSingleLineInputItem.swift b/TelegramUI/ItemListSingleLineInputItem.swift index 011ee2cdb9..ebe2d2cd49 100644 --- a/TelegramUI/ItemListSingleLineInputItem.swift +++ b/TelegramUI/ItemListSingleLineInputItem.swift @@ -11,6 +11,7 @@ enum ItemListSingleLineInputItemType { } class ItemListSingleLineInputItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let title: NSAttributedString let text: String let placeholder: String @@ -21,7 +22,8 @@ class ItemListSingleLineInputItem: ListViewItem, ItemListItem { let textUpdated: (String) -> Void let tag: ItemListItemTag? - init(title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular, spacing: CGFloat = 0.0, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { + init(theme: PresentationTheme, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular, spacing: CGFloat = 0.0, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, action: @escaping () -> Void) { + self.theme = theme self.title = title self.text = text self.placeholder = placeholder @@ -84,14 +86,11 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.titleNode = TextNode() @@ -108,7 +107,10 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It self.textNode.textField.typingAttributes = [NSFontAttributeName: Font.regular(17.0)] self.textNode.textField.font = Font.regular(17.0) - self.textNode.textField.textColor = .black + if let item = self.item { + self.textNode.textField.textColor = item.theme.list.itemPrimaryTextColor + self.textNode.textField.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance + } self.textNode.clipsToBounds = true self.textNode.textField.delegate = self self.textNode.textField.addTarget(self, action: #selector(self.textFieldTextChanged(_:)), for: .editingChanged) @@ -118,7 +120,15 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It func asyncLayout() -> (_ item: ItemListSingleLineInputItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let currentItem = self.item + return { item, width, neighbors in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + let leftInset: CGFloat = 16.0 let titleString = NSMutableAttributedString(attributedString: item.title) @@ -135,12 +145,21 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size - let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: UIColor(0x878787)) + let attributedPlaceholderText = NSAttributedString(string: item.placeholder, font: Font.regular(17.0), textColor: item.theme.list.itemPlaceholderTextColor) return (layout, { [weak self] in if let strongSelf = self { strongSelf.item = item + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + + strongSelf.textNode.textField.textColor = item.theme.list.itemPrimaryTextColor + strongSelf.textNode.textField.keyboardAppearance = item.theme.chatList.searchBarKeyboardColor.keyboardAppearance + } + let _ = titleApply() strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: floor((layout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) diff --git a/TelegramUI/ItemListStickerPackItem.swift b/TelegramUI/ItemListStickerPackItem.swift index ca08ba90a3..ab02383b00 100644 --- a/TelegramUI/ItemListStickerPackItem.swift +++ b/TelegramUI/ItemListStickerPackItem.swift @@ -47,9 +47,10 @@ enum ItemListStickerPackItemControl: Equatable { } final class ItemListStickerPackItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let account: Account let packInfo: StickerPackCollectionInfo - let itemCount: Int32 + let itemCount: String let topItem: StickerPackItem? let unread: Bool let control: ItemListStickerPackItemControl @@ -61,7 +62,8 @@ final class ItemListStickerPackItem: ListViewItem, ItemListItem { let addPack: () -> Void let removePack: () -> Void - init(account: Account, packInfo: StickerPackCollectionInfo, itemCount: Int32, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void) { + init(theme: PresentationTheme, account: Account, packInfo: StickerPackCollectionInfo, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void) { + self.theme = theme self.account = account self.packInfo = packInfo self.itemCount = itemCount @@ -124,34 +126,6 @@ final class ItemListStickerPackItem: ListViewItem, ItemListItem { private let titleFont = Font.bold(15.0) private let statusFont = Font.regular(14.0) -private func stringForStickerCount(_ count: Int32) -> String { - if count == 1 { - return "1 sticker" - } else { - return "\(count) stickers" - } -} - -private let plusIcon = generateImage(CGSize(width: 18.0, height: 18.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0x007ee5).cgColor) - let lineWidth = min(1.5, UIScreenPixel * 4.0) - context.fill(CGRect(x: floorToScreenPixels((18.0 - lineWidth) / 2.0), y: 0.0, width: lineWidth, height: 18.0)) - context.fill(CGRect(x: 0.0, y: floorToScreenPixels((18.0 - lineWidth) / 2.0), width: 18.0, height: lineWidth)) -}) - -private let checkIcon = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x8d8c9d).cgColor) - context.setLineWidth(2.0) - context.move(to: CGPoint(x: 12.0, y: 1.0)) - context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) - context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) - context.strokePath() -}) - -private let unreadIcon = generateFilledCircleImage(diameter: 6.0, color: UIColor(0x007ee5)) - class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode @@ -186,14 +160,11 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.imageNode = TransformImageNode() @@ -221,10 +192,9 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { self.installationActionNode = HighlightableButtonNode() self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true - super.init(layerBacked: false, dynamicBounce: false, rotated: false) + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) self.addSubnode(self.imageNode) self.addSubnode(self.titleNode) @@ -249,13 +219,21 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { var currentDisabledOverlayNode = self.disabledOverlayNode + let currentItem = self.layoutParams?.0 + return { item, width, neighbors in var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + let packRevealOptions: [ItemListRevealOption] if item.editing.editable && item.enabled { - packRevealOptions = [ItemListRevealOption(key: 0, title: "Remove", icon: nil, color: UIColor(0xff3824))] + packRevealOptions = [ItemListRevealOption(key: 0, title: "Remove", icon: nil, color: UIColor(rgb: 0xff3824))] } else { packRevealOptions = [] } @@ -269,19 +247,19 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { case let .installation(installed): rightInset += 50.0 if installed { - installationActionImage = checkIcon + installationActionImage = PresentationResourcesItemList.checkIconImage(item.theme) } else { - installationActionImage = plusIcon + installationActionImage = PresentationResourcesItemList.plusIconImage(item.theme) } } var unreadImage: UIImage? if item.unread { - unreadImage = unreadIcon + unreadImage = PresentationResourcesItemList.stickerUnreadDotImage(item.theme) } - titleAttributedString = NSAttributedString(string: item.packInfo.title, font: titleFont, textColor: UIColor.black) - statusAttributedString = NSAttributedString(string: stringForStickerCount(item.itemCount), font: statusFont, textColor: UIColor(0x808080)) + titleAttributedString = NSAttributedString(string: item.packInfo.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) + statusAttributedString = NSAttributedString(string: item.itemCount, font: statusFont, textColor: item.theme.list.itemSecondaryTextColor) let leftInset: CGFloat = 65.0 @@ -309,7 +287,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { if !item.enabled { if currentDisabledOverlayNode == nil { currentDisabledOverlayNode = ASDisplayNode() - currentDisabledOverlayNode?.backgroundColor = UIColor(white: 1.0, alpha: 0.5) + currentDisabledOverlayNode?.backgroundColor = item.theme.list.itemBackgroundColor.withAlphaComponent(0.5) } } else { currentDisabledOverlayNode = nil @@ -346,6 +324,13 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { if let strongSelf = self { strongSelf.layoutParams = (item, width, neighbors) + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + let revealOffset = strongSelf.revealOffset let transition: ContainedViewLayoutTransition @@ -532,7 +517,6 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { super.updateRevealOffset(offset: offset, transition: transition) let leftInset: CGFloat = 65.0 - let width = self.bounds.size.width let editingOffset: CGFloat if let editableControlNode = self.editableControlNode { diff --git a/TelegramUI/ItemListSwitchItem.swift b/TelegramUI/ItemListSwitchItem.swift index 774ccb23f9..5347c9766d 100644 --- a/TelegramUI/ItemListSwitchItem.swift +++ b/TelegramUI/ItemListSwitchItem.swift @@ -4,15 +4,23 @@ import AsyncDisplayKit import SwiftSignalKit class ItemListSwitchItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let title: String let value: Bool + let enabled: Bool let sectionId: ItemListSectionId let style: ItemListStyle let updated: (Bool) -> Void - init(title: String, value: Bool, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void) { + init(theme: PresentationTheme? = nil, title: String, value: Bool, enabled: Bool = true, sectionId: ItemListSectionId, style: ItemListStyle, updated: @escaping (Bool) -> Void) { + if let theme = theme { + self.theme = theme + } else { + self.theme = defaultPresentationTheme + } self.title = title self.value = value + self.enabled = enabled self.sectionId = sectionId self.style = style self.updated = updated @@ -63,6 +71,7 @@ class ItemListSwitchItemNode: ListViewItemNode { private let titleNode: TextNode private var switchNode: SwitchNode + private var disabledOverlayNode: ASDisplayNode? private var item: ItemListSwitchItem? @@ -72,11 +81,9 @@ class ItemListSwitchItemNode: ListViewItemNode { self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.titleNode = TextNode() @@ -98,14 +105,20 @@ class ItemListSwitchItemNode: ListViewItemNode { func asyncLayout() -> (_ item: ItemListSwitchItem, _ width: CGFloat, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let currentItem = self.item + var currentDisabledOverlayNode = self.disabledOverlayNode + return { item, width, neighbors in - let sectionInset: CGFloat = 22.0 - let rightInset: CGFloat = 80.0 - let contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + switch item.style { case .plain: contentSize = CGSize(width: width, height: 44.0) @@ -115,7 +128,16 @@ class ItemListSwitchItemNode: ListViewItemNode { insets = itemListNeighborsGroupedInsets(neighbors) } - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + + if !item.enabled { + if currentDisabledOverlayNode == nil { + currentDisabledOverlayNode = ASDisplayNode() + currentDisabledOverlayNode?.backgroundColor = item.theme.list.itemBackgroundColor.withAlphaComponent(0.5) + } + } else { + currentDisabledOverlayNode = nil + } let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size @@ -124,6 +146,40 @@ class ItemListSwitchItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + let transition: ContainedViewLayoutTransition + if animated { + transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + + if let currentDisabledOverlayNode = currentDisabledOverlayNode { + if currentDisabledOverlayNode != strongSelf.disabledOverlayNode { + strongSelf.disabledOverlayNode = currentDisabledOverlayNode + strongSelf.addSubnode(currentDisabledOverlayNode) + currentDisabledOverlayNode.alpha = 0.0 + transition.updateAlpha(node: currentDisabledOverlayNode, alpha: 1.0) + currentDisabledOverlayNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight)) + } else { + transition.updateFrame(node: currentDisabledOverlayNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height - separatorHeight))) + } + } else if let disabledOverlayNode = strongSelf.disabledOverlayNode { + transition.updateAlpha(node: disabledOverlayNode, alpha: 0.0, completion: { [weak disabledOverlayNode] _ in + disabledOverlayNode?.removeFromSupernode() + }) + strongSelf.disabledOverlayNode = nil + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + + strongSelf.switchNode.frameColor = item.theme.list.itemSwitchColors.frameColor + strongSelf.switchNode.contentColor = item.theme.list.itemSwitchColors.contentColor + strongSelf.switchNode.handleColor = item.theme.list.itemSwitchColors.handleColor + } + let _ = titleApply() let leftInset: CGFloat diff --git a/TelegramUI/ItemListTextItem.swift b/TelegramUI/ItemListTextItem.swift index 5b5db037bf..3d9a6078d3 100644 --- a/TelegramUI/ItemListTextItem.swift +++ b/TelegramUI/ItemListTextItem.swift @@ -13,13 +13,19 @@ enum ItemListTextItemLinkAction { } class ItemListTextItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let text: ItemListTextItemText let sectionId: ItemListSectionId let linkAction: ((ItemListTextItemLinkAction) -> Void)? let isAlwaysPlain: Bool = true - init(text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { + init(theme: PresentationTheme? = nil, text: ItemListTextItemText, sectionId: ItemListSectionId, linkAction: ((ItemListTextItemLinkAction) -> Void)? = nil) { + if let theme = theme { + self.theme = theme + } else { + self.theme = defaultPresentationTheme + } self.text = text self.sectionId = sectionId self.linkAction = linkAction @@ -99,9 +105,9 @@ class ItemListTextItemNode: ListViewItemNode { let attributedText: NSAttributedString switch item.text { case let .plain(text): - attributedText = NSAttributedString(string: text, font: titleFont, textColor: UIColor(0x6d6d72)) + attributedText = NSAttributedString(string: text, font: titleFont, textColor: item.theme.list.freeTextColor) case let .markdown(text): - attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: UIColor(0x6d6d72)), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: UIColor(0x6d6d72)), link: MarkdownAttributeSet(font: titleFont, textColor: UIColor(0x007ee5)), linkAttribute: { contents in + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.freeTextColor), bold: MarkdownAttributeSet(font: titleBoldFont, textColor: item.theme.list.freeTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.list.itemAccentColor), linkAttribute: { contents in return (TextNode.UrlAttribute, contents) })) } @@ -142,9 +148,10 @@ class ItemListTextItemNode: ListViewItemNode { case .tap: let titleFrame = self.titleNode.frame if let item = self.item, titleFrame.contains(location) { - let attributes = self.titleNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) - if let url = attributes[TextNode.UrlAttribute] as? String { - item.linkAction?(.tap(url)) + if let (_, attributes) = self.titleNode.attributesAtPoint(CGPoint(x: location.x - titleFrame.minX, y: location.y - titleFrame.minY)) { + if let url = attributes[TextNode.UrlAttribute] as? String { + item.linkAction?(.tap(url)) + } } } default: diff --git a/TelegramUI/ItemListTextWithLabelItem.swift b/TelegramUI/ItemListTextWithLabelItem.swift index d9eaa91f7e..58f36b1f79 100644 --- a/TelegramUI/ItemListTextWithLabelItem.swift +++ b/TelegramUI/ItemListTextWithLabelItem.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit import SwiftSignalKit final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { + let theme: PresentationTheme let label: String let text: String let multiline: Bool @@ -11,7 +12,8 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { let action: (() -> Void)? let tag: Any? - init(label: String, text: String, multiline: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, tag: Any? = nil) { + init(theme: PresentationTheme, label: String, text: String, multiline: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, tag: Any? = nil) { + self.theme = theme self.label = label self.text = text self.multiline = multiline @@ -78,18 +80,14 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = .white self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) self.bottomStripeNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true self.labelNode = TextNode() @@ -112,13 +110,20 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { let makeLabelLayout = TextNode.asyncLayout(self.labelNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) + let currentItem = self.item + return { item, width, neighbors in + var updatedTheme: PresentationTheme? + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + let insets = itemListNeighborsPlainInsets(neighbors) let leftInset: CGFloat = 35.0 let separatorHeight = UIScreenPixel - let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: labelFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.text, font: textFont, textColor: UIColor.black), nil, item.multiline ? 0 : 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: labelFont, textColor: item.theme.list.itemAccentColor), nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor), nil, item.multiline ? 0 : 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let contentSize = CGSize(width: width, height: textLayout.size.height + 39.0) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = nodeLayout.size @@ -126,6 +131,13 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + let _ = labelApply() let _ = textApply() diff --git a/TelegramUI/LanguageSelectionController.swift b/TelegramUI/LanguageSelectionController.swift new file mode 100644 index 0000000000..12fd11574a --- /dev/null +++ b/TelegramUI/LanguageSelectionController.swift @@ -0,0 +1,440 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + +extension UISearchBar { + func setTextColor(_ color: UIColor) { + for view in self.subviews { + if let view = view as? UITextField { + view.textColor = color + return + } else { + for subview in view.subviews { + if let subview = subview as? UITextField { + subview.textColor = color + } + } + } + } + } +} + +private final class LanguageAccessoryView: UIView { + private let check: UIImageView + private let indicator: ActivityIndicator + + init(theme: PresentationTheme) { + self.check = UIImageView() + self.check.image = PresentationResourcesItemList.checkIconImage(theme) + + self.indicator = ActivityIndicator(theme: theme) + + super.init(frame: CGRect()) + + self.addSubview(self.check) + self.addSubnode(self.indicator) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func sizeToFit() { + self.frame = CGRect(origin: CGPoint(), size: self.check.image!.size) + + let size = self.bounds.size + + if let image = self.check.image { + let checkSize = image.size + self.check.frame = CGRect(origin: CGPoint(x: floor((size.width - checkSize.width) / 2.0), y: floor((size.height - checkSize.height) / 2.0)), size: checkSize) + } + + let indicatorSize = self.indicator.measure(CGSize(width: 22.0, height: 22.0)) + self.indicator.frame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: floor((size.height - indicatorSize.height) / 2.0)), size: indicatorSize) + } + + func setType(_ type: Int) { + switch type { + case 0: + self.check.isHidden = true + self.indicator.isHidden = true + case 1: + self.check.isHidden = false + self.indicator.isHidden = true + case 2: + self.check.isHidden = true + self.indicator.isHidden = false + default: + break + } + } +} + +private final class InnerCoutrySearchResultsController: UIViewController, UITableViewDelegate, UITableViewDataSource { + private let tableView: UITableView + + var searchResults: [LocalizationInfo] = [] { + didSet { + self.tableView.reloadData() + } + } + + var itemSelected: ((LocalizationInfo) -> Void)? + + init() { + self.tableView = UITableView(frame: CGRect(), style: .plain) + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.view.backgroundColor = .white + + self.view.addSubview(self.tableView) + self.tableView.frame = self.view.bounds + self.tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.tableView.dataSource = self + self.tableView.delegate = self + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.searchResults.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: UITableViewCell + if let currentCell = tableView.dequeueReusableCell(withIdentifier: "LanguageCell") { + cell = currentCell + } else { + cell = UITableViewCell(style: .subtitle, reuseIdentifier: "LanguageCell") + } + cell.textLabel?.text = self.searchResults[indexPath.row].title + cell.detailTextLabel?.text = self.searchResults[indexPath.row].localizedTitle + + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + self.itemSelected?(self.searchResults[indexPath.row]) + } +} + +private final class InnerLanguageSelectionController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating, UISearchBarDelegate { + private let account: Account + + private let tableView: UITableView + + private var languages: [LocalizationInfo] + + private var searchController: UISearchController! + private var searchResultsController: InnerCoutrySearchResultsController! + + var dismiss: (() -> Void)? + + private var languagesDisposable: Disposable? + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private var applyingLanguage: (LocalizationInfo, Disposable)? + + init(account: Account) { + self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.tableView = UITableView(frame: CGRect(), style: .plain) + + self.languages = [] + + super.init(nibName: nil, bundle: nil) + + self.title = self.presentationData.strings.Settings_AppLanguage + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + + self.definesPresentationContext = true + + 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.languagesDisposable = (availableLocalizations(network: account.network) + |> deliverOnMainQueue).start(next: { [weak self] languages in + if let strongSelf = self { + strongSelf.languages = languages + if strongSelf.isViewLoaded { + strongSelf.tableView.reloadData() + } + } + }) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.languagesDisposable?.dispose() + self.presentationDataDisposable?.dispose() + self.applyingLanguage?.1.dispose() + } + + private func updateThemeAndStrings() { + self.title = self.presentationData.strings.Settings_AppLanguage + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + + if self.isViewLoaded { + self.searchController.searchBar.placeholder = self.presentationData.strings.Common_Search + self.tableView.reloadData() + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.view.backgroundColor = .white + + self.searchResultsController = InnerCoutrySearchResultsController() + self.searchResultsController.itemSelected = { [weak self] language in + if let strongSelf = self { + strongSelf.searchController.searchBar.resignFirstResponder() + strongSelf.applyLanguage(language) + } + } + + self.searchController = UISearchController(searchResultsController: self.searchResultsController) + self.searchController.searchResultsUpdater = self + self.searchController.dimsBackgroundDuringPresentation = true + self.searchController.searchBar.delegate = self + + self.tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.view.addSubview(self.tableView) + self.tableView.tableHeaderView = self.searchController.searchBar + self.tableView.dataSource = self + self.tableView.delegate = self + self.tableView.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.tableView.separatorColor = self.presentationData.theme.chatList.itemSeparatorColor + self.tableView.backgroundView = UIView() + + self.tableView.frame = self.view.bounds + self.view.addSubview(self.tableView) + + self.searchController.searchBar.placeholder = self.presentationData.strings.Common_Search + self.searchController.searchBar.barTintColor = self.presentationData.theme.chatList.backgroundColor + self.searchController.searchBar.tintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor + self.searchController.searchBar.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.searchController.searchBar.setTextColor(self.presentationData.theme.chatList.titleColor) + + let searchImage = generateImage(CGSize(width: 8.0, height: 28.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(self.presentationData.theme.chatList.regularSearchBarColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - size.width), size: CGSize(width: size.width, height: size.width))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.width / 2.0), size: CGSize(width: size.width, height: size.height - size.width))) + }) + self.searchController.searchBar.setSearchFieldBackgroundImage(searchImage, for: []) + self.searchController.searchBar.backgroundImage = UIImage() + } + + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.languages.count + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return nil + } + + func sectionIndexTitles(for tableView: UITableView) -> [String]? { + return nil + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: UITableViewCell + if let currentCell = tableView.dequeueReusableCell(withIdentifier: "LanguageCell") { + cell = currentCell + } else { + cell = UITableViewCell(style: .subtitle, reuseIdentifier: "LanguageCell") + cell.selectedBackgroundView = UIView() + cell.accessoryView = LanguageAccessoryView(theme: self.presentationData.theme) + cell.accessoryView?.sizeToFit() + } + cell.textLabel?.text = self.languages[indexPath.row].title + cell.textLabel?.textColor = self.presentationData.theme.chatList.titleColor + cell.detailTextLabel?.text = self.languages[indexPath.row].localizedTitle + cell.detailTextLabel?.textColor = self.presentationData.theme.chatList.titleColor + cell.backgroundColor = self.presentationData.theme.chatList.itemBackgroundColor + cell.selectedBackgroundView?.backgroundColor = self.presentationData.theme.chatList.itemHighlightedBackgroundColor + + var type: Int = 0 + if let (info, _) = self.applyingLanguage, info.languageCode == self.languages[indexPath.row].languageCode { + type = 2 + } else if self.presentationData.strings.languageCode == self.languages[indexPath.row].languageCode { + type = 1 + } + + (cell.accessoryView as? LanguageAccessoryView)?.setType(type) + + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + self.applyLanguage(self.languages[indexPath.row]) + } + + func updateSearchResults(for searchController: UISearchController) { + guard let normalizedQuery = searchController.searchBar.text?.lowercased() else { + self.searchResultsController.searchResults = [] + return + } + + var results: [LocalizationInfo] = [] + for language in self.languages { + if language.title.lowercased().hasPrefix(normalizedQuery) || language.localizedTitle.lowercased().hasPrefix(normalizedQuery) { + results.append(language) + } + } + self.searchResultsController.searchResults = results + } + + @objc func cancelPressed() { + self.dismiss?() + } + + private func applyLanguage(_ language: LocalizationInfo) { + if let (info, disposable) = self.applyingLanguage { + if info.languageCode == language.languageCode { + return + } else { + disposable.dispose() + self.applyingLanguage = nil + self.tableView.reloadData() + } + } + if language.languageCode != self.presentationData.strings.languageCode { + self.applyingLanguage = (language, (downoadAndApplyLocalization(postbox: self.account.postbox, network: self.account.network, languageCode: language.languageCode) |> deliverOnMainQueue).start(completed: { [weak self] in + if let strongSelf = self { + strongSelf.applyingLanguage = nil + strongSelf.tableView.reloadData() + } + })) + self.tableView.reloadData() + } + } +} + +final class LanguageSelectionController: ViewController { + private var controllerNode: LanguageSelectionControllerNode { + return self.displayNode as! LanguageSelectionControllerNode + } + + private let innerNavigationController: UINavigationController + private let innerController: InnerLanguageSelectionController + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + init(account: Account) { + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.innerController = InnerLanguageSelectionController(account: account) + self.innerNavigationController = UINavigationController(rootViewController: self.innerController) + + super.init(navigationBarTheme: nil) + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.innerNavigationController.navigationBar.barTintColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + self.innerNavigationController.navigationBar.tintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor + self.innerNavigationController.navigationBar.shadowImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(self.presentationData.theme.rootController.navigationBar.separatorColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: UIScreenPixel))) + }) + self.innerNavigationController.navigationBar.isTranslucent = false + self.innerNavigationController.navigationBar.titleTextAttributes = [NSFontAttributeName: Font.semibold(17.0), NSForegroundColorAttributeName: self.presentationData.theme.rootController.navigationBar.primaryTextColor] + + self.innerController.dismiss = { [weak self] in + self?.cancelPressed() + } + + 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() + } + } + }) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + } + + override public func loadDisplayNode() { + self.displayNode = LanguageSelectionControllerNode() + self.displayNodeDidLoad() + + self.innerNavigationController.willMove(toParentViewController: self) + self.addChildViewController(self.innerNavigationController) + self.displayNode.view.addSubview(self.innerNavigationController.view) + self.innerNavigationController.didMove(toParentViewController: self) + + self.controllerNode.dismiss = { [weak self] in + self?.presentingViewController?.dismiss(animated: true, completion: nil) + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.innerNavigationController.viewWillAppear(false) + self.innerNavigationController.viewDidAppear(false) + + self.controllerNode.animateIn() + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.innerNavigationController.view.frame = CGRect(origin: CGPoint(), size: layout.size) + } + + private func cancelPressed() { + self.controllerNode.animateOut() + } +} diff --git a/TelegramUI/LanguageSelectionControllerNode.swift b/TelegramUI/LanguageSelectionControllerNode.swift new file mode 100644 index 0000000000..0bfa776e76 --- /dev/null +++ b/TelegramUI/LanguageSelectionControllerNode.swift @@ -0,0 +1,31 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore + +final class LanguageSelectionControllerNode: ASDisplayNode { + var dismiss: (() -> Void)? + + override init() { + super.init(viewBlock: { + return UITracingLayerView() + }, didLoad: nil) + + self.backgroundColor = UIColor.white + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + } + + func animateIn() { + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateOut() { + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.dismiss?() + } + }) + } +} diff --git a/TelegramUI/LegacyAttachmentMenu.swift b/TelegramUI/LegacyAttachmentMenu.swift index e4bde209df..d27026b34e 100644 --- a/TelegramUI/LegacyAttachmentMenu.swift +++ b/TelegramUI/LegacyAttachmentMenu.swift @@ -5,7 +5,7 @@ import Display import SwiftSignalKit import Postbox -func legacyAttachmentMenu(parentController: LegacyController, recentlyUsedInlineBots: [Peer], presentOverlayController: @escaping (UIViewController) -> (() -> Void), openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController { +func legacyAttachmentMenu(theme: PresentationTheme, strings: PresentationStrings, parentController: LegacyController, recentlyUsedInlineBots: [Peer], presentOverlayController: @escaping (UIViewController) -> (() -> Void), openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void) -> TGMenuSheetController { let controller = TGMenuSheetController() controller.applicationInterface = parentController.applicationInterface controller.dismissesByOutsideTap = true @@ -34,25 +34,25 @@ func legacyAttachmentMenu(parentController: LegacyController, recentlyUsedInline carouselItem.allowCaptions = true itemViews.append(carouselItem) - let galleryItem = TGMenuSheetButtonItemView(title: "Photo or Video", type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + let galleryItem = TGMenuSheetButtonItemView(title: strings.AttachmentMenu_PhotoOrVideo, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in controller?.dismiss(animated: true) openGallery() })! itemViews.append(galleryItem) - let fileItem = TGMenuSheetButtonItemView(title: "File", type: TGMenuSheetButtonTypeDefault, action: {[weak controller] in + let fileItem = TGMenuSheetButtonItemView(title: strings.AttachmentMenu_File, type: TGMenuSheetButtonTypeDefault, action: {[weak controller] in controller?.dismiss(animated: true) openFileGallery() })! itemViews.append(fileItem) - let locationItem = TGMenuSheetButtonItemView(title: "Location", type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + let locationItem = TGMenuSheetButtonItemView(title: strings.Conversation_Location, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in controller?.dismiss(animated: true) openMap() })! itemViews.append(locationItem) - let contactItem = TGMenuSheetButtonItemView(title: "Contact", type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + let contactItem = TGMenuSheetButtonItemView(title: strings.Conversation_Contact, type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in controller?.dismiss(animated: true) openContacts() })! @@ -76,7 +76,7 @@ func legacyAttachmentMenu(parentController: LegacyController, recentlyUsedInline carouselItem.remainingHeight = TGMenuSheetButtonItemViewHeight * CGFloat(itemViews.count - 1) - let cancelItem = TGMenuSheetButtonItemView(title: "Cancel", type: TGMenuSheetButtonTypeCancel, action: { [weak controller] in + let cancelItem = TGMenuSheetButtonItemView(title: strings.Common_Cancel, type: TGMenuSheetButtonTypeCancel, action: { [weak controller] in controller?.dismiss(animated: true) })! itemViews.append(cancelItem) diff --git a/TelegramUI/LegacyController.swift b/TelegramUI/LegacyController.swift index c740636fec..5f15faba49 100644 --- a/TelegramUI/LegacyController.swift +++ b/TelegramUI/LegacyController.swift @@ -111,13 +111,11 @@ public class LegacyController: ViewController { self.legacyController = legacyController self.presentation = presentation - super.init() + super.init(navigationBarTheme: nil) if let legacyController = legacyController as? TGLegacyApplicationInterfaceHolder { legacyController.applicationInterface = self.applicationInterface } - - self.navigationBar.isHidden = true } required public init(coder aDecoder: NSCoder) { diff --git a/TelegramUI/LegacyControllerNode.swift b/TelegramUI/LegacyControllerNode.swift index c9281adfdc..d322de6b09 100644 --- a/TelegramUI/LegacyControllerNode.swift +++ b/TelegramUI/LegacyControllerNode.swift @@ -33,7 +33,7 @@ final class LegacyControllerNode: ASDisplayNode { } func animateModalOut(completion: @escaping () -> Void) { - self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: 0.0, y: self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, additive: true, completion: { _ in completion() }) } diff --git a/TelegramUI/LinkHighlightingNode.swift b/TelegramUI/LinkHighlightingNode.swift new file mode 100644 index 0000000000..7b96108fc9 --- /dev/null +++ b/TelegramUI/LinkHighlightingNode.swift @@ -0,0 +1,187 @@ +import Foundation +import AsyncDisplayKit +import Display + +private enum CornerType { + case topLeft + case topRight + case bottomLeft + case bottomRight +} + +private func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) { + context.setFillColor(color.cgColor) + switch type { + case .topLeft: + context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .topRight: + context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomLeft: + context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomRight: + context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + } +} + +private func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) { + context.setFillColor(color.cgColor) + switch type { + case .topLeft: + context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .topRight: + context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomLeft: + context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + case .bottomRight: + context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) + } +} + +private func generateRectsImage(color: UIColor, rects: [CGRect]) -> (CGPoint, UIImage?) { + if rects.isEmpty { + return (CGPoint(), nil) + } + + let inset: CGFloat = 2.0 + + var topLeft = rects[0].origin + var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY) + for i in 1 ..< rects.count { + topLeft.x = min(topLeft.x, rects[i].origin.x) + topLeft.y = min(topLeft.x, rects[i].origin.y) + bottomRight.x = max(bottomRight.x, rects[i].maxX) + bottomRight.y = max(bottomRight.x, rects[i].maxY) + } + + topLeft.x -= inset + topLeft.y -= inset + bottomRight.x += inset * 2.0 + bottomRight.y += inset * 2.0 + + return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + + context.setBlendMode(.copy) + + let radius: CGFloat = 4.0 + + for i in 0 ..< rects.count { + let rect = rects[i].insetBy(dx: -inset, dy: -inset) + context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y)) + } + + for i in 0 ..< rects.count { + let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + + var previous: CGRect? + if i != 0 { + previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + } + + var next: CGRect? + if i != rects.count - 1 { + next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) + } + + if let previous = previous { + if previous.contains(rect.topLeft) { + if abs(rect.topLeft.x - previous.minX) >= radius { + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: radius) + } + if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) { + if abs(rect.topRight.x - previous.maxX) >= radius { + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: radius) + drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: radius) + } + + if let next = next { + if next.contains(rect.bottomLeft) { + if abs(rect.bottomRight.x - next.maxX) >= radius { + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: radius) + } + if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) { + if abs(rect.bottomRight.x - next.maxX) >= radius { + drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: radius) + } + } else { + drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: radius) + drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: radius) + } + } + })) + +} + +final class LinkHighlightingNode: ASDisplayNode { + private var rects: [CGRect] = [] + private let imageNode: ASImageNode + + var color: UIColor { + didSet { + if !self.rects.isEmpty { + self.updateImage() + } + } + } + + init(color: UIColor) { + self.imageNode = ASImageNode() + self.imageNode.isLayerBacked = true + self.imageNode.displaysAsynchronously = false + self.imageNode.displayWithoutProcessing = true + + self.color = color + + super.init() + + self.addSubnode(self.imageNode) + } + + func updateRects(_ rects: [CGRect]) { + if self.rects != rects { + self.rects = rects + + self.updateImage() + } + } + + private func updateImage() { + if rects.isEmpty { + self.imageNode.image = nil + } + let (offset, image) = generateRectsImage(color: self.color, rects: self.rects) + + if let image = image { + self.imageNode.image = image + self.imageNode.frame = CGRect(origin: offset, size: image.size) + } + } +} diff --git a/TelegramUI/ListController.swift b/TelegramUI/ListController.swift deleted file mode 100644 index d212807b50..0000000000 --- a/TelegramUI/ListController.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit - -public class ListController: ViewController { - public var items: [ListControllerItem] = [] - - public var listDisplayNode: ListControllerNode { - get { - return super.displayNode as! ListControllerNode - } - } - - override public init(navigationBar: NavigationBar = NavigationBar()) { - super.init(navigationBar: navigationBar) - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func loadDisplayNode() { - self.displayNode = ListControllerNode() - - self.displayNode.backgroundColor = UIColor(0xefeff4) - - if !self.items.isEmpty { - self.listDisplayNode.listView.transaction(deleteIndices: [], insertIndicesAndItems: (0 ..< self.items.count).map({ ListViewInsertItem(index: $0, previousIndex: nil, item: self.items[$0], directionHint: .Down) }), updateIndicesAndItems: [], options: [.LowLatency, .Synchronous], updateOpaqueState: nil) - } - - self.displayNodeDidLoad() - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.listDisplayNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) - } -} diff --git a/TelegramUI/ListControllerButtonItem.swift b/TelegramUI/ListControllerButtonItem.swift deleted file mode 100644 index 8a362da5fe..0000000000 --- a/TelegramUI/ListControllerButtonItem.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Foundation -import UIKit -import Display - -private let titleFont = Font.regular(17.0) - -class ListControllerButtonItem: ListControllerGroupableItem { - fileprivate let title: String - fileprivate let action: () -> () - fileprivate let color: UIColor - - let selectable: Bool = true - - init(title: String, action: @escaping () -> (), color: UIColor = .blue) { - self.title = title - self.action = action - self.color = color - } - - func setupNode(async: @escaping (@escaping () -> Void) -> Void, completion: @escaping (ListControllerGroupableItemNode) -> Void) { - let node = ListControllerButtonItemNode() - completion(node) - } - - func selected(listView: ListView) { - self.action() - } -} - -class ListControllerButtonItemNode: ListControllerGroupableItemNode { - let label: TextNode - - override init() { - self.label = TextNode() - - super.init() - - self.label.isLayerBacked = true - self.addSubnode(self.label) - } - - override func asyncLayoutContent() -> (_ item: ListControllerGroupableItem, _ width: CGFloat) -> (CGSize, () -> Void) { - let layoutLabel = TextNode.asyncLayout(self.label) - return { item, width in - if let item = item as? ListControllerButtonItem { - let (labelLayout, labelApply) = layoutLabel(NSAttributedString(string: item.title, font: titleFont, textColor: item.color), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - return (CGSize(width: width, height: 44.0), { [weak self] in - if let strongSelf = self { - let _ = labelApply() - - strongSelf.label.frame = CGRect(origin: CGPoint(x: 16.0, y: floorToScreenPixels((44.0 - labelLayout.size.height) / 2.0)), size: labelLayout.size) - } - }) - } else { - return (CGSize(width: width, height: 0.0), { - }) - } - } - } -} diff --git a/TelegramUI/ListControllerDisclosureActionItem.swift b/TelegramUI/ListControllerDisclosureActionItem.swift deleted file mode 100644 index 23b9fd6f3a..0000000000 --- a/TelegramUI/ListControllerDisclosureActionItem.swift +++ /dev/null @@ -1,80 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit - -private let titleFont = Font.regular(17.0) - -private func generateDisclosureIconImage(color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 8.0, height: 14.0), contextGenerator: { size, context -> Void in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(color.cgColor) - let _ = try? drawSvgPath(context, path: "M6.36396103,7.4746212 L7.4246212,6.41396103 L1.06066017,0.0500000007 L0,1.11066017 L6.36396103,7.4746212 Z M1.06066017,12.9697384 L7.4246212,6.60577736 L6.36396103,5.54511719 L0,11.9090782 L1.06066017,12.9697384 L1.06066017,12.9697384 Z") - }) -} - -private let disclosureIconImage = generateDisclosureIconImage(color: UIColor(0xc6c6ca)) - -class ListControllerDisclosureActionItem: ListControllerGroupableItem { - fileprivate let title: String - private let action: () -> () - - let selectable: Bool = true - - init(title: String, action: @escaping () -> ()) { - self.title = title - self.action = action - } - - func setupNode(async: @escaping (@escaping () -> Void) -> Void, completion: @escaping (ListControllerGroupableItemNode) -> Void) { - let node = ListControllerDisclosureActionItemNode() - completion(node) - } - - func selected(listView: ListView) { - self.action() - } -} - -class ListControllerDisclosureActionItemNode: ListControllerGroupableItemNode { - let label: TextNode - let disclosureIcon: ASDisplayNode - - override init() { - self.label = TextNode() - self.label.isLayerBacked = true - - self.disclosureIcon = ASDisplayNode() - if let disclosureIconImage = disclosureIconImage { - self.disclosureIcon.frame = CGRect(origin: CGPoint(), size: disclosureIconImage.size) - self.disclosureIcon.contents = disclosureIconImage.cgImage - } - self.disclosureIcon.isLayerBacked = true - - super.init() - - self.addSubnode(self.label) - self.addSubnode(self.disclosureIcon) - } - - override func asyncLayoutContent() -> (_ item: ListControllerGroupableItem, _ width: CGFloat) -> (CGSize, () -> Void) { - let layoutLabel = TextNode.asyncLayout(self.label) - return { item, width in - if let item = item as? ListControllerDisclosureActionItem { - let (labelLayout, labelApply) = layoutLabel(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - return (CGSize(width: width, height: 44.0), { [weak self] in - if let strongSelf = self { - let _ = labelApply() - let disclosureSize = strongSelf.disclosureIcon.bounds.size - strongSelf.disclosureIcon.frame = CGRect(origin: CGPoint(x: width - 15.0 - disclosureSize.width, y: floorToScreenPixels((44.0 - disclosureSize.height) / 2.0)), size: disclosureSize) - - strongSelf.label.frame = CGRect(origin: CGPoint(x: 16.0, y: floorToScreenPixels((44.0 - labelLayout.size.height) / 2.0 + 0.5)), size: labelLayout.size) - } - }) - } else { - return (CGSize(width: width, height: 0.0), { - }) - } - } - } -} diff --git a/TelegramUI/ListControllerGroupableItem.swift b/TelegramUI/ListControllerGroupableItem.swift deleted file mode 100644 index bd43496e8d..0000000000 --- a/TelegramUI/ListControllerGroupableItem.swift +++ /dev/null @@ -1,145 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import SwiftSignalKit - -private let separatorHeight = 1.0 / UIScreen.main.scale - -protocol ListControllerGroupableItem: ListControllerItem { - func setupNode(async: @escaping (@escaping () -> Void) -> Void, completion: @escaping (ListControllerGroupableItemNode) -> Void) -} - -extension ListControllerGroupableItem { - func nodeConfiguredForWidth(async: @escaping (@escaping() -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping(ListViewItemNode, @escaping() -> (Signal?, () -> Void)) -> Void) { - self.setupNode(async: async, completion: { node in - let asyncLayout = node.asyncLayout() - let (layout, apply) = asyncLayout(self, width, previousItem is ListControllerGroupableItem, nextItem is ListControllerGroupableItem) - node.contentSize = layout.contentSize - node.insets = layout.insets - completion(node, { - return (nil, apply) - }) - }) - } - - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { - if let node = node as? ListControllerGroupableItemNode { - Queue.mainQueue().async { - let asyncLayout = node.asyncLayout() - async { - let (layout, apply) = asyncLayout(self, width, previousItem is ListControllerGroupableItem, nextItem is ListControllerGroupableItem) - Queue.mainQueue().async { - completion(layout, apply) - } - } - } - } - } -} - -class ListControllerGroupableItemNode: ListViewItemNode { - private let backgroundNode: ASDisplayNode - private let highlightedBackgroundNode: ASDisplayNode - - private let topStripeNode: ASDisplayNode - private let bottomStripeNode: ASDisplayNode - - init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = UIColor.white - self.backgroundNode.isLayerBacked = true - - self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) - self.highlightedBackgroundNode.isLayerBacked = true - - self.topStripeNode = ASDisplayNode() - self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) - self.topStripeNode.isLayerBacked = true - - self.bottomStripeNode = ASDisplayNode() - self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) - self.bottomStripeNode.isLayerBacked = true - - super.init(layerBacked: false, dynamicBounce: false) - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.topStripeNode) - self.addSubnode(self.bottomStripeNode) - } - - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - if let item = item as? ListControllerGroupableItem { - let layout = self.asyncLayout() - let (_, apply) = layout(item, width, previousItem is ListControllerGroupableItem, nextItem is ListControllerGroupableItem) - apply() - } - } - - func updateBackgroundAndSeparatorsLayout(groupBottom: Bool) { - let size = self.bounds.size - let insets = self.insets - - self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: CGSize(width: size.width, height: size.height)) - self.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -separatorHeight), size: CGSize(width: size.width, height: size.height + separatorHeight - insets.top)) - self.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -insets.top), size: CGSize(width: size.width, height: separatorHeight)) - let bottomStripeInset: CGFloat = groupBottom ? 16.0 : 0.0 - self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: size.height - insets.top - separatorHeight), size: CGSize(width: size.width - bottomStripeInset, height: separatorHeight)) - } - - func asyncLayoutContent() -> (_ item: ListControllerGroupableItem, _ width: CGFloat) -> (CGSize, () -> Void) { - return { _, width in - return (CGSize(width: width, height: 0.0), { - }) - } - } - - fileprivate func asyncLayout() -> (_ item: ListControllerGroupableItem, _ width: CGFloat, _ groupedTop: Bool, _ groupedBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { - let contentLayout = self.asyncLayoutContent() - - return { item, width, groupedTop, groupedBottom in - let (contentSize, contentApply) = contentLayout(item, width) - - let insets = UIEdgeInsets(top: groupedTop ? 0.0 : separatorHeight, left: 0.0, bottom: separatorHeight, right: 0.0) - let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: contentSize.height), insets: insets) - - return (layout, { [weak self] in - if let strongSelf = self { - contentApply() - - strongSelf.topStripeNode.isHidden = groupedTop - strongSelf.contentSize = layout.contentSize - strongSelf.insets = layout.insets - strongSelf.updateBackgroundAndSeparatorsLayout(groupBottom: groupedBottom) - } - }) - } - } - - override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) - - if highlighted { - self.highlightedBackgroundNode.alpha = 1.0 - if self.highlightedBackgroundNode.supernode == nil { - self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.bottomStripeNode) - } - } else { - if self.highlightedBackgroundNode.supernode != nil { - if animated { - self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in - if let strongSelf = self { - if completed { - strongSelf.highlightedBackgroundNode.removeFromSupernode() - } - } - }) - self.highlightedBackgroundNode.alpha = 0.0 - } else { - self.highlightedBackgroundNode.removeFromSupernode() - } - } - } - } -} diff --git a/TelegramUI/ListControllerItem.swift b/TelegramUI/ListControllerItem.swift deleted file mode 100644 index db2871d041..0000000000 --- a/TelegramUI/ListControllerItem.swift +++ /dev/null @@ -1,4 +0,0 @@ -import Display - -public protocol ListControllerItem: ListViewItem { -} diff --git a/TelegramUI/ListControllerNode.swift b/TelegramUI/ListControllerNode.swift deleted file mode 100644 index a35d11f0a9..0000000000 --- a/TelegramUI/ListControllerNode.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation -import AsyncDisplayKit -import Display - -public class ListControllerNode: ASDisplayNode { - let listView: ListView - - override init() { - self.listView = ListView() - - super.init(viewBlock: { - return UITracingLayerView() - }, didLoad: nil) - - self.addSubnode(self.listView) - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut: - break - case .spring: - curve = 7 - } - } - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default - } - - var insets = layout.insets(options: [.input]) - insets.top += navigationBarHeight - - self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - self.listView.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - - self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } -} diff --git a/TelegramUI/ListControllerSpacerItem.swift b/TelegramUI/ListControllerSpacerItem.swift deleted file mode 100644 index 1afe59db68..0000000000 --- a/TelegramUI/ListControllerSpacerItem.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -import Display -import SwiftSignalKit - -class ListControllerSpacerItem: ListControllerItem { - private let height: CGFloat - - init(height: CGFloat) { - self.height = height - } - - func mergesBackgroundWithItem(other: ListControllerItem) -> Bool { - return false - } - - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { - async { - let node = ListControllerSpacerItemNode() - node.height = self.height - node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) - completion(node, { - return (nil, {}) - }) - } - } - - public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { - completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { - }) - } -} - -class ListControllerSpacerItemNode: ListViewItemNode { - var height: CGFloat = 0.0 - - init() { - super.init(layerBacked: true, dynamicBounce: false) - } - - override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - self.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: self.height)) - } -} diff --git a/TelegramUI/ListMessageFileItemNode.swift b/TelegramUI/ListMessageFileItemNode.swift index b396a76725..dcf4f25b6f 100644 --- a/TelegramUI/ListMessageFileItemNode.swift +++ b/TelegramUI/ListMessageFileItemNode.swift @@ -40,7 +40,7 @@ private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? { let cornerSize: CGFloat = 10.0 let size = CGSize(width: 42.0, height: 42.0) - context.setFillColor(UIColor(colors.0).cgColor) + context.setFillColor(UIColor(rgb: colors.0).cgColor) context.beginPath() context.move(to: CGPoint(x: 0.0, y: radius)) if !radius.isZero { @@ -61,7 +61,7 @@ private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? { context.closePath() context.fillPath() - context.setFillColor(UIColor(colors.1).cgColor) + context.setFillColor(UIColor(rgb: colors.1).cgColor) context.beginPath() context.move(to: CGPoint(x: size.width - cornerSize, y: 0.0)) context.addLine(to: CGPoint(x: size.width, y: cornerSize)) @@ -108,16 +108,6 @@ private let titleFont = Font.medium(16.0) private let descriptionFont = Font.regular(13.0) private let extensionFont = Font.medium(13.0) -private let downloadFileStartIcon = generateTintedImage(image: UIImage(bundleImageName: "List Menu/ListDownloadStartIcon"), color: UIColor(0x007ee5)) -private let downloadFilePauseIcon = generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(UIColor(0x007ee5).cgColor) - - context.fill(CGRect(x: 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0)) - context.fill(CGRect(x: 2.0 + 2.0 + 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0)) -}) - private struct FetchControls { let fetch: () -> Void let cancel: () -> Void @@ -149,14 +139,14 @@ final class ListMessageFileItemNode: ListMessageNode { private var account: Account? private (set) var message: Message? + private var appliedItem: ListMessageItem? + public required init() { self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) self.separatorNode.displaysAsynchronously = false self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true self.titleNode = TextNode() @@ -181,11 +171,10 @@ final class ListMessageFileItemNode: ListMessageNode { self.downloadStatusIconNode.displaysAsynchronously = false self.downloadStatusIconNode.displayWithoutProcessing = true - self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(0x007ee5), foregroundColor: UIColor.white, icon: nil)) + self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: .black, foregroundColor: .white, icon: nil)) self.progressNode.isLayerBacked = true self.linearProgressNode = ASDisplayNode() - self.linearProgressNode.backgroundColor = UIColor(0x007ee5) self.linearProgressNode.isLayerBacked = true super.init() @@ -239,7 +228,15 @@ final class ListMessageFileItemNode: ListMessageNode { let currentMessage = self.message let currentIconImageRepresentation = self.currentIconImageRepresentation + let currentItem = self.appliedItem + return { [weak self] item, width, mergedTop, _, _ in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + let leftInset: CGFloat = 65.0 var extensionIconImage: UIImage? @@ -265,7 +262,7 @@ final class ListMessageFileItemNode: ListMessageNode { if case let .Audio(voice, duration, title, performer, waveform) = attribute { isAudio = true - titleText = NSAttributedString(string: title ?? "Unknown Track", font: titleFont, textColor: UIColor.black) + titleText = NSAttributedString(string: title ?? "Unknown Track", font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) let descriptionString: String if let performer = performer { @@ -276,13 +273,13 @@ final class ListMessageFileItemNode: ListMessageNode { descriptionString = "" } - descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: UIColor(0xa8a8a8)) + descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor) } } if !isAudio { let fileName: String = file.fileName ?? "" - titleText = NSAttributedString(string: fileName, font: titleFont, textColor: UIColor.black) + titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) var fileExtension: String? if let range = fileName.range(of: ".", options: [.backwards]) { @@ -307,7 +304,7 @@ final class ListMessageFileItemNode: ListMessageNode { descriptionString = "\(dateString)" } - descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: UIColor(0xa8a8a8)) + descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor) } break @@ -390,6 +387,15 @@ final class ListMessageFileItemNode: ListMessageNode { strongSelf.currentMedia = selectedMedia strongSelf.message = message strongSelf.account = item.account + strongSelf.appliedItem = item + + if let _ = updatedTheme { + strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + + strongSelf.progressNode.updateTheme(RadialProgressTheme(backgroundColor: item.theme.list.itemAccentColor, foregroundColor: item.theme.list.itemBackgroundColor, icon: nil)) + strongSelf.linearProgressNode.backgroundColor = item.theme.list.itemAccentColor + } strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - nodeLayout.insets.top), size: CGSize(width: width, height: nodeLayout.size.height + UIScreenPixel)) @@ -562,7 +568,7 @@ final class ListMessageFileItemNode: ListMessageNode { private func updateProgressFrame(size: CGSize) { var descriptionOffset: CGFloat = 0.0 - if let resourceStatus = self.resourceStatus { + if let resourceStatus = self.resourceStatus, let item = self.appliedItem { var maybeFetchStatus: MediaResourceStatus = .Local switch resourceStatus { case .playbackStatus: @@ -589,7 +595,7 @@ final class ListMessageFileItemNode: ListMessageNode { if self.downloadStatusIconNode.supernode == nil { self.addSubnode(self.downloadStatusIconNode) } - self.downloadStatusIconNode.image = downloadFilePauseIcon + self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadPauseIcon(item.theme) case .Local: if self.linearProgressNode.supernode != nil { self.linearProgressNode.removeFromSupernode() @@ -605,7 +611,7 @@ final class ListMessageFileItemNode: ListMessageNode { if self.downloadStatusIconNode.supernode == nil { self.addSubnode(self.downloadStatusIconNode) } - self.downloadStatusIconNode.image = downloadFileStartIcon + self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(item.theme) } } else { if self.linearProgressNode.supernode != nil { diff --git a/TelegramUI/ListMessageItem.swift b/TelegramUI/ListMessageItem.swift index 6e90344035..2696f8f3fe 100644 --- a/TelegramUI/ListMessageItem.swift +++ b/TelegramUI/ListMessageItem.swift @@ -6,6 +6,7 @@ import SwiftSignalKit import Postbox final class ListMessageItem: ListViewItem { + let theme: PresentationTheme let account: Account let peerId: PeerId let controllerInteraction: ChatControllerInteraction @@ -13,7 +14,8 @@ final class ListMessageItem: ListViewItem { let selectable: Bool = true - public init(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, message: Message) { + public init(theme: PresentationTheme, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, message: Message) { + self.theme = theme self.account = account self.peerId = peerId self.controllerInteraction = controllerInteraction diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index d2ad742ce5..79f20a1e43 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -9,7 +9,7 @@ private let titleFont = Font.medium(16.0) private let descriptionFont = Font.regular(14.0) private let iconFont = Font.medium(22.0) -private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 2.0, color: UIColor(0xdfdfdf)) +private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 2.0, color: UIColor(rgb: 0xdfdfdf)) final class ListMessageSnippetItemNode: ListMessageNode { private let highlightedBackgroundNode: ASDisplayNode @@ -24,14 +24,14 @@ final class ListMessageSnippetItemNode: ListMessageNode { private var currentIconImageRepresentation: TelegramMediaImageRepresentation? private var currentMedia: Media? + private var appliedItem: ListMessageItem? + public required init() { self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) self.separatorNode.displaysAsynchronously = false self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true self.titleNode = TextNode() @@ -93,13 +93,19 @@ final class ListMessageSnippetItemNode: ListMessageNode { let iconTextMakeLayout = TextNode.asyncLayout(self.iconTextNode) let iconImageLayout = self.iconImageNode.asyncLayout() - let currentMedia = self.currentMedia let currentIconImageRepresentation = self.currentIconImageRepresentation + let currentItem = self.appliedItem + return { [weak self] item, width, _, _, _ in + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + let leftInset: CGFloat = 65.0 - var extensionIconImage: UIImage? var title: NSAttributedString? var descriptionText: NSAttributedString? var iconText: NSAttributedString? @@ -121,7 +127,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { iconText = NSAttributedString(string: host.substring(to: host.index(after: host.startIndex)).uppercased(), font: iconFont, textColor: UIColor.white) } - title = NSAttributedString(string: content.title ?? content.websiteName ?? hostName, font: titleFont, textColor: UIColor.black) + title = NSAttributedString(string: content.title ?? content.websiteName ?? hostName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor) if let image = content.image { iconImageRepresentation = smallestImageRepresentation(image.representations) @@ -131,10 +137,10 @@ final class ListMessageSnippetItemNode: ListMessageNode { let mutableDescriptionText = NSMutableAttributedString() if let text = content.text { - mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: UIColor.black)) + mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.theme.list.itemPrimaryTextColor)) } - mutableDescriptionText.append(NSAttributedString(string: content.displayUrl, font: descriptionFont, textColor: UIColor(0x007ee5))) + mutableDescriptionText.append(NSAttributedString(string: content.displayUrl, font: descriptionFont, textColor: item.theme.list.itemAccentColor)) let style = NSMutableParagraphStyle() style.lineSpacing = 4.0 @@ -174,6 +180,13 @@ final class ListMessageSnippetItemNode: ListMessageNode { return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: contentHeight), insets: UIEdgeInsets()), { _ in if let strongSelf = self { + strongSelf.appliedItem = item + + if let _ = updatedTheme { + strongSelf.separatorNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentHeight - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: contentHeight + UIScreenPixel)) diff --git a/TelegramUI/ListSectionHeaderNode.swift b/TelegramUI/ListSectionHeaderNode.swift index 8313c67700..b7e430e1ab 100644 --- a/TelegramUI/ListSectionHeaderNode.swift +++ b/TelegramUI/ListSectionHeaderNode.swift @@ -4,6 +4,7 @@ import Display final class ListSectionHeaderNode: ASDisplayNode { private let label: TextNode + private var theme: PresentationTheme var title: String? { didSet { @@ -12,7 +13,9 @@ final class ListSectionHeaderNode: ASDisplayNode { } } - override init() { + init(theme: PresentationTheme) { + self.theme = theme + self.label = TextNode() self.label.isLayerBacked = true self.label.isOpaque = true @@ -21,14 +24,25 @@ final class ListSectionHeaderNode: ASDisplayNode { self.addSubnode(self.label) - self.backgroundColor = UIColor(0xf7f7f7) + self.backgroundColor = theme.chatList.sectionHeaderFillColor + } + + func updateTheme(theme: PresentationTheme) { + if self.theme !== theme { + self.theme = theme + + self.backgroundColor = theme.chatList.sectionHeaderFillColor + if !self.bounds.size.width.isZero && !self.bounds.size.height.isZero { + self.layout() + } + } } override func layout() { let size = self.bounds.size let makeLayout = TextNode.asyncLayout(self.label) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: self.title ?? "", font: Font.medium(12.0), textColor: UIColor(0x8e8e93)), self.backgroundColor, 1, .end, CGSize(width: max(0.0, size.width - 18.0), height: size.height), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: self.title ?? "", font: Font.medium(12.0), textColor: self.theme.chatList.sectionHeaderTextColor), self.backgroundColor, 1, .end, CGSize(width: max(0.0, size.width - 18.0), height: size.height), .natural, nil, UIEdgeInsets()) let _ = labelApply() self.label.frame = CGRect(origin: CGPoint(x: 9.0, y: 6.0), size: labelLayout.size) } diff --git a/TelegramUI/Localizable.swift b/TelegramUI/Localizable.swift deleted file mode 100644 index f7298d61f8..0000000000 --- a/TelegramUI/Localizable.swift +++ /dev/null @@ -1,73 +0,0 @@ -// Generated using SwiftGen, by O.Halligon — https://github.com/AliSoftware/SwiftGen - -import Foundation - -enum L10n { - /// Group Created - case ChatServiceGroupCreated - /// %@ invited %@ - case ChatServiceGroupAddedMembers(String, String) - /// %@ joined group - case ChatServiceGroupAddedSelf(String) - /// %@ kicked %@ - case ChatServiceGroupRemovedMembers(String, String) - /// %@ left group - case ChatServiceGroupRemovedSelf(String) - /// %@ updated group photo - case ChatServiceGroupUpdatedPhoto(String) - /// %@ removed group photo - case ChatServiceGroupRemovedPhoto(String) - /// %@ renamed group to \"%@\" - case ChatServiceGroupUpdatedTitle(String, String) - /// %@ pinned %@ - case ChatServiceGroupUpdatedPinnedMessage(String, String) - /// %@ removed pinned message - case ChatServiceGroupRemovedPinnedMessage(String) - /// %@ joined group via invite link - case ChatServiceGroupJoinedByLink(String) - /// The group was upgraded to a supergroup - case ChatServiceGroupMigratedToSupergroup -} - -extension L10n: CustomStringConvertible { - var description: String { return self.string } - - var string: String { - switch self { - case .ChatServiceGroupCreated: - return L10n.tr("Chat.Service.Group.Created") - case .ChatServiceGroupAddedMembers(let p0, let p1): - return L10n.tr("Chat.Service.Group.AddedMembers", p0, p1) - case .ChatServiceGroupAddedSelf(let p0): - return L10n.tr("Chat.Service.Group.AddedSelf", p0) - case .ChatServiceGroupRemovedMembers(let p0, let p1): - return L10n.tr("Chat.Service.Group.RemovedMembers", p0, p1) - case .ChatServiceGroupRemovedSelf(let p0): - return L10n.tr("Chat.Service.Group.RemovedSelf", p0) - case .ChatServiceGroupUpdatedPhoto(let p0): - return L10n.tr("Chat.Service.Group.UpdatedPhoto", p0) - case .ChatServiceGroupRemovedPhoto(let p0): - return L10n.tr("Chat.Service.Group.RemovedPhoto", p0) - case .ChatServiceGroupUpdatedTitle(let p0, let p1): - return L10n.tr("Chat.Service.Group.UpdatedTitle", p0, p1) - case .ChatServiceGroupUpdatedPinnedMessage(let p0, let p1): - return L10n.tr("Chat.Service.Group.UpdatedPinnedMessage", p0, p1) - case .ChatServiceGroupRemovedPinnedMessage(let p0): - return L10n.tr("Chat.Service.Group.RemovedPinnedMessage", p0) - case .ChatServiceGroupJoinedByLink(let p0): - return L10n.tr("Chat.Service.Group.JoinedByLink", p0) - case .ChatServiceGroupMigratedToSupergroup: - return L10n.tr("Chat.Service.Group.MigratedToSupergroup") - } - } - - private static func tr(_ key: String, _ args: CVarArg...) -> String { - let format = NSLocalizedString(key, comment: "") - return String(format: format, locale: Locale.current, arguments: args) - } -} - -func tr(_ key: L10n) -> String { - return key.string -} - diff --git a/TelegramUI/ManagedAudioPlaylistPlayer.swift b/TelegramUI/ManagedAudioPlaylistPlayer.swift index 29c81bf230..b337830732 100644 --- a/TelegramUI/ManagedAudioPlaylistPlayer.swift +++ b/TelegramUI/ManagedAudioPlaylistPlayer.swift @@ -6,6 +6,7 @@ import SwiftSignalKit enum AudioPlaylistItemLabelInfo: Equatable { case music(title: String?, performer: String?) case voice + case video static func ==(lhs: AudioPlaylistItemLabelInfo, rhs: AudioPlaylistItemLabelInfo) -> Bool { switch lhs { @@ -21,6 +22,12 @@ enum AudioPlaylistItemLabelInfo: Equatable { } else { return false } + case .video: + if case .video = rhs { + return true + } else { + return false + } } } } @@ -110,11 +117,25 @@ struct AudioPlaylistStateAndStatus: Equatable { } } +private enum AudioPlaylistItemPlayer { + case player(MediaPlayer) + case videoContext(ManagedMediaId, MediaResource, MediaPlayer, Disposable) + + var player: MediaPlayer { + switch self { + case let .player(player): + return player + case let .videoContext(_, _, player, _): + return player + } + } +} + private final class AudioPlaylistItemState { let item: AudioPlaylistItem - let player: MediaPlayer? + let player: AudioPlaylistItemPlayer? - init(item: AudioPlaylistItem, player: MediaPlayer?) { + init(item: AudioPlaylistItem, player: AudioPlaylistItemPlayer?) { self.item = item self.player = player } @@ -128,23 +149,58 @@ private final class AudioPlaylistInternalState { final class ManagedAudioPlaylistPlayer { private let audioSessionManager: ManagedAudioSession + private let overlayMediaManager: OverlayMediaManager + private weak var mediaManager: MediaManager? private let postbox: Postbox let playlist: AudioPlaylist private let currentState = Atomic(value: AudioPlaylistInternalState()) private let currentStateAndStatusValue = Promise() + private let overlayContextValue = Promise<(ManagedMediaId, MediaResource, Disposable)?>(nil) var stateAndStatus: Signal { return self.currentStateAndStatusValue.get() } - init(audioSessionManager: ManagedAudioSession, postbox: Postbox, playlist: AudioPlaylist) { + var currentContext: (ManagedMediaId, MediaResource, Disposable)? + + var overlayContextDisposable: Disposable? + + init(audioSessionManager: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, mediaManager: MediaManager, postbox: Postbox, playlist: AudioPlaylist) { self.audioSessionManager = audioSessionManager + self.overlayMediaManager = overlayMediaManager + self.mediaManager = mediaManager self.postbox = postbox self.playlist = playlist + + self.overlayContextDisposable = (self.overlayContextValue.get() |> deliverOnMainQueue).start(next: { [weak self] context in + if let strongSelf = self { + var updated = false + if let lhsId = strongSelf.currentContext?.0, let rhsId = context?.0 { + updated = !lhsId.isEqual(to: rhsId) + } else if (strongSelf.currentContext?.0 != nil) != (context?.0 != nil) { + updated = true + } + if updated { + strongSelf.currentContext?.2.dispose() + if let id = strongSelf.currentContext?.0 { + strongSelf.overlayMediaManager.controller?.removeVideoContext(id: id) + } + strongSelf.currentContext = context + if let (id, resource, _) = context, let mediaManager = strongSelf.mediaManager { + strongSelf.overlayMediaManager.controller?.addVideoContext(mediaManager: mediaManager, postbox: postbox, id: id, resource: resource, priority: 0) + } + } + } + }) } deinit { + self.overlayContextDisposable?.dispose() + if let id = self.currentContext?.0 { + self.overlayMediaManager.controller?.removeVideoContext(id: id) + } + self.currentContext?.2.dispose() self.currentState.with { state -> Void in state.navigationDisposable.dispose() } @@ -157,13 +213,13 @@ final class ManagedAudioPlaylistPlayer { if let item = state.currentItem { switch playback { case .play: - item.player?.play() + item.player?.player.play() case .pause: - item.player?.pause() + item.player?.player.pause() case .togglePlayPause: - item.player?.togglePlayPause() + item.player?.player.togglePlayPause() case let .seek(timestamp): - item.player?.seek(timestamp: timestamp) + item.player?.player.seek(timestamp: timestamp) } } } @@ -174,52 +230,102 @@ final class ManagedAudioPlaylistPlayer { state.navigationDisposable.set(disposable) currentItem = state.currentItem?.item } - disposable.set(self.playlist.navigate(currentItem, navigation).start(next: { [weak self] item in - if let strongSelf = self { - let updatedStateAndStatus = strongSelf.currentState.with { state -> AudioPlaylistStateAndStatus in - if let item = item { - if let item = item as? PeerMessageHistoryAudioPlaylistItem { - switch item.entry { - case let .MessageEntry(message, _, _, _): - if message.flags.contains(.Incoming) { - for attribute in message.attributes { - if let attribute = attribute as? ConsumableContentMessageAttribute { - if !attribute.consumed { - let _ = markMessageContentAsConsumedInteractively(postbox: strongSelf.postbox, messageId: message.id).start() - } - break - } - } + let postbox = self.postbox + let audioSessionManager = self.audioSessionManager + let overlayMediaManager = self.overlayMediaManager + let mediaManager = self.mediaManager + disposable.set((self.playlist.navigate(currentItem, navigation) + |> deliverOnMainQueue + |> mapToSignal { [weak mediaManager] item -> Signal<(AudioPlaylistItem, AudioPlaylistItemState)?, NoError> in + if let item = item { + var instantVideo: (MediaResource, MessageId)? + if let item = item as? PeerMessageHistoryAudioPlaylistItem { + switch item.entry { + case let .MessageEntry(message, _, _, _): + for media in message.media { + if let file = media as? TelegramMediaFile { + if file.isInstantVideo { + instantVideo = (file.resource, message.id) } - case .HoleEntry: - break - } - } - - if let resource = item.resource { - let player = MediaPlayer(audioSessionManager: strongSelf.audioSessionManager, postbox: strongSelf.postbox, resource: resource, streamable: item.streamable, video: false, preferSoftwareDecoding: false, enableSound: true) - player.actionAtEnd = .action({ - if let strongSelf = self { - strongSelf.control(.navigation(.next)) } - }) - state.currentItem = AudioPlaylistItemState(item: item, player: player) - player.play() + } + if message.flags.contains(.Incoming) { + for attribute in message.attributes { + if let attribute = attribute as? ConsumableContentMessageAttribute { + if !attribute.consumed { + let _ = markMessageContentAsConsumedInteractively(postbox: postbox, messageId: message.id).start() + } + break + } + } + } + case .HoleEntry: + break + } + } + + if let resource = item.resource { + var itemPlayer: AudioPlaylistItemPlayer? + if let instantVideo = instantVideo { + if let mediaManager = mediaManager { + let (player, disposable) = mediaManager.videoContext(postbox: postbox, id: PeerMessageManagedMediaId(messageId: instantVideo.1), resource: instantVideo.0, preferSoftwareDecoding: false, backgroundThread: false, priority: -1, initiatePlayback: true, activate: { _ in + }, deactivate: { + return .complete() + }) + itemPlayer = .videoContext(PeerMessageManagedMediaId(messageId: instantVideo.1), instantVideo.0, player, disposable) + } + } else { + let player = MediaPlayer(audioSessionManager: audioSessionManager, overlayMediaManager: overlayMediaManager, postbox: postbox, resource: resource, streamable: item.streamable, video: false, preferSoftwareDecoding: false, enableSound: true) + itemPlayer = .player(player) + } + return .single((item, AudioPlaylistItemState(item: item, player: itemPlayer))) + } else { + return .single((item, AudioPlaylistItemState(item: item, player: nil))) + } + } else { + return .single(nil) + } + }).start(next: { [weak self] next in + if let strongSelf = self { + let updatedStateAndStatus = strongSelf.currentState.with { state -> AudioPlaylistStateAndStatus in + if let (item, itemState) = next { + state.currentItem = itemState + if let player = itemState.player { + switch player { + case let .player(player): + player.play() + player.actionAtEnd = .action({ + if let strongSelf = self { + strongSelf.control(.navigation(.next)) + } + }) + case let .videoContext(_, _, player, _): + player.actionAtEnd = .loopDisablingSound({ + if let strongSelf = self { + strongSelf.control(.navigation(.next)) + } + }) + player.playOnceWithSound() + } + } let playbackId = state.nextPlaybackId state.nextPlaybackId += 1 - return AudioPlaylistStateAndStatus(state: AudioPlaylistState(playlistId: strongSelf.playlist.id, item: item), playbackId: playbackId, status: player.status) + return AudioPlaylistStateAndStatus(state: AudioPlaylistState(playlistId: strongSelf.playlist.id, item: item), playbackId: playbackId, status: itemState.player?.player.status) } else { - state.currentItem = AudioPlaylistItemState(item: item, player: nil) - return AudioPlaylistStateAndStatus(state: AudioPlaylistState(playlistId: strongSelf.playlist.id, item: item), playbackId: 0, status: nil) + state.currentItem = nil + return AudioPlaylistStateAndStatus(state: AudioPlaylistState(playlistId: strongSelf.playlist.id, item: nil), playbackId: 0, status: nil) } - } else { - state.currentItem = nil - return AudioPlaylistStateAndStatus(state: AudioPlaylistState(playlistId: strongSelf.playlist.id, item: nil), playbackId: 0, status: nil) } + strongSelf.currentStateAndStatusValue.set(.single(updatedStateAndStatus)) + var overlayContextValue: (ManagedMediaId, MediaResource, Disposable)? + if let (_, itemState) = next { + if let player = itemState.player, case let .videoContext(id, resource, _, disposable) = player { + overlayContextValue = (id, resource, disposable) + } + } + strongSelf.overlayContextValue.set(.single(overlayContextValue)) } - strongSelf.currentStateAndStatusValue.set(.single(updatedStateAndStatus)) - } - })) + })) } } } diff --git a/TelegramUI/ManagedAudioSession.swift b/TelegramUI/ManagedAudioSession.swift index b3de1ddb07..f4e6941cd5 100644 --- a/TelegramUI/ManagedAudioSession.swift +++ b/TelegramUI/ManagedAudioSession.swift @@ -3,55 +3,142 @@ import SwiftSignalKit import AVFoundation enum ManagedAudioSessionType { - case none case play case playAndRecord + case voiceCall } private func nativeCategoryForType(_ type: ManagedAudioSessionType) -> String { switch type { - case .none: - return AVAudioSessionCategoryPlayback case .play: return AVAudioSessionCategoryPlayback - case .playAndRecord: + case .playAndRecord, .voiceCall: return AVAudioSessionCategoryPlayAndRecord } } +private func allowBluetoothForType(_ type: ManagedAudioSessionType) -> Bool { + switch type { + case .play: + return false + case .playAndRecord, .voiceCall: + return true + } +} + private final class HolderRecord { let id: Int32 let audioSessionType: ManagedAudioSessionType - let activate: () -> Void + let control: ManagedAudioSessionControl + let activate: (ManagedAudioSessionControl) -> Void let deactivate: () -> Signal let once: Bool + var overrideSpeaker: Bool var active: Bool = false var deactivatingDisposable: Disposable? = nil - init(id: Int32, audioSessionType: ManagedAudioSessionType, activate: @escaping () -> Void, deactivate: @escaping () -> Signal, once: Bool) { + init(id: Int32, audioSessionType: ManagedAudioSessionType, control: ManagedAudioSessionControl, activate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal, once: Bool, overrideSpeaker: Bool) { self.id = id self.audioSessionType = audioSessionType + self.control = control self.activate = activate self.deactivate = deactivate self.once = once + self.overrideSpeaker = overrideSpeaker } } -final class ManagedAudioSession { +public class ManagedAudioSessionControl { + private let setupImpl: (Bool) -> Void + private let activateImpl: () -> Void + private let setSpeakerImpl: (Bool) -> Void + + fileprivate init(setupImpl: @escaping (Bool) -> Void, activateImpl: @escaping () -> Void, setSpeakerImpl: @escaping (Bool) -> Void) { + self.setupImpl = setupImpl + self.activateImpl = activateImpl + self.setSpeakerImpl = setSpeakerImpl + } + + public func setup(synchronous: Bool = false) { + self.setupImpl(synchronous) + } + + public func activate() { + self.activateImpl() + } + + public func setSpeaker(_ value: Bool) { + self.setSpeakerImpl(value) + } +} + +public final class ManagedAudioSession { private var nextId: Int32 = 0 private let queue = Queue() private var holders: [HolderRecord] = [] - private var currentType: ManagedAudioSessionType = .none + private var currentTypeAndOverrideSpeaker: (ManagedAudioSessionType, Bool)? private var deactivateTimer: SwiftSignalKit.Timer? deinit { self.deactivateTimer?.invalidate() } - func push(audioSessionType: ManagedAudioSessionType, activate: @escaping () -> Void, deactivate: @escaping () -> Signal, once: Bool = false) -> Disposable { + func push(audioSessionType: ManagedAudioSessionType, overrideSpeaker: Bool = false, once: Bool = false, activate: @escaping () -> Void, deactivate: @escaping () -> Signal) -> Disposable { + return self.push(audioSessionType: audioSessionType, once: once, manualActivate: { control in + control.setup() + control.activate() + activate() + }, deactivate: deactivate) + } + + func push(audioSessionType: ManagedAudioSessionType, overrideSpeaker: Bool = false, once: Bool = false, manualActivate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal) -> Disposable { let id = OSAtomicIncrement32(&self.nextId) self.queue.async { - self.holders.append(HolderRecord(id: id, audioSessionType: audioSessionType, activate: activate, deactivate: deactivate, once: once)) + self.holders.append(HolderRecord(id: id, audioSessionType: audioSessionType, control: ManagedAudioSessionControl(setupImpl: { [weak self] synchronous in + if let strongSelf = self { + let f: () -> Void = { + for holder in strongSelf.holders { + if holder.id == id && holder.active { + strongSelf.setup(type: audioSessionType, overrideSpeaker: holder.overrideSpeaker) + break + } + } + } + + if synchronous { + strongSelf.queue.sync(f) + } else { + strongSelf.queue.async(f) + } + } + }, activateImpl: { [weak self] in + if let strongSelf = self { + strongSelf.queue.async { + for holder in strongSelf.holders { + if holder.id == id && holder.active { + strongSelf.activate() + break + } + } + } + } + }, setSpeakerImpl: { [weak self] value in + if let strongSelf = self { + strongSelf.queue.async { + for holder in strongSelf.holders { + if holder.id == id { + if holder.overrideSpeaker != value { + holder.overrideSpeaker = value + } + + if holder.active { + strongSelf.update(overrideSpeaker: value) + } + } + } + } + } + }), activate: manualActivate, deactivate: deactivate, once: once, overrideSpeaker: overrideSpeaker)) self.updateHolders() } return ActionDisposable { [weak self] in @@ -96,71 +183,138 @@ final class ManagedAudioSession { index += 1 } if !deactivating { - if let activeIndex = activeIndex, activeIndex != self.holders.count - 1 { - self.holders[activeIndex].active = false - let id = self.holders[activeIndex].id - self.holders[activeIndex].deactivatingDisposable = (self.holders[activeIndex].deactivate() |> deliverOn(self.queue)).start(completed: { [weak self] in - if let strongSelf = self { - var index = 0 - for currentRecord in strongSelf.holders { - if currentRecord.id == id { - currentRecord.deactivatingDisposable = nil - if currentRecord.once { - strongSelf.holders.remove(at: index) - } - break - } - index += 1 - } - strongSelf.updateHolders() + if let activeIndex = activeIndex { + var deactivate = false + + if activeIndex != self.holders.count - 1 { + if self.holders[activeIndex].audioSessionType == .voiceCall { + deactivate = false + } else { + deactivate = true } - }) + } + + if deactivate { + self.holders[activeIndex].active = false + let id = self.holders[activeIndex].id + self.holders[activeIndex].deactivatingDisposable = (self.holders[activeIndex].deactivate() |> deliverOn(self.queue)).start(completed: { [weak self] in + if let strongSelf = self { + var index = 0 + for currentRecord in strongSelf.holders { + if currentRecord.id == id { + currentRecord.deactivatingDisposable = nil + if currentRecord.once { + strongSelf.holders.remove(at: index) + } + break + } + index += 1 + } + strongSelf.updateHolders() + } + }) + } } else if activeIndex == nil { let lastIndex = self.holders.count - 1 + + self.deactivateTimer?.invalidate() + self.deactivateTimer = nil + self.holders[lastIndex].active = true - self.applyType(self.holders[lastIndex].audioSessionType) - self.holders[lastIndex].activate() + self.holders[lastIndex].activate(self.holders[lastIndex].control) } } } else { - self.applyTypeNoneDelayed() + self.applyNoneDelayed() } } - private func applyTypeNoneDelayed() { + private func applyNoneDelayed() { self.deactivateTimer?.invalidate() - let deactivateTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in - if let strongSelf = self { - strongSelf.applyType(.none) - } - }, queue: self.queue) - self.deactivateTimer = deactivateTimer - deactivateTimer.start() + + if self.currentTypeAndOverrideSpeaker?.0 == .voiceCall { + self.applyNone() + } else { + let deactivateTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.applyNone() + } + }, queue: self.queue) + self.deactivateTimer = deactivateTimer + deactivateTimer.start() + } } - private func applyType(_ type: ManagedAudioSessionType) { - if type != .none { - self.deactivateTimer?.invalidate() - self.deactivateTimer = nil - } + private func applyNone() { + self.deactivateTimer?.invalidate() + self.deactivateTimer = nil - if self.currentType != type { - self.currentType = type + self.currentTypeAndOverrideSpeaker = nil + + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch let error { + print("ManagedAudioSession applyNone error \(error)") + } + } + + private func setup(type: ManagedAudioSessionType, overrideSpeaker: Bool) { + self.deactivateTimer?.invalidate() + self.deactivateTimer = nil + + if self.currentTypeAndOverrideSpeaker == nil || self.currentTypeAndOverrideSpeaker! != (type, overrideSpeaker) { + self.currentTypeAndOverrideSpeaker = (type, overrideSpeaker) do { - if type != .none { - print("ManagedAudioSession setting category for \(type)") - try AVAudioSession.sharedInstance().setCategory(nativeCategoryForType(type)) - print("ManagedAudioSession setting active \(type != .none)") - try AVAudioSession.sharedInstance().setActive(type != .none) - } else { - print("ManagedAudioSession setting active false") - try AVAudioSession.sharedInstance().setActive(false) - } + print("ManagedAudioSession setting category for \(type)") + try AVAudioSession.sharedInstance().setCategory(nativeCategoryForType(type), with: AVAudioSessionCategoryOptions(rawValue: allowBluetoothForType(type) ? AVAudioSessionCategoryOptions.allowBluetooth.rawValue : 0)) + print("ManagedAudioSession setting active \(type != .none)") + try AVAudioSession.sharedInstance().setMode(type == .voiceCall ? AVAudioSessionModeVoiceChat : AVAudioSessionModeDefault) } catch let error { - print("ManagedAudioSession applyType error \(error)") + print("ManagedAudioSession setup error \(error)") } - //[[AVAudioSession sharedInstance] setCategory:[self nativeCategoryForType:type] withOptions:(type == TGAudioSessionTypePlayAndRecord || type == TGAudioSessionTypePlayAndRecordHeadphones) ? AVAudioSessionCategoryOptionAllowBluetooth : 0 error:&error]; } } + + private func activate() { + if let (type, overrideSpeaker) = self.currentTypeAndOverrideSpeaker { + do { + try AVAudioSession.sharedInstance().setActive(true) + try AVAudioSession.sharedInstance().overrideOutputAudioPort(overrideSpeaker ? .speaker : .none) + + if case .voiceCall = type { + try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.005) + } + } catch let error { + print("ManagedAudioSession activate error \(error)") + } + } + } + + private func update(overrideSpeaker: Bool) { + if let (type, currentOverrideSpeaker) = self.currentTypeAndOverrideSpeaker, currentOverrideSpeaker != overrideSpeaker { + self.currentTypeAndOverrideSpeaker = (type, overrideSpeaker) + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(overrideSpeaker ? .speaker : .none) + } catch let error { + print("ManagedAudioSession overrideOutputAudioPort error \(error)") + } + } + } + + func callKitActivatedAudioSession() { + /*self.queue.async { + print("ManagedAudioSession callKitDeactivatedAudioSession") + self.callKitAudioSessionIsActive = true + self.updateHolders() + }*/ + } + + func callKitDeactivatedAudioSession() { + /*self.queue.async { + print("ManagedAudioSession callKitDeactivatedAudioSession") + self.callKitAudioSessionIsActive = false + self.updateHolders() + }*/ + } } diff --git a/TelegramUI/ManagedVideoNode.swift b/TelegramUI/ManagedVideoNode.swift index b38573e30e..d84bc6d0ca 100644 --- a/TelegramUI/ManagedVideoNode.swift +++ b/TelegramUI/ManagedVideoNode.swift @@ -5,11 +5,12 @@ import Postbox import TelegramCore class ManagedVideoNode: ASDisplayNode { - private var videoContext: ManagedVideoContext? + private var videoPlayer: MediaPlayer? + private var playerNode: MediaPlayerNode? private let videoContextDisposable = MetaDisposable() var transformArguments: TransformImageArguments? { didSet { - self.videoContext?.playerNode.transformArguments = self.transformArguments + self.playerNode?.transformArguments = self.transformArguments } } @@ -36,34 +37,51 @@ class ManagedVideoNode: ASDisplayNode { self.videoContextDisposable.set(nil) } - func acquireContext(account: Account, mediaManager: MediaManager, id: ManagedMediaId, resource: MediaResource) { - self.videoContextDisposable.set((mediaManager.videoContext(account: account, id: id, resource: resource, preferSoftwareDecoding: self.preferSoftwareDecoding, backgroundThread: self.backgroundThread) |> deliverOnMainQueue).start(next: { [weak self] videoContext in + func acquireContext(account: Account, mediaManager: MediaManager, id: ManagedMediaId, resource: MediaResource, priority: Int32) { + let (player, disposable) = mediaManager.videoContext(postbox: account.postbox, id: id, resource: resource, preferSoftwareDecoding: false, backgroundThread: false, priority: priority, initiatePlayback: true, activate: { [weak self] playerNode in if let strongSelf = self { - if strongSelf.videoContext !== videoContext { - if let videoContext = strongSelf.videoContext { - if videoContext.playerNode.supernode == self { - videoContext.playerNode.removeFromSupernode() - } - } - - strongSelf.videoContext = videoContext - strongSelf._player.set(.single(videoContext?.mediaPlayer)) - if let videoContext = videoContext { - strongSelf.addSubnode(videoContext.playerNode) - videoContext.playerNode.transformArguments = strongSelf.transformArguments - strongSelf.setNeedsLayout() - //videoContext.mediaPlayer.play() + if strongSelf.playerNode !== playerNode { + if strongSelf.playerNode?.supernode === self { + strongSelf.playerNode?.removeFromSupernode() } + strongSelf.playerNode = playerNode + strongSelf.addSubnode(playerNode) + playerNode.transformArguments = strongSelf.transformArguments + strongSelf.setNeedsLayout() } } - })) + }, deactivate: { [weak self] in + if let strongSelf = self { + if let playerNode = strongSelf.playerNode { + strongSelf.playerNode = nil + if playerNode.supernode === strongSelf { + playerNode.removeFromSupernode() + } + } + return .complete() + } else { + return .complete() + } + }) + + self._player.set(.single(player)) + self.videoContextDisposable.set(disposable) + } + + func discardContext() { + self._player.set(.single(nil)) + if let playerNode = self.playerNode { + self.playerNode = nil + if playerNode.supernode === self { + playerNode.removeFromSupernode() + } + } + self.videoContextDisposable.set(nil) } override func layout() { super.layout() - if let videoContext = videoContext { - videoContext.playerNode.frame = self.bounds - } + self.playerNode?.frame = self.bounds } } diff --git a/TelegramUI/MapInputController.swift b/TelegramUI/MapInputController.swift index bff6172f18..8e24bd5d02 100644 --- a/TelegramUI/MapInputController.swift +++ b/TelegramUI/MapInputController.swift @@ -18,7 +18,7 @@ final class MapInputController: ViewController { } init() { - super.init(navigationBar: NavigationBar()) + super.init(navigationBarTheme: nil) self._ready.set(.single(true)) diff --git a/TelegramUI/Markdown.swift b/TelegramUI/Markdown.swift index 8077183958..26123b78ba 100644 --- a/TelegramUI/Markdown.swift +++ b/TelegramUI/Markdown.swift @@ -7,10 +7,12 @@ private let controlCharactersSet = CharacterSet(charactersIn: "[]()*_-\\") final class MarkdownAttributeSet { let font: UIFont let textColor: UIColor + let additionalAttributes: [String: Any] - init(font: UIFont, textColor: UIColor) { + init(font: UIFont, textColor: UIColor, additionalAttributes: [String: Any] = [:]) { self.font = font self.textColor = textColor + self.additionalAttributes = additionalAttributes } } @@ -46,12 +48,23 @@ func escapedPlaintextForMarkdown(_ string: String) -> String { return result as String } -func parseMarkdownIntoAttributedString(_ string: String, attributes: MarkdownAttributes) -> NSAttributedString { +func paragraphStyleWithAlignment(_ alignment: NSTextAlignment) -> NSParagraphStyle { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = alignment + return paragraphStyle +} + +func parseMarkdownIntoAttributedString(_ string: String, attributes: MarkdownAttributes, textAlignment: NSTextAlignment = .natural) -> NSAttributedString { let nsString = string as NSString let result = NSMutableAttributedString() var remainingRange = NSMakeRange(0, nsString.length) - let bodyAttributes: [String: Any] = [NSFontAttributeName: attributes.body.font, NSForegroundColorAttributeName: attributes.body.textColor] + var bodyAttributes: [String: Any] = [NSFontAttributeName: attributes.body.font, NSForegroundColorAttributeName: attributes.body.textColor, NSParagraphStyleAttributeName: paragraphStyleWithAlignment(textAlignment)] + if !attributes.body.additionalAttributes.isEmpty { + for (key, value) in attributes.body.additionalAttributes { + bodyAttributes[key] = value + } + } while true { let range = nsString.rangeOfCharacter(from: controlStartCharactersSet, options: [], range: remainingRange) @@ -65,7 +78,12 @@ func parseMarkdownIntoAttributedString(_ string: String, attributes: MarkdownAtt if character == UInt16(("[" as UnicodeScalar).value) { remainingRange = NSMakeRange(range.location + range.length, remainingRange.location + remainingRange.length - (range.location + range.length)) if let (parsedLinkText, parsedLinkContents) = parseLink(string: nsString, remainingRange: &remainingRange) { - var linkAttributes: [String: Any] = [NSFontAttributeName: attributes.link.font, NSForegroundColorAttributeName: attributes.link.textColor] + var linkAttributes: [String: Any] = [NSFontAttributeName: attributes.link.font, NSForegroundColorAttributeName: attributes.link.textColor, NSParagraphStyleAttributeName: paragraphStyleWithAlignment(textAlignment)] + if !attributes.body.additionalAttributes.isEmpty { + for (key, value) in attributes.link.additionalAttributes { + linkAttributes[key] = value + } + } if let (attributeName, attributeValue) = attributes.linkAttribute(parsedLinkContents) { linkAttributes[attributeName] = attributeValue } @@ -78,7 +96,12 @@ func parseMarkdownIntoAttributedString(_ string: String, attributes: MarkdownAtt remainingRange = NSMakeRange(range.location + range.length + 1, remainingRange.location + remainingRange.length - (range.location + range.length + 1)) if let bold = parseBold(string: nsString, remainingRange: &remainingRange) { - let boldAttributes: [String: Any] = [NSFontAttributeName: attributes.bold.font, NSForegroundColorAttributeName: attributes.bold.textColor] + var boldAttributes: [String: Any] = [NSFontAttributeName: attributes.bold.font, NSForegroundColorAttributeName: attributes.bold.textColor, NSParagraphStyleAttributeName: paragraphStyleWithAlignment(textAlignment)] + if !attributes.body.additionalAttributes.isEmpty { + for (key, value) in attributes.bold.additionalAttributes { + boldAttributes[key] = value + } + } result.append(NSAttributedString(string: bold, attributes: boldAttributes)) } else { result.append(NSAttributedString(string: nsString.substring(with: NSMakeRange(remainingRange.location, 1)), attributes: bodyAttributes)) diff --git a/TelegramUI/MediaManager.swift b/TelegramUI/MediaManager.swift index 0e498a5dab..78ad88ea08 100644 --- a/TelegramUI/MediaManager.swift +++ b/TelegramUI/MediaManager.swift @@ -47,7 +47,7 @@ private final class ManagedAudioPlaylistPlayerStatusesContext { } } -private struct WrappedManagedMediaId: Hashable { +struct WrappedManagedMediaId: Hashable { let id: ManagedMediaId var hashValue: Int { @@ -61,27 +61,131 @@ private struct WrappedManagedMediaId: Hashable { final class ManagedVideoContext { let mediaPlayer: MediaPlayer - let playerNode: MediaPlayerNode + let playerNode: MediaPlayerNode? - init(mediaPlayer: MediaPlayer, playerNode: MediaPlayerNode) { + init(mediaPlayer: MediaPlayer, playerNode: MediaPlayerNode?) { self.mediaPlayer = mediaPlayer self.playerNode = playerNode } } -private final class ActiveManagedVideoContext { - let context: ManagedVideoContext - let contextSubscribers = Bag<(ManagedVideoContext?) -> Void>() +final class ManagedVideoContextSubscriber { + let id: Int32 + let priority: Int32 + var active = false + let activate: (MediaPlayerNode) -> Void + let deactivate: () -> Signal + var deactivatingDisposable: Disposable? = nil - init(context: ManagedVideoContext) { - self.context = context + init(id: Int32, priority: Int32, activate: @escaping (MediaPlayerNode) -> Void, deactivate: @escaping () -> Signal) { + self.id = id + self.priority = priority + self.activate = activate + self.deactivate = deactivate } } -final class MediaManager: NSObject { +private final class ActiveManagedVideoContext { + let mediaPlayer: MediaPlayer + let playerNode: MediaPlayerNode + private var becameEmpty: () -> Void + private var nextSubscriberId: Int32 = 0 + var contextSubscribers: [ManagedVideoContextSubscriber] = [] + + init(mediaPlayer: MediaPlayer, playerNode: MediaPlayerNode, becameEmpty: @escaping () -> Void) { + self.mediaPlayer = mediaPlayer + self.playerNode = playerNode + self.becameEmpty = becameEmpty + } + + func addContextSubscriber(priority: Int32, activate: @escaping (MediaPlayerNode) -> Void, deactivate: @escaping () -> Signal) -> Disposable { + let id = self.nextSubscriberId + self.nextSubscriberId += 1 + self.contextSubscribers.append(ManagedVideoContextSubscriber(id: id, priority: priority, activate: activate, deactivate: deactivate)) + self.contextSubscribers.sort(by: { lhs, rhs in + if lhs.priority != rhs.priority { + return lhs.priority < rhs.priority + } else { + return lhs.id < rhs.id + } + }) + self.updateSubscribers() + + return ActionDisposable { [weak self] in + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.removeDeactivatedSubscriber(id: id) + } + } + } + } + + private func removeDeactivatedSubscriber(id: Int32) { + assert(Queue.mainQueue().isCurrent()) + + for i in 0 ..< self.contextSubscribers.count { + if self.contextSubscribers[i].id == id { + self.contextSubscribers[i].deactivatingDisposable?.dispose() + self.contextSubscribers.remove(at: i) + self.updateSubscribers() + break + } + } + } + + private func updateSubscribers() { + assert(Queue.mainQueue().isCurrent()) + + if !self.contextSubscribers.isEmpty { + var activeIndex: Int? + var deactivating = false + var index = 0 + for subscriber in self.contextSubscribers { + if subscriber.active { + activeIndex = index + break + } + else if subscriber.deactivatingDisposable != nil { + deactivating = false + } + index += 1 + } + if !deactivating { + if let activeIndex = activeIndex, activeIndex != self.contextSubscribers.count - 1 { + self.contextSubscribers[activeIndex].active = false + let id = self.contextSubscribers[activeIndex].id + self.contextSubscribers[activeIndex].deactivatingDisposable = (self.contextSubscribers[activeIndex].deactivate() |> deliverOn(Queue.mainQueue())).start(completed: { [weak self] in + if let strongSelf = self { + var index = 0 + for currentRecord in strongSelf.contextSubscribers { + if currentRecord.id == id { + currentRecord.deactivatingDisposable = nil + break + } + index += 1 + } + strongSelf.updateSubscribers() + } + }) + } else if activeIndex == nil { + let lastIndex = self.contextSubscribers.count - 1 + self.contextSubscribers[lastIndex].active = true + //self.applyType(self.contextSubscribers[lastIndex].audioSessionType) + + self.contextSubscribers[lastIndex].activate(self.playerNode) + } + } + } else { + self.becameEmpty() + } + } +} + +public final class MediaManager: NSObject { private let queue = Queue.mainQueue() - let audioSession = ManagedAudioSession() + public let audioSession: ManagedAudioSession + let overlayMediaManager = OverlayMediaManager() private let playlistPlayer = Atomic(value: nil) private let playlistPlayerStateAndStatusValue = Promise(nil) @@ -99,6 +203,8 @@ final class MediaManager: NSObject { private var managedVideoContexts: [WrappedManagedMediaId: ActiveManagedVideoContext] = [:] override init() { + self.audioSession = ManagedAudioSession() + super.init() let commandCenter = MPRemoteCommandCenter.shared() @@ -142,6 +248,10 @@ final class MediaManager: NSObject { case .voice: let titleText: String = "Voice Message" + nowPlayingInfo[MPMediaItemPropertyTitle] = titleText + case .video: + let titleText: String = "Video Message" + nowPlayingInfo[MPMediaItemPropertyTitle] = titleText } @@ -204,54 +314,39 @@ final class MediaManager: NSObject { self.globalControlsStatusDisposable.dispose() } - func videoContext(account: Account, id: ManagedMediaId, resource: MediaResource, preferSoftwareDecoding: Bool, backgroundThread: Bool) -> Signal { - return Signal { subscriber in - let disposable = MetaDisposable() + //func push(audioSessionType: ManagedAudioSessionType, activate: @escaping () -> Void, deactivate: @escaping () -> Signal, once: Bool = false) -> Disposable { + + func videoContext(postbox: Postbox, id: ManagedMediaId, resource: MediaResource, preferSoftwareDecoding: Bool, backgroundThread: Bool, priority: Int32, initiatePlayback: Bool, activate: @escaping (MediaPlayerNode) -> Void, deactivate: @escaping () -> Signal) -> (MediaPlayer, Disposable) { + assert(Queue.mainQueue().isCurrent()) + + let wrappedId = WrappedManagedMediaId(id: id) + let activeContext: ActiveManagedVideoContext + var startPlayback = false + if let currentActiveContext = self.managedVideoContexts[wrappedId] { + activeContext = currentActiveContext + } else { + let mediaPlayer = MediaPlayer(audioSessionManager: self.audioSession, overlayMediaManager: self.overlayMediaManager, postbox: postbox, resource: resource, streamable: false, video: true, preferSoftwareDecoding: preferSoftwareDecoding, enableSound: false) + mediaPlayer.actionAtEnd = .loop + let playerNode = MediaPlayerNode(backgroundThread: backgroundThread) + mediaPlayer.attachPlayerNode(playerNode) - self.queue.async { - let wrappedId = WrappedManagedMediaId(id: id) - let activeContext: ActiveManagedVideoContext - if let currentActiveContext = self.managedVideoContexts[wrappedId] { - activeContext = currentActiveContext - } else { - let mediaPlayer = MediaPlayer(audioSessionManager: self.audioSession, postbox: account.postbox, resource: resource, streamable: false, video: true, preferSoftwareDecoding: preferSoftwareDecoding, enableSound: false) - mediaPlayer.actionAtEnd = .loop - let playerNode = MediaPlayerNode(backgroundThread: backgroundThread) - mediaPlayer.attachPlayerNode(playerNode) - activeContext = ActiveManagedVideoContext(context: ManagedVideoContext(mediaPlayer: mediaPlayer, playerNode: playerNode)) - self.managedVideoContexts[wrappedId] = activeContext + activeContext = ActiveManagedVideoContext(mediaPlayer: mediaPlayer, playerNode: playerNode, becameEmpty: { [weak self] in + if let strongSelf = self { + strongSelf.managedVideoContexts[wrappedId]?.playerNode.removeFromSupernode() + strongSelf.managedVideoContexts.removeValue(forKey: wrappedId) } - - let index = activeContext.contextSubscribers.add({ context in - subscriber.putNext(context) - }) - - for (subscriberIndex, subscriberSink) in activeContext.contextSubscribers.copyItemsWithIndices() { - if subscriberIndex == index { - subscriberSink(activeContext.context) - } else { - subscriberSink(nil) - } - } - - disposable.set(ActionDisposable { - self.queue.async { - if let activeContext = self.managedVideoContexts[wrappedId] { - activeContext.contextSubscribers.remove(index) - - if activeContext.contextSubscribers.isEmpty { - self.managedVideoContexts.removeValue(forKey: wrappedId) - } else { - let lastSubscriber = activeContext.contextSubscribers.copyItemsWithIndices().last!.1 - lastSubscriber(activeContext.context) - } - } - } - }) + }) + self.managedVideoContexts[wrappedId] = activeContext + if initiatePlayback { + startPlayback = true } - - return disposable } + + if startPlayback { + activeContext.mediaPlayer.play() + } + + return (activeContext.mediaPlayer, activeContext.addContextSubscriber(priority: priority, activate: activate, deactivate: deactivate)) } func audioRecorder() -> Signal { diff --git a/TelegramUI/MediaNavigationAccessoryContainerNode.swift b/TelegramUI/MediaNavigationAccessoryContainerNode.swift index f96a550238..bdf28b576e 100644 --- a/TelegramUI/MediaNavigationAccessoryContainerNode.swift +++ b/TelegramUI/MediaNavigationAccessoryContainerNode.swift @@ -18,14 +18,18 @@ final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecog } } + private var presentationData: PresentationData + init(account: Account) { + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + self.backgroundNode = ASDisplayNode() - self.headerNode = MediaNavigationAccessoryHeaderNode() + self.headerNode = MediaNavigationAccessoryHeaderNode(theme: self.presentationData.theme, strings: self.presentationData.strings) self.itemListNode = MediaNavigationAccessoryItemListNode(account: account) super.init() - self.backgroundNode.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0) + self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor self.addSubnode(self.backgroundNode) self.addSubnode(self.itemListNode) diff --git a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift index afe3bd838c..00d7c24720 100644 --- a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift +++ b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift @@ -2,40 +2,18 @@ import Foundation import AsyncDisplayKit import Display -private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x9099A2).cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - context.move(to: CGPoint(x: 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) - context.strokePath() - context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) - context.strokePath() -}) - private let titleFont = Font.regular(12.0) private let subtitleFont = Font.regular(10.0) private let maximizedTitleFont = Font.bold(17.0) private let maximizedSubtitleFont = Font.regular(12.0) -private let titleColor = UIColor.black -private let subtitleColor = UIColor(0x8b8b8b) - -private let playIcon = UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPlay")?.precomposed() -private let pauseIcon = UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPause")?.precomposed() -private let maximizedPlayIcon = UIImage(bundleImageName: "GlobalMusicPlayer/Play")?.precomposed() -private let maximizedPauseIcon = UIImage(bundleImageName: "GlobalMusicPlayer/Pause")?.precomposed() -private let maximizedPreviousIcon = UIImage(bundleImageName: "GlobalMusicPlayer/Previous")?.precomposed() -private let maximizedNextIcon = UIImage(bundleImageName: "GlobalMusicPlayer/Next")?.precomposed() -private let maximizedShuffleIcon = UIImage(bundleImageName: "GlobalMusicPlayer/Shuffle")?.precomposed() -private let maximizedRepeatIcon = UIImage(bundleImageName: "GlobalMusicPlayer/Repeat")?.precomposed() - final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { static let minimizedHeight: CGFloat = 37.0 static let maximizedHeight: CGFloat = 166.0 + private var theme: PresentationTheme + private var strings: PresentationStrings + private let titleNode: TextNode private let subtitleNode: TextNode private let maximizedTitleNode: TextNode @@ -81,7 +59,10 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { } } - override init() { + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + self.titleNode = TextNode() self.titleNode.isLayerBacked = true self.subtitleNode = TextNode() @@ -93,7 +74,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.maximizedSubtitleNode.isLayerBacked = true self.closeButton = HighlightableButtonNode() - self.closeButton.setImage(closeButtonImage, for: []) + self.closeButton.setImage(PresentationResourcesRootController.navigationPlayerCloseButton(self.theme), for: []) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.closeButton.displaysAsynchronously = false @@ -106,18 +87,18 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.actionPauseNode.isLayerBacked = true self.actionPauseNode.displaysAsynchronously = false self.actionPauseNode.displayWithoutProcessing = true - self.actionPauseNode.image = pauseIcon + self.actionPauseNode.image = PresentationResourcesRootController.navigationPlayerPauseIcon(self.theme) self.actionPlayNode = ASImageNode() self.actionPlayNode.contentMode = .center self.actionPlayNode.isLayerBacked = true self.actionPlayNode.displaysAsynchronously = false self.actionPlayNode.displayWithoutProcessing = true - self.actionPlayNode.image = playIcon + self.actionPlayNode.image = PresentationResourcesRootController.navigationPlayerPlayIcon(self.theme) self.actionPlayNode.isHidden = true - self.maximizedLeftTimestampNode = MediaPlayerTimeTextNode(textColor: UIColor(0x686669)) - self.maximizedRightTimestampNode = MediaPlayerTimeTextNode(textColor: UIColor(0x686669)) + self.maximizedLeftTimestampNode = MediaPlayerTimeTextNode(textColor: self.theme.rootController.navigationBar.secondaryTextColor) + self.maximizedRightTimestampNode = MediaPlayerTimeTextNode(textColor: self.theme.rootController.navigationBar.secondaryTextColor) self.maximizedLeftTimestampNode.alignment = .right self.maximizedRightTimestampNode.mode = .reversed @@ -129,46 +110,46 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.maximizedActionPauseNode.isLayerBacked = true self.maximizedActionPauseNode.displaysAsynchronously = false self.maximizedActionPauseNode.displayWithoutProcessing = true - self.maximizedActionPauseNode.image = maximizedPauseIcon + self.maximizedActionPauseNode.image = PresentationResourcesRootController.navigationPlayerMaximizedPauseIcon(self.theme) self.maximizedActionPlayNode = ASImageNode() self.maximizedActionPlayNode.isLayerBacked = true self.maximizedActionPlayNode.displaysAsynchronously = false self.maximizedActionPlayNode.displayWithoutProcessing = true - self.maximizedActionPlayNode.image = maximizedPlayIcon + self.maximizedActionPlayNode.image = PresentationResourcesRootController.navigationPlayerMaximizedPlayIcon(self.theme) self.maximizedActionPlayNode.isHidden = true let maximizedActionButtonSize = CGSize(width: 66.0, height: 50.0) self.maximizedActionButton.frame = CGRect(origin: CGPoint(), size: maximizedActionButtonSize) - if let maximizedPauseIcon = maximizedPauseIcon { + if let maximizedPauseIcon = self.maximizedActionPauseNode.image { self.maximizedActionPauseNode.frame = CGRect(origin: CGPoint(x: floor((maximizedActionButtonSize.width - maximizedPauseIcon.size.width) / 2.0), y: floor((maximizedActionButtonSize.height - maximizedPauseIcon.size.height) / 2.0)), size: maximizedPauseIcon.size) } - if let maximizedPlayIcon = maximizedPlayIcon { + if let maximizedPlayIcon = self.maximizedActionPlayNode.image { self.maximizedActionPlayNode.frame = CGRect(origin: CGPoint(x: floor((maximizedActionButtonSize.width - maximizedPlayIcon.size.width) / 2.0) + 2.0, y: floor((maximizedActionButtonSize.height - maximizedPlayIcon.size.height) / 2.0)), size: maximizedPlayIcon.size) } self.maximizedPreviousButton = HighlightableButtonNode() - self.maximizedPreviousButton.setImage(maximizedPreviousIcon, for: []) + self.maximizedPreviousButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedPreviousIcon(self.theme), for: []) self.maximizedPreviousButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.maximizedPreviousButton.displaysAsynchronously = false self.maximizedNextButton = HighlightableButtonNode() - self.maximizedNextButton.setImage(maximizedNextIcon, for: []) + self.maximizedNextButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedNextIcon(self.theme), for: []) self.maximizedNextButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.maximizedNextButton.displaysAsynchronously = false self.maximizedShuffleButton = HighlightableButtonNode() - self.maximizedShuffleButton.setImage(maximizedShuffleIcon, for: []) + self.maximizedShuffleButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedShuffleIcon(self.theme), for: []) self.maximizedShuffleButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.maximizedShuffleButton.displaysAsynchronously = false self.maximizedRepeatButton = HighlightableButtonNode() - self.maximizedRepeatButton.setImage(maximizedRepeatIcon, for: []) + self.maximizedRepeatButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRepeatIcon(self.theme), for: []) self.maximizedRepeatButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.maximizedRepeatButton.displaysAsynchronously = false - self.scrubbingNode = MediaPlayerScrubbingNode(lineHeight: 2.0, lineCap: .square, scrubberHandle: false, backgroundColor: .clear, foregroundColor: UIColor(0x007ee5)) - self.maximizedScrubbingNode = MediaPlayerScrubbingNode(lineHeight: 3.0, lineCap: .round, scrubberHandle: true, backgroundColor: UIColor(0xcfcccf), foregroundColor: UIColor(0x007ee5)) + self.scrubbingNode = MediaPlayerScrubbingNode(lineHeight: 2.0, lineCap: .square, scrubberHandle: false, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor) + self.maximizedScrubbingNode = MediaPlayerScrubbingNode(lineHeight: 3.0, lineCap: .round, scrubberHandle: true, backgroundColor: self.theme.rootController.navigationBar.secondaryTextColor, foregroundColor: self.theme.rootController.navigationBar.accentTextColor) super.init() @@ -275,16 +256,21 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { let titleText: String = title ?? "Unknown Track" let subtitleText: String = performer ?? "Unknown Artist" - titleString = NSAttributedString(string: titleText, font: titleFont, textColor: titleColor) - subtitleString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: subtitleColor) + titleString = NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + subtitleString = NSAttributedString(string: subtitleText, font: subtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) - maximizedTitleString = NSAttributedString(string: titleText, font: maximizedTitleFont, textColor: titleColor) - maximizedSubtitleString = NSAttributedString(string: subtitleText, font: maximizedSubtitleFont, textColor: subtitleColor) + maximizedTitleString = NSAttributedString(string: titleText, font: maximizedTitleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + maximizedSubtitleString = NSAttributedString(string: subtitleText, font: maximizedSubtitleFont, textColor: self.theme.rootController.navigationBar.secondaryTextColor) case .voice: - let titleText: String = "Voice Message" - titleString = NSAttributedString(string: titleText, font: titleFont, textColor: titleColor) + let titleText: String = self.strings.Message_Audio + titleString = NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) - maximizedTitleString = NSAttributedString(string: titleText, font: maximizedTitleFont, textColor: titleColor) + maximizedTitleString = NSAttributedString(string: titleText, font: maximizedTitleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + case .video: + let titleText: String = self.strings.Message_VideoMessage + titleString = NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) + + maximizedTitleString = NSAttributedString(string: titleText, font: maximizedTitleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) } } let makeTitleLayout = TextNode.asyncLayout(self.titleNode) diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index d5ab88872a..ede52b5ae6 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -4,11 +4,11 @@ import Display import TelegramCore import Postbox -private let handleImage = generateStretchableFilledCircleImage(diameter: 7.0, color: UIColor(0xbab7ba)) - final class MediaNavigationAccessoryItemListNode: ASDisplayNode { static let minimizedPanelHeight: CGFloat = 31.0 + private var theme: PresentationTheme + var collapse: (() -> Void)? private var previousMaximizedHeight: CGFloat? @@ -57,7 +57,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { if let galleryMedia = galleryMedia { if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice { if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - let player = ManagedAudioPlaylistPlayer(audioSessionManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) + let player = ManagedAudioPlaylistPlayer(audioSessionManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, overlayMediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.overlayMediaManager, mediaManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) applicationContext.mediaManager.setPlaylistPlayer(player) player.control(.navigation(.next)) } @@ -65,7 +65,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { } } }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _ in }, navigateToMessage: { _ in }, clickThroughMessage: { }, toggleMessageSelection: { _ in }, sendMessage: { _ in }, sendSticker: { _ in }, sendGif: { _ in }, requestMessageActionCallback: { _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _ in }) + }, presentController: { _ in }, callPeer: { _ in }, longTap: { _ in }) let listNode = ChatHistoryListNode(account: account, peerId: updatedPlaylistPeerId, tagMask: .Music, messageId: nil, controllerInteraction: controllerInteraction, mode: .list) listNode.preloadPages = true @@ -102,28 +102,31 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { init(account: Account) { self.account = account + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + self.theme = presentationData.theme + self.topSeparatorNode = ASDisplayNode() self.topSeparatorNode.isLayerBacked = true - self.topSeparatorNode.backgroundColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) + self.topSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor self.bottomSeparatorNode = ASDisplayNode() self.bottomSeparatorNode.isLayerBacked = true - self.bottomSeparatorNode.backgroundColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) + self.bottomSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) + self.separatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor self.panelNode = HighlightTrackingButtonNode() - self.panelNode.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0) + self.panelNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor self.panelHandleNode = ASImageNode() self.panelHandleNode.displaysAsynchronously = false self.panelHandleNode.displayWithoutProcessing = true - self.panelHandleNode.image = handleImage + self.panelHandleNode.image = PresentationResourcesRootController.navigationPlayerHandleIcon(self.theme) self.contentNode = ASDisplayNode() - self.contentNode.backgroundColor = .white + self.contentNode.backgroundColor = self.theme.chatList.backgroundColor self.contentNode.clipsToBounds = true super.init() diff --git a/TelegramUI/MediaPlayer.swift b/TelegramUI/MediaPlayer.swift index e5f3aaa043..96e9cc87e7 100644 --- a/TelegramUI/MediaPlayer.swift +++ b/TelegramUI/MediaPlayer.swift @@ -33,12 +33,15 @@ private enum MediaPlayerState { enum MediaPlayerActionAtEnd { case loop case action(() -> Void) + case loopDisablingSound(() -> Void) case stop } private final class MediaPlayerContext { private let queue: Queue private let audioSessionManager: ManagedAudioSession + private let overlayMediaManager: OverlayMediaManager + private let postbox: Postbox private let resource: MediaResource private let streamable: Bool @@ -57,11 +60,12 @@ private final class MediaPlayerContext { fileprivate var actionAtEnd: MediaPlayerActionAtEnd = .stop - init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: ValuePromise, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, enableSound: Bool) { + init(queue: Queue, audioSessionManager: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, playerStatus: ValuePromise, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, enableSound: Bool) { assert(queue.isCurrent()) self.queue = queue self.audioSessionManager = audioSessionManager + self.overlayMediaManager = overlayMediaManager self.playerStatus = playerStatus self.postbox = postbox self.resource = resource @@ -244,6 +248,9 @@ private final class MediaPlayerContext { let controlTimebase: MediaPlayerControlTimebase if let _ = buffers.audioBuffer { + self.audioRenderer?.stop() + self.audioRenderer = nil + let renderer: MediaPlayerAudioRenderer if let currentRenderer = self.audioRenderer { renderer = currentRenderer @@ -252,7 +259,11 @@ private final class MediaPlayerContext { renderer = MediaPlayerAudioRenderer(audioSessionManager: self.audioSessionManager, audioPaused: { [weak self] in queue.async { if let strongSelf = self { - strongSelf.pause() + if case .loopDisablingSound = strongSelf.actionAtEnd { + strongSelf.continuePlayingWithoutSound() + } else { + strongSelf.pause() + } } } }) @@ -323,6 +334,37 @@ private final class MediaPlayerContext { } } + fileprivate func playOnceWithSound() { + assert(self.queue.isCurrent()) + + self.lastStatusUpdateTimestamp = nil + self.enableSound = true + self.seek(timestamp: 0.0, action: .play) + } + + fileprivate func continuePlayingWithoutSound() { + self.lastStatusUpdateTimestamp = nil + self.enableSound = true + + var loadedState: MediaPlayerLoadedState? + switch self.state { + case .empty: + break + case let .playing(currentLoadedState): + loadedState = currentLoadedState + case let .paused(currentLoadedState): + loadedState = currentLoadedState + case let .seeking(previousFrameSource, previousTimestamp, previousDisposable, _): + self.enableSound = false + } + + if let loadedState = loadedState { + self.enableSound = false + let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) + self.seek(timestamp: timestamp, action: .play) + } + } + fileprivate func pause() { assert(self.queue.isCurrent()) @@ -537,6 +579,10 @@ private final class MediaPlayerContext { case let .action(f): self.pause() f() + case let .loopDisablingSound(f): + self.enableSound = false + self.seek(timestamp: 0.0, action: .play) + f() } } } @@ -615,9 +661,9 @@ final class MediaPlayer { } } - init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, enableSound: Bool) { + init(audioSessionManager: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, postbox: Postbox, resource: MediaResource, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, enableSound: Bool) { self.queue.async { - let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, postbox: postbox, resource: resource, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, enableSound: enableSound) + let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, overlayMediaManager: overlayMediaManager, playerStatus: self.statusValue, postbox: postbox, resource: resource, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, enableSound: enableSound) self.contextRef = Unmanaged.passRetained(context) } } @@ -637,6 +683,22 @@ final class MediaPlayer { } } + func playOnceWithSound() { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.playOnceWithSound() + } + } + } + + func continuePlayingWithoutSound() { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.continuePlayingWithoutSound() + } + } + } + func pause() { self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { diff --git a/TelegramUI/MediaPlayerAudioRenderer.swift b/TelegramUI/MediaPlayerAudioRenderer.swift index b1aff80333..0d1b2c52d0 100644 --- a/TelegramUI/MediaPlayerAudioRenderer.swift +++ b/TelegramUI/MediaPlayerAudioRenderer.swift @@ -325,25 +325,25 @@ private final class AudioPlayerRendererContext { self.audioUnit = audioUnit } - self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, activate: { [weak self] in + self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, once: true, activate: { [weak self] in audioPlayerRendererQueue.async { if let strongSelf = self, !strongSelf.paused { strongSelf.audioSessionAcquired() } } - }, deactivate: { [weak self] in - return Signal { subscriber in - audioPlayerRendererQueue.async { - if let strongSelf = self { - strongSelf.audioPaused() - strongSelf.stop() - subscriber.putCompletion() + }, deactivate: { [weak self] in + return Signal { subscriber in + audioPlayerRendererQueue.async { + if let strongSelf = self { + strongSelf.audioPaused() + strongSelf.stop() + subscriber.putCompletion() + } } + + return EmptyDisposable } - - return EmptyDisposable - } - }, once: true)) + })) } private func audioSessionAcquired() { diff --git a/TelegramUI/MediaPlayerNode.swift b/TelegramUI/MediaPlayerNode.swift index c09079f374..a135484db1 100644 --- a/TelegramUI/MediaPlayerNode.swift +++ b/TelegramUI/MediaPlayerNode.swift @@ -83,8 +83,8 @@ final class MediaPlayerNode: ASDisplayNode { if let (timebase, requestFrames, rotationAngle) = self.state { if let videoLayer = self.videoLayer { videoQueue.async { - if videoLayer.controlTimebase !== timebase { - //self.videoNode.playerLayer.flush() + if videoLayer.controlTimebase !== timebase || videoLayer.status == .failed { + videoLayer.flush() videoLayer.controlTimebase = timebase } } diff --git a/TelegramUI/MediaResources.swift b/TelegramUI/MediaResources.swift index a115e7af81..afb13d2d58 100644 --- a/TelegramUI/MediaResources.swift +++ b/TelegramUI/MediaResources.swift @@ -65,7 +65,7 @@ public final class VideoLibraryMediaResource: TelegramMediaResource { } public required init(decoder: Decoder) { - self.localIdentifier = decoder.decodeStringForKey("i") + self.localIdentifier = decoder.decodeStringForKey("i", orElse: "") self.adjustments = decoder.decodeObjectForKey("a", decoder: { VideoMediaResourceAdjustments(decoder: $0) }) as? VideoMediaResourceAdjustments } @@ -127,8 +127,8 @@ public final class LocalFileVideoMediaResource: TelegramMediaResource { } public required init(decoder: Decoder) { - self.randomId = decoder.decodeInt64ForKey("i") - self.path = decoder.decodeStringForKey("p") + self.randomId = decoder.decodeInt64ForKey("i", orElse: 0) + self.path = decoder.decodeStringForKey("p", orElse: "") self.adjustments = decoder.decodeObjectForKey("a", decoder: { VideoMediaResourceAdjustments(decoder: $0) }) as? VideoMediaResourceAdjustments } @@ -183,7 +183,7 @@ public class PhotoLibraryMediaResource: TelegramMediaResource { } public required init(decoder: Decoder) { - self.localIdentifier = decoder.decodeStringForKey("i") + self.localIdentifier = decoder.decodeStringForKey("i", orElse: "") } public func encode(_ encoder: Encoder) { diff --git a/TelegramUI/MentionChatInputPanelItem.swift b/TelegramUI/MentionChatInputPanelItem.swift index e6adcfd9a8..c8992a3ea0 100644 --- a/TelegramUI/MentionChatInputPanelItem.swift +++ b/TelegramUI/MentionChatInputPanelItem.swift @@ -85,15 +85,15 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { self.textNode = TextNode() self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = UIColor(0xC9CDD1) + self.topSeparatorNode.backgroundColor = UIColor(rgb: 0xC9CDD1) self.topSeparatorNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xD6D6DA) + self.separatorNode.backgroundColor = UIColor(rgb: 0xD6D6DA) self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) + self.highlightedBackgroundNode.backgroundColor = UIColor(rgb: 0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true super.init(layerBacked: false, dynamicBounce: false) diff --git a/TelegramUI/NetworkStatusTitleView.swift b/TelegramUI/NetworkStatusTitleView.swift index bd2e94dea7..7ebe694bef 100644 --- a/TelegramUI/NetworkStatusTitleView.swift +++ b/TelegramUI/NetworkStatusTitleView.swift @@ -21,7 +21,7 @@ final class NetworkStatusTitleView: UIView { var title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false) { didSet { if self.title != oldValue { - self.titleNode.attributedText = NSAttributedString(string: title.text, font: Font.medium(17.0), textColor: .black) + self.titleNode.attributedText = NSAttributedString(string: title.text, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) if self.title.activity != oldValue.activity { if self.title.activity { self.activityIndicator.isHidden = false @@ -41,7 +41,21 @@ final class NetworkStatusTitleView: UIView { private var isPasscodeSet = false private var isManuallyLocked = false - override init(frame: CGRect) { + var theme: PresentationTheme { + didSet { + self.titleNode.attributedText = NSAttributedString(string: self.title.text, font: Font.medium(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor) + + if self.isPasscodeSet { + self.lockView.setIsLocked(self.isManuallyLocked, theme: self.theme, animated: false) + } else { + self.lockView.setIsLocked(false, theme: self.theme, animated: false) + } + } + } + + init(theme: PresentationTheme) { + self.theme = theme + self.titleNode = ASTextNode() self.titleNode.displaysAsynchronously = false self.titleNode.maximumNumberOfLines = 1 @@ -58,7 +72,7 @@ final class NetworkStatusTitleView: UIView { self.buttonView = HighlightTrackingButton() - super.init(frame: frame) + super.init(frame: CGRect()) self.addSubview(self.buttonView) self.addSubnode(self.titleNode) @@ -126,11 +140,11 @@ final class NetworkStatusTitleView: UIView { if isPasscodeSet { self.buttonView.isHidden = false self.lockView.isHidden = false - self.lockView.setIsLocked(isManuallyLocked, animated: !self.bounds.size.width.isZero) + self.lockView.setIsLocked(isManuallyLocked, theme: self.theme, animated: !self.bounds.size.width.isZero) } else { self.buttonView.isHidden = true self.lockView.isHidden = true - self.lockView.setIsLocked(false, animated: false) + self.lockView.setIsLocked(false, theme: self.theme, animated: false) } } diff --git a/TelegramUI/NetworkUsageStatsController.swift b/TelegramUI/NetworkUsageStatsController.swift index f92c5069f2..79ca9672ae 100644 --- a/TelegramUI/NetworkUsageStatsController.swift +++ b/TelegramUI/NetworkUsageStatsController.swift @@ -29,32 +29,32 @@ private enum NetworkUsageStatsSection: Int32 { } private enum NetworkUsageStatsEntry: ItemListNodeEntry { - case messagesHeader(String) - case messagesSent(String, String) - case messagesReceived(String, String) + case messagesHeader(PresentationTheme, String) + case messagesSent(PresentationTheme, String, String) + case messagesReceived(PresentationTheme, String, String) - case imageHeader(String) - case imageSent(String, String) - case imageReceived(String, String) + case imageHeader(PresentationTheme, String) + case imageSent(PresentationTheme, String, String) + case imageReceived(PresentationTheme, String, String) - case videoHeader(String) - case videoSent(String, String) - case videoReceived(String, String) + case videoHeader(PresentationTheme, String) + case videoSent(PresentationTheme, String, String) + case videoReceived(PresentationTheme, String, String) - case audioHeader(String) - case audioSent(String, String) - case audioReceived(String, String) + case audioHeader(PresentationTheme, String) + case audioSent(PresentationTheme, String, String) + case audioReceived(PresentationTheme, String, String) - case fileHeader(String) - case fileSent(String, String) - case fileReceived(String, String) + case fileHeader(PresentationTheme, String) + case fileSent(PresentationTheme, String, String) + case fileReceived(PresentationTheme, String, String) - case callHeader(String) - case callSent(String, String) - case callReceived(String, String) + case callHeader(PresentationTheme, String) + case callSent(PresentationTheme, String, String) + case callReceived(PresentationTheme, String, String) - case reset(NetworkUsageControllerSection, String) - case resetTimestamp(String) + case reset(PresentationTheme, NetworkUsageControllerSection, String) + case resetTimestamp(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -122,122 +122,122 @@ private enum NetworkUsageStatsEntry: ItemListNodeEntry { static func ==(lhs: NetworkUsageStatsEntry, rhs: NetworkUsageStatsEntry) -> Bool { switch lhs { - case let .messagesHeader(text): - if case .messagesHeader(text) = rhs { + case let .messagesHeader(lhsTheme, lhsText): + if case let .messagesHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .messagesSent(text, value): - if case .messagesSent(text, value) = rhs { + case let .messagesSent(lhsTheme, lhsText, lhsValue): + if case let .messagesSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .messagesReceived(text, value): - if case .messagesReceived(text, value) = rhs { + case let .messagesReceived(lhsTheme, lhsText, lhsValue): + if case let .messagesReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .imageHeader(text): - if case .imageHeader(text) = rhs { + case let .imageHeader(lhsTheme, lhsText): + if case let .imageHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .imageSent(text, value): - if case .imageSent(text, value) = rhs { + case let .imageSent(lhsTheme, lhsText, lhsValue): + if case let .imageSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .imageReceived(text, value): - if case .imageReceived(text, value) = rhs { + case let .imageReceived(lhsTheme, lhsText, lhsValue): + if case let .imageReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .videoHeader(text): - if case .videoHeader(text) = rhs { + case let .videoHeader(lhsTheme, lhsText): + if case let .videoHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .videoSent(text, value): - if case .videoSent(text, value) = rhs { + case let .videoSent(lhsTheme, lhsText, lhsValue): + if case let .videoSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .videoReceived(text, value): - if case .videoReceived(text, value) = rhs { + case let .videoReceived(lhsTheme, lhsText, lhsValue): + if case let .videoReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .audioHeader(text): - if case .audioHeader(text) = rhs { + case let .audioHeader(lhsTheme, lhsText): + if case let .audioHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .audioSent(text, value): - if case .audioSent(text, value) = rhs { + case let .audioSent(lhsTheme, lhsText, lhsValue): + if case let .audioSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .audioReceived(text, value): - if case .audioReceived(text, value) = rhs { + case let .audioReceived(lhsTheme, lhsText, lhsValue): + if case let .audioReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .fileHeader(text): - if case .fileHeader(text) = rhs { + case let .fileHeader(lhsTheme, lhsText): + if case let .fileHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .fileSent(text, value): - if case .fileSent(text, value) = rhs { + case let .fileSent(lhsTheme, lhsText, lhsValue): + if case let .fileSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .fileReceived(text, value): - if case .fileReceived(text, value) = rhs { + case let .fileReceived(lhsTheme, lhsText, lhsValue): + if case let .fileReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .callHeader(text): - if case .callHeader(text) = rhs { + case let .callHeader(lhsTheme, lhsText): + if case let .callHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .callSent(text, value): - if case .callSent(text, value) = rhs { + case let .callSent(lhsTheme, lhsText, lhsValue): + if case let .callSent(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .callReceived(text, value): - if case .callReceived(text, value) = rhs { + case let .callReceived(lhsTheme, lhsText, lhsValue): + if case let .callReceived(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .reset(section, text): - if case .reset(section, text) = rhs { + case let .reset(lhsTheme, lhsSection, lhsText): + if case let .reset(rhsTheme, rhsSection, rhsText) = rhs, lhsTheme === rhsTheme, lhsSection == rhsSection, lhsText == rhsText { return true } else { return false } - case let .resetTimestamp(text): - if case .resetTimestamp(text) = rhs { + case let .resetTimestamp(lhsTheme, lhsText): + if case let .resetTimestamp(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -251,122 +251,122 @@ private enum NetworkUsageStatsEntry: ItemListNodeEntry { func item(_ arguments: NetworkUsageStatsControllerArguments) -> ListViewItem { switch self { - case let .messagesHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .messagesSent(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .messagesReceived(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .imageHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .imageSent(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .imageReceived(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .videoHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .videoSent(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .videoReceived(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .audioHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .audioSent(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .audioReceived(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .fileHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .fileSent(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .fileReceived(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .callHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .callSent(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .callReceived(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) - case let .reset(section, text): - return ItemListActionItem(title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .messagesHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .messagesSent(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .messagesReceived(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .imageHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .imageSent(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .imageReceived(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .videoHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .videoSent(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .videoReceived(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .audioHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .audioSent(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .audioReceived(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .fileHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .fileSent(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .fileReceived(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .callHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .callSent(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .callReceived(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .none , action: nil) + case let .reset(theme, section, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.resetStatistics(section) }) - case let .resetTimestamp(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) + case let .resetTimestamp(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } -private func networkUsageStatsControllerEntries(section: NetworkUsageControllerSection, stats: NetworkUsageStats) -> [NetworkUsageStatsEntry] { +private func networkUsageStatsControllerEntries(presentationData: PresentationData, section: NetworkUsageControllerSection, stats: NetworkUsageStats) -> [NetworkUsageStatsEntry] { var entries: [NetworkUsageStatsEntry] = [] switch section { case .cellular: - entries.append(.messagesHeader("MESSAGES")) - entries.append(.messagesSent("Bytes Sent", dataSizeString(Int(stats.generic.cellular.outgoing)))) - entries.append(.messagesReceived("Bytes Received", dataSizeString(Int(stats.generic.cellular.incoming)))) + entries.append(.messagesHeader(presentationData.theme, "MESSAGES")) + entries.append(.messagesSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.generic.cellular.outgoing)))) + entries.append(.messagesReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.generic.cellular.incoming)))) - entries.append(.imageHeader("PHOTOS")) - entries.append(.imageSent("Bytes Sent", dataSizeString(Int(stats.image.cellular.outgoing)))) - entries.append(.imageReceived("Bytes Received", dataSizeString(Int(stats.image.cellular.incoming)))) + entries.append(.imageHeader(presentationData.theme, "PHOTOS")) + entries.append(.imageSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.image.cellular.outgoing)))) + entries.append(.imageReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.image.cellular.incoming)))) - entries.append(.videoHeader("VIDEOS")) - entries.append(.videoSent("Bytes Sent", dataSizeString(Int(stats.video.cellular.outgoing)))) - entries.append(.videoReceived("Bytes Received", dataSizeString(Int(stats.video.cellular.incoming)))) + entries.append(.videoHeader(presentationData.theme, "VIDEOS")) + entries.append(.videoSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.video.cellular.outgoing)))) + entries.append(.videoReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.video.cellular.incoming)))) - entries.append(.audioHeader("AUDIO")) - entries.append(.audioSent("Bytes Sent", dataSizeString(Int(stats.audio.cellular.outgoing)))) - entries.append(.audioReceived("Bytes Received", dataSizeString(Int(stats.audio.cellular.incoming)))) + entries.append(.audioHeader(presentationData.theme, "AUDIO")) + entries.append(.audioSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.audio.cellular.outgoing)))) + entries.append(.audioReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.audio.cellular.incoming)))) - entries.append(.fileHeader("DOCUMENTS")) - entries.append(.fileSent("Bytes Sent", dataSizeString(Int(stats.file.cellular.outgoing)))) - entries.append(.fileReceived("Bytes Received", dataSizeString(Int(stats.file.cellular.incoming)))) + entries.append(.fileHeader(presentationData.theme, "DOCUMENTS")) + entries.append(.fileSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.file.cellular.outgoing)))) + entries.append(.fileReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.file.cellular.incoming)))) - entries.append(.callHeader("CALLS")) - entries.append(.callSent("Bytes Sent", dataSizeString(0))) - entries.append(.callReceived("Bytes Received", dataSizeString(0))) + entries.append(.callHeader(presentationData.theme, "CALLS")) + entries.append(.callSent(presentationData.theme, "Bytes Sent", dataSizeString(0))) + entries.append(.callReceived(presentationData.theme, "Bytes Received", dataSizeString(0))) - entries.append(.reset(section, "Reset Statistics")) + entries.append(.reset(presentationData.theme, section, "Reset Statistics")) if stats.resetCellularTimestamp != 0 { let formatter = DateFormatter() formatter.dateFormat = "E, d MMM yyyy HH:mm" let dateStringPlain = formatter.string(from: Date(timeIntervalSince1970: Double(stats.resetCellularTimestamp))) - entries.append(.resetTimestamp("Cellular usage since \(dateStringPlain)")) + entries.append(.resetTimestamp(presentationData.theme, "Cellular usage since \(dateStringPlain)")) } case .wifi: - entries.append(.messagesHeader("MESSAGES")) - entries.append(.messagesSent("Bytes Sent", dataSizeString(Int(stats.generic.wifi.outgoing)))) - entries.append(.messagesReceived("Bytes Received", dataSizeString(Int(stats.generic.wifi.incoming)))) + entries.append(.messagesHeader(presentationData.theme, "MESSAGES")) + entries.append(.messagesSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.generic.wifi.outgoing)))) + entries.append(.messagesReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.generic.wifi.incoming)))) - entries.append(.imageHeader("PHOTOS")) - entries.append(.imageSent("Bytes Sent", dataSizeString(Int(stats.image.wifi.outgoing)))) - entries.append(.imageReceived("Bytes Received", dataSizeString(Int(stats.image.wifi.incoming)))) + entries.append(.imageHeader(presentationData.theme, "PHOTOS")) + entries.append(.imageSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.image.wifi.outgoing)))) + entries.append(.imageReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.image.wifi.incoming)))) - entries.append(.videoHeader("VIDEOS")) - entries.append(.videoSent("Bytes Sent", dataSizeString(Int(stats.video.wifi.outgoing)))) - entries.append(.videoReceived("Bytes Received", dataSizeString(Int(stats.video.wifi.incoming)))) + entries.append(.videoHeader(presentationData.theme, "VIDEOS")) + entries.append(.videoSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.video.wifi.outgoing)))) + entries.append(.videoReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.video.wifi.incoming)))) - entries.append(.audioHeader("AUDIO")) - entries.append(.audioSent("Bytes Sent", dataSizeString(Int(stats.audio.wifi.outgoing)))) - entries.append(.audioReceived("Bytes Received", dataSizeString(Int(stats.audio.wifi.incoming)))) + entries.append(.audioHeader(presentationData.theme, "AUDIO")) + entries.append(.audioSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.audio.wifi.outgoing)))) + entries.append(.audioReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.audio.wifi.incoming)))) - entries.append(.fileHeader("DOCUMENTS")) - entries.append(.fileSent("Bytes Sent", dataSizeString(Int(stats.file.wifi.outgoing)))) - entries.append(.fileReceived("Bytes Received", dataSizeString(Int(stats.file.wifi.incoming)))) + entries.append(.fileHeader(presentationData.theme, "DOCUMENTS")) + entries.append(.fileSent(presentationData.theme, "Bytes Sent", dataSizeString(Int(stats.file.wifi.outgoing)))) + entries.append(.fileReceived(presentationData.theme, "Bytes Received", dataSizeString(Int(stats.file.wifi.incoming)))) - entries.append(.callHeader("CALLS")) - entries.append(.callSent("Bytes Sent", dataSizeString(0))) - entries.append(.callReceived("Bytes Received", dataSizeString(0))) + entries.append(.callHeader(presentationData.theme, "CALLS")) + entries.append(.callSent(presentationData.theme, "Bytes Sent", dataSizeString(0))) + entries.append(.callReceived(presentationData.theme, "Bytes Received", dataSizeString(0))) - entries.append(.reset(section, "Reset Statistics")) + entries.append(.reset(presentationData.theme, section, "Reset Statistics")) if stats.resetWifiTimestamp != 0 { let formatter = DateFormatter() formatter.dateFormat = "E, d MMM yyyy HH:mm" let dateStringPlain = formatter.string(from: Date(timeIntervalSince1970: Double(stats.resetWifiTimestamp))) - entries.append(.resetTimestamp("Wifi usage since \(dateStringPlain)")) + entries.append(.resetTimestamp(presentationData.theme, "Wifi usage since \(dateStringPlain)")) } } @@ -405,18 +405,16 @@ func networkUsageStatsController(account: Account) -> ViewController { presentControllerImpl?(controller) }) - let signal = combineLatest(section.get(), stats.get()) |> deliverOnMainQueue - |> map { section, stats -> (ItemListControllerState, (ItemListNodeState, NetworkUsageStatsEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, section.get(), stats.get()) |> deliverOnMainQueue + |> map { presentationData, section, stats -> (ItemListControllerState, (ItemListNodeState, NetworkUsageStatsEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(title: .sectionControl(["Cellular", "Wifi"], 0), leftNavigationButton: nil, rightNavigationButton: nil, animateChanges: false) - let listState = ItemListNodeState(entries: networkUsageStatsControllerEntries(section: section, stats: stats), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .sectionControl(["Cellular", "Wifi"], 0), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: false) + let listState = ItemListNodeState(entries: networkUsageStatsControllerEntries(presentationData: presentationData, section: section, stats: stats), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) - + let controller = ItemListController(account: account, state: signal) controller.titleControlValueChanged = { [weak section] index in section?.set(index == 0 ? .cellular : .wifi) } diff --git a/TelegramUI/NotificationContainerController.swift b/TelegramUI/NotificationContainerController.swift index c53d1b9fc3..703d2d8500 100644 --- a/TelegramUI/NotificationContainerController.swift +++ b/TelegramUI/NotificationContainerController.swift @@ -8,7 +8,7 @@ public final class NotificationContainerController: ViewController { } public init() { - super.init(navigationBar: NavigationBar()) + super.init(navigationBarTheme: nil) self.statusBar.statusBarStyle = .Ignore } @@ -19,8 +19,6 @@ public final class NotificationContainerController: ViewController { override public func loadView() { super.loadView() - - self.navigationBar.removeFromSupernode() } override public func loadDisplayNode() { diff --git a/TelegramUI/NotificationSoundSelection.swift b/TelegramUI/NotificationSoundSelection.swift index 9da77f0b1e..6718796fd8 100644 --- a/TelegramUI/NotificationSoundSelection.swift +++ b/TelegramUI/NotificationSoundSelection.swift @@ -26,10 +26,10 @@ private struct NotificationSoundSelectionState: Equatable { } private enum NotificationSoundSelectionEntry: ItemListNodeEntry { - case modernHeader - case classicHeader - case none(section: NotificationSoundSelectionSection, selected: Bool) - case sound(section: NotificationSoundSelectionSection, index: Int32, sound: PeerMessageSound, selected: Bool) + case modernHeader(PresentationTheme, String) + case classicHeader(PresentationTheme, String) + case none(section: NotificationSoundSelectionSection, theme: PresentationTheme, text: String, selected: Bool) + case sound(section: NotificationSoundSelectionSection, index: Int32, theme: PresentationTheme, text: String, sound: PeerMessageSound, selected: Bool) var section: ItemListSectionId { switch self { @@ -37,9 +37,9 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { return NotificationSoundSelectionSection.modern.rawValue case .classicHeader: return NotificationSoundSelectionSection.classic.rawValue - case let .none(section, _): + case let .none(section, _, _, _): return section.rawValue - case let .sound(section, _, _, _): + case let .sound(section, _, _, _, _, _): return section.rawValue } } @@ -50,14 +50,14 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { return 0 case .classicHeader: return 1000 - case let .none(section, _): + case let .none(section, _, _, _): switch section { case .modern: return 1 case .classic: return 1001 } - case let .sound(section, index, _, _): + case let .sound(section, index, _, _, _, _): switch section { case .modern: return 2 + index @@ -69,20 +69,26 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { static func ==(lhs: NotificationSoundSelectionEntry, rhs: NotificationSoundSelectionEntry) -> Bool { switch lhs { - case .modernHeader, .classicHeader: - if lhs.stableId == rhs.stableId { + case let .modernHeader(lhsTheme, lhsText): + if case let .modernHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .none(section, selected): - if case .none(section, selected) = rhs { + case let .classicHeader(lhsTheme, lhsText): + if case let .classicHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .sound(section, index, name, selected): - if case .sound(section, index, name, selected) = rhs { + case let .none(lhsSection, lhsTheme, lhsText, lhsSelected): + if case let .none(rhsSection, rhsTheme, rhsText, rhsSelected) = rhs, lhsSection == rhsSection, lhsTheme === rhsTheme, lhsText == rhsText, lhsSelected == rhsSelected { + return true + } else { + return false + } + case let .sound(lhsSection, lhsIndex, lhsTheme, lhsText, lhsSound, lhsSelected): + if case let .sound(rhsSection, rhsIndex, rhsTheme, rhsText, rhsSound, rhsSelected) = rhs, lhsSection == rhsSection, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText, lhsSound == rhsSound, lhsSelected == rhsSelected { return true } else { return false @@ -96,36 +102,36 @@ private enum NotificationSoundSelectionEntry: ItemListNodeEntry { func item(_ arguments: NotificationSoundSelectionArguments) -> ListViewItem { switch self { - case .modernHeader: - return ItemListSectionHeaderItem(text: "ALERT TONES", sectionId: self.section) - case .classicHeader: - return ItemListSectionHeaderItem(text: "ALERT TONES", sectionId: self.section) - case let .none(_, selected): - return ItemListCheckboxItem(title: localizedPeerNotificationSoundString(.none), checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: { + case let.modernHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .classicHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .none(_, theme, text, selected): + return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: { arguments.selectSound(.none) }) - case let .sound(_, _, sound, selected): - return ItemListCheckboxItem(title: localizedPeerNotificationSoundString(sound), checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + case let .sound(_, _, theme, text, sound, selected): + return ItemListCheckboxItem(theme: theme, title: text, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectSound(sound) }) } } } -private func notificationsAndSoundsEntries(state: NotificationSoundSelectionState) -> [NotificationSoundSelectionEntry] { +private func notificationsAndSoundsEntries(presentationData: PresentationData, state: NotificationSoundSelectionState) -> [NotificationSoundSelectionEntry] { var entries: [NotificationSoundSelectionEntry] = [] - entries.append(.modernHeader) - entries.append(.none(section: .modern, selected: state.selectedSound == .none)) + entries.append(.modernHeader(presentationData.theme, presentationData.strings.Notifications_AlertTones)) + entries.append(.none(section: .modern, theme: presentationData.theme, text: "None", selected: state.selectedSound == .none)) for i in 0 ..< 12 { let sound: PeerMessageSound = .bundledModern(id: Int32(i)) - entries.append(.sound(section: .modern, index: Int32(i), sound: sound, selected: sound == state.selectedSound)) + entries.append(.sound(section: .modern, index: Int32(i), theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, sound: sound), sound: sound, selected: sound == state.selectedSound)) } - entries.append(.classicHeader) + entries.append(.classicHeader(presentationData.theme, presentationData.strings.Notifications_ClassicTones)) for i in 0 ..< 8 { let sound: PeerMessageSound = .bundledClassic(id: Int32(i)) - entries.append(.sound(section: .classic, index: Int32(i), sound: sound, selected: sound == state.selectedSound)) + entries.append(.sound(section: .classic, index: Int32(i), theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, sound: sound), sound: sound, selected: sound == state.selectedSound)) } return entries @@ -151,24 +157,24 @@ public func notificationSoundSelectionController(account: Account, isModal: Bool cancelImpl?() }) - let leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { - arguments.cancel() - }) - - let rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { - arguments.complete() - }) - - let signal = statePromise.get() - |> map { state -> (ItemListControllerState, (ItemListNodeState, NotificationSoundSelectionEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, NotificationSoundSelectionEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(title: .text("Text Tone"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton) - let listState = ItemListNodeState(entries: notificationsAndSoundsEntries(state: state), style: .blocks) + let leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { + arguments.cancel() + }) + + let rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { + arguments.complete() + }) + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Notifications_TextTone), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: notificationsAndSoundsEntries(presentationData: presentationData, state: state), style: .blocks) return (controllerState, (listState, arguments)) - } + } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) let result = Promise() diff --git a/TelegramUI/NotificationsAndSounds.swift b/TelegramUI/NotificationsAndSounds.swift index 81863ce63f..461dabf36c 100644 --- a/TelegramUI/NotificationsAndSounds.swift +++ b/TelegramUI/NotificationsAndSounds.swift @@ -48,25 +48,25 @@ private enum NotificationsAndSoundsSection: Int32 { } private enum NotificationsAndSoundsEntry: ItemListNodeEntry { - case messageHeader - case messageAlerts(Bool) - case messagePreviews(Bool) - case messageSound(PeerMessageSound) - case messageNotice + case messageHeader(PresentationTheme, String) + case messageAlerts(PresentationTheme, String, Bool) + case messagePreviews(PresentationTheme, String, Bool) + case messageSound(PresentationTheme, String, String, PeerMessageSound) + case messageNotice(PresentationTheme, String) - case groupHeader - case groupAlerts(Bool) - case groupPreviews(Bool) - case groupSound(PeerMessageSound) - case groupNotice + case groupHeader(PresentationTheme, String) + case groupAlerts(PresentationTheme, String, Bool) + case groupPreviews(PresentationTheme, String, Bool) + case groupSound(PresentationTheme, String, String, PeerMessageSound) + case groupNotice(PresentationTheme, String) - case inAppHeader - case inAppSounds(Bool) - case inAppVibrate(Bool) - case inAppPreviews(Bool) + case inAppHeader(PresentationTheme, String) + case inAppSounds(PresentationTheme, String, Bool) + case inAppVibrate(PresentationTheme, String, Bool) + case inAppPreviews(PresentationTheme, String, Bool) - case reset - case resetNotice + case reset(PresentationTheme, String) + case resetNotice(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -120,98 +120,98 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { static func ==(lhs: NotificationsAndSoundsEntry, rhs: NotificationsAndSoundsEntry) -> Bool { switch lhs { - case .messageHeader: - if case .messageHeader = rhs { + case let .messageHeader(lhsTheme, lhsText): + if case let .messageHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .messageAlerts(value): - if case .messageAlerts(value) = rhs { + case let .messageAlerts(lhsTheme, lhsText, lhsValue): + if case let .messageAlerts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .messagePreviews(value): - if case .messagePreviews(value) = rhs { + case let .messagePreviews(lhsTheme, lhsText, lhsValue): + if case let .messagePreviews(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .messageSound(value): - if case .messageSound(value) = rhs { + case let .messageSound(lhsTheme, lhsText, lhsValue, lhsSound): + if case let .messageSound(rhsTheme, rhsText, rhsValue, rhsSound) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsSound == rhsSound { return true } else { return false } - case .messageNotice: - if case .messageNotice = rhs { + case let .messageNotice(lhsTheme, lhsText): + if case let .messageNotice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case .groupHeader: - if case .groupHeader = rhs { + case let .groupHeader(lhsTheme, lhsText): + if case let .groupHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .groupAlerts(value): - if case .groupAlerts(value) = rhs { + case let .groupAlerts(lhsTheme, lhsText, lhsValue): + if case let .groupAlerts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .groupPreviews(value): - if case .groupPreviews(value) = rhs { + case let .groupPreviews(lhsTheme, lhsText, lhsValue): + if case let .groupPreviews(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .groupSound(value): - if case .groupSound(value) = rhs { + case let .groupSound(lhsTheme, lhsText, lhsValue, lhsSound): + if case let .groupSound(rhsTheme, rhsText, rhsValue, rhsSound) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsSound == rhsSound { return true } else { return false } - case .groupNotice: - if case .groupNotice = rhs { + case let .groupNotice(lhsTheme, lhsText): + if case let .groupNotice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case .inAppHeader: - if case .inAppHeader = rhs { + case let .inAppHeader(lhsTheme, lhsText): + if case let .inAppHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .inAppSounds(value): - if case .inAppSounds(value) = rhs { + case let .inAppSounds(lhsTheme, lhsText, lhsValue): + if case let .inAppSounds(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .inAppVibrate(value): - if case .inAppVibrate(value) = rhs { + case let .inAppVibrate(lhsTheme, lhsText, lhsValue): + if case let .inAppVibrate(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .inAppPreviews(value): - if case .inAppPreviews(value) = rhs { + case let .inAppPreviews(lhsTheme, lhsText, lhsValue): + if case let .inAppPreviews(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case .reset: - if case .reset = rhs { + case let .reset(lhsTheme, lhsText): + if case let .reset(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case .resetNotice: - if case .resetNotice = rhs { + case let .resetNotice(lhsTheme, lhsText): + if case let .resetNotice(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -225,19 +225,19 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { func item(_ arguments: NotificationsAndSoundsArguments) -> ListViewItem { switch self { - case .messageHeader: - return ItemListSectionHeaderItem(text: "MESSAGE NOTIFICATIONS", sectionId: self.section) - case let .messageAlerts(value): - return ItemListSwitchItem(title: "Alert", value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + case let .messageHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .messageAlerts(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateMessageAlerts(updatedValue) }) - case let .messagePreviews(value): - return ItemListSwitchItem(title: "Message Preview", value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + case let .messagePreviews(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateMessagePreviews(updatedValue) }) - case let .messageSound(value): - return ItemListDisclosureItem(title: "Sound", label: localizedPeerNotificationSoundString(value), sectionId: self.section, style: .blocks, action: { - let (controller, result) = notificationSoundSelectionController(account: arguments.account, isModal: true, currentSound: value) + case let .messageSound(theme, text, value, sound): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + let (controller, result) = notificationSoundSelectionController(account: arguments.account, isModal: true, currentSound: sound) arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) arguments.soundSelectionDisposable.set(result.start(next: { [weak arguments] value in if let value = value { @@ -245,21 +245,21 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { } })) }) - case .messageNotice: - return ItemListTextItem(text: .plain("You can set custom notifications for specific users on their info page."), sectionId: self.section) - case .groupHeader: - return ItemListSectionHeaderItem(text: "GROUP NOTIFICATIONS", sectionId: self.section) - case let .groupAlerts(value): - return ItemListSwitchItem(title: "Alert", value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + case let .messageNotice(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .groupHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .groupAlerts(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateGroupAlerts(updatedValue) }) - case let .groupPreviews(value): - return ItemListSwitchItem(title: "Message Preview", value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + case let .groupPreviews(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateGroupPreviews(updatedValue) }) - case let .groupSound(value): - return ItemListDisclosureItem(title: "Sound", label: localizedPeerNotificationSoundString(value), sectionId: self.section, style: .blocks, action: { - let (controller, result) = notificationSoundSelectionController(account: arguments.account, isModal: true, currentSound: value) + case let .groupSound(theme, text, value, sound): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { + let (controller, result) = notificationSoundSelectionController(account: arguments.account, isModal: true, currentSound: sound) arguments.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) arguments.soundSelectionDisposable.set(result.start(next: { [weak arguments] value in if let value = value { @@ -267,54 +267,54 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { } })) }) - case .groupNotice: - return ItemListTextItem(text: .plain("You can set custom notifications for specific groups on their info page."), sectionId: self.section) - case .inAppHeader: - return ItemListSectionHeaderItem(text: "IN-APP NOTIFICATIONS", sectionId: self.section) - case let .inAppSounds(value): - return ItemListSwitchItem(title: "In-App Sounds", value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + case let .groupNotice(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .inAppHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .inAppSounds(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateInAppSounds(updatedValue) }) - case let .inAppVibrate(value): - return ItemListSwitchItem(title: "In-App Vibrate", value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + case let .inAppVibrate(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateInAppVibration(updatedValue) }) - case let .inAppPreviews(value): - return ItemListSwitchItem(title: "In-App Preview", value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + case let .inAppPreviews(theme, text, value): + return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateInAppPreviews(updatedValue) }) - case .reset: - return ItemListActionItem(title: "Reset All Notifications", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .reset(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.resetNotifications() }) - case .resetNotice: - return ItemListTextItem(text: .plain("Undo all custom notification settings for all your contacts and groups."), sectionId: self.section) + case let .resetNotice(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } -private func notificationsAndSoundsEntries(globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings) -> [NotificationsAndSoundsEntry] { +private func notificationsAndSoundsEntries(globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, presentationData: PresentationData) -> [NotificationsAndSoundsEntry] { var entries: [NotificationsAndSoundsEntry] = [] - entries.append(.messageHeader) - entries.append(.messageAlerts(globalSettings.privateChats.enabled)) - entries.append(.messagePreviews(globalSettings.privateChats.displayPreviews)) - entries.append(.messageSound(globalSettings.privateChats.sound)) - entries.append(.messageNotice) + 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: globalSettings.privateChats.sound), globalSettings.privateChats.sound)) + entries.append(.messageNotice(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsHelp)) - entries.append(.groupHeader) - entries.append(.groupAlerts(globalSettings.groupChats.enabled)) - entries.append(.groupPreviews(globalSettings.groupChats.displayPreviews)) - entries.append(.groupSound(globalSettings.groupChats.sound)) - entries.append(.groupNotice) + 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: globalSettings.groupChats.sound), globalSettings.groupChats.sound)) + entries.append(.groupNotice(presentationData.theme, presentationData.strings.Notifications_GroupNotificationsHelp)) - entries.append(.inAppHeader) - entries.append(.inAppSounds(inAppSettings.playSounds)) - entries.append(.inAppVibrate(inAppSettings.vibrate)) - entries.append(.inAppPreviews(inAppSettings.displayPreviews)) + entries.append(.inAppHeader(presentationData.theme, presentationData.strings.Notifications_InAppNotifications)) + entries.append(.inAppSounds(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsSounds, inAppSettings.playSounds)) + entries.append(.inAppVibrate(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsVibrate, inAppSettings.vibrate)) + entries.append(.inAppPreviews(presentationData.theme, presentationData.strings.Notifications_InAppNotificationsPreview, inAppSettings.displayPreviews)) - entries.append(.reset) - entries.append(.resetNotice) + entries.append(.reset(presentationData.theme, presentationData.strings.Notifications_ResetAllNotifications)) + entries.append(.resetNotice(presentationData.theme, presentationData.strings.Notifications_ResetAllNotificationsHelp)) return entries } @@ -398,8 +398,8 @@ public func notificationsAndSoundsController(account: Account) -> ViewController let preferences = account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications, ApplicationSpecificPreferencesKeys.inAppNotificationSettings]) - let signal = preferences - |> map { view -> (ItemListControllerState, (ItemListNodeState, NotificationsAndSoundsEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, preferences) + |> map { presentationData, view -> (ItemListControllerState, (ItemListNodeState, NotificationsAndSoundsEntry.ItemGenerationArguments)) in let viewSettings: GlobalNotificationSettingsSet if let settings = view.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { @@ -415,13 +415,13 @@ public func notificationsAndSoundsController(account: Account) -> ViewController inAppSettings = InAppNotificationSettings.defaultSettings } - let controllerState = ItemListControllerState(title: .text("Notifications"), leftNavigationButton: nil, rightNavigationButton: nil) - let listState = ItemListNodeState(entries: notificationsAndSoundsEntries(globalSettings: viewSettings, inAppSettings: inAppSettings), style: .blocks) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Notifications"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back")) + let listState = ItemListNodeState(entries: notificationsAndSoundsEntries(globalSettings: viewSettings, inAppSettings: inAppSettings, presentationData: presentationData), style: .blocks) return (controllerState, (listState, arguments)) } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, a in controller?.present(c, in: .window, with: a) } diff --git a/TelegramUI/NumberPluralizationForm.h b/TelegramUI/NumberPluralizationForm.h new file mode 100644 index 0000000000..8d72b8504f --- /dev/null +++ b/TelegramUI/NumberPluralizationForm.h @@ -0,0 +1,12 @@ +#import + +typedef NS_ENUM(int32_t, NumberPluralizationForm) { + NumberPluralizationFormZero, + NumberPluralizationFormOne, + NumberPluralizationFormTwo, + NumberPluralizationFormFew, + NumberPluralizationFormMany, + NumberPluralizationFormOther +}; + +NumberPluralizationForm numberPluralizationForm(unsigned int lc, int n); diff --git a/TelegramUI/NumberPluralizationForm.m b/TelegramUI/NumberPluralizationForm.m new file mode 100644 index 0000000000..47f33f9224 --- /dev/null +++ b/TelegramUI/NumberPluralizationForm.m @@ -0,0 +1,346 @@ +#import "NumberPluralizationForm.h" + +NumberPluralizationForm numberPluralizationForm(unsigned int lc, int n) { + switch (lc) { + + // set1 + case 0x6c74: // lt + if (((n % 10) == 1) && (((n % 100) < 11 || (n % 100) > 19))) // n mod 10 is 1 and n mod 100 not in 11..19 + return NumberPluralizationFormOne; + if ((((n % 10) >= 2 && (n % 10) <= 9)) && (((n % 100) < 11 || (n % 100) > 19))) // n mod 10 in 2..9 and n mod 100 not in 11..19 + return NumberPluralizationFormFew; + break; + + // set2 + case 0x6c76: // lv + if (n == 0) // n is 0 + return NumberPluralizationFormZero; + if (((n % 10) == 1) && ((n % 100) != 11)) // n mod 10 is 1 and n mod 100 is not 11 + return NumberPluralizationFormOne; + break; + + // set3 + case 0x6379: // cy + if (n == 2) // n is 2 + return NumberPluralizationFormTwo; + if (n == 3) // n is 3 + return NumberPluralizationFormFew; + if (n == 0) // n is 0 + return NumberPluralizationFormZero; + if (n == 1) // n is 1 + return NumberPluralizationFormOne; + if (n == 6) // n is 6 + return NumberPluralizationFormMany; + break; + + // set4 + case 0x6265: // be + case 0x6273: // bs + case 0x6872: // hr + case 0x7275: // ru + case 0x7368: // sh + case 0x7372: // sr + case 0x756b: // uk + if (((n % 10) == 1) && ((n % 100) != 11)) // n mod 10 is 1 and n mod 100 is not 11 + return NumberPluralizationFormOne; + if ((((n % 10) >= 2 && (n % 10) <= 4)) && (((n % 100) < 12 || (n % 100) > 14))) // n mod 10 in 2..4 and n mod 100 not in 12..14 + return NumberPluralizationFormFew; + if (((n % 10) == 0) || (((n % 10) >= 5 && (n % 10) <= 9)) || (((n % 100) >= 11 && (n % 100) <= 14))) // n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14 + return NumberPluralizationFormMany; + break; + + // set5 + case 0x6b7368: // ksh + if (n == 0) // n is 0 + return NumberPluralizationFormZero; + if (n == 1) // n is 1 + return NumberPluralizationFormOne; + break; + + // set6 + case 0x736869: // shi + if ((n >= 2 && n <= 10)) // n in 2..10 + return NumberPluralizationFormFew; + if ((n >= 0 && n <= 1)) // n within 0..1 + return NumberPluralizationFormOne; + break; + + // set7 + case 0x6865: // he + if (n == 2) // n is 2 + return NumberPluralizationFormTwo; + if (n == 1) // n is 1 + return NumberPluralizationFormOne; + if ((n != 0) && ((n % 10) == 0)) // n is not 0 AND n mod 10 is 0 + return NumberPluralizationFormMany; + break; + + // set8 + case 0x6373: // cs + case 0x736b: // sk + if (n == 1) // n is 1 + return NumberPluralizationFormOne; + if ((n >= 2 && n <= 4)) // n in 2..4 + return NumberPluralizationFormFew; + break; + + // set9 + case 0x6272: // br + if ((n != 0) && ((n % 1000000) == 0)) // n is not 0 and n mod 1000000 is 0 + return NumberPluralizationFormMany; + if (((n % 10) == 1) && (((n % 100) != 11) && ((n % 100) != 71) && ((n % 100) != 91))) // n mod 10 is 1 and n mod 100 not in 11,71,91 + return NumberPluralizationFormOne; + if (((n % 10) == 2) && (((n % 100) != 12) && ((n % 100) != 72) && ((n % 100) != 92))) // n mod 10 is 2 and n mod 100 not in 12,72,92 + return NumberPluralizationFormTwo; + if ((((n % 10) >= 3 && (n % 10) <= 4) || ((n % 10) == 9)) && (((n % 100) < 10 || (n % 100) > 19) && ((n % 100) < 70 || (n % 100) > 79) && ((n % 100) < 90 || (n % 100) > 99))) // n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99 + return NumberPluralizationFormFew; + break; + + // set10 + case 0x736c: // sl + if ((n % 100) == 2) // n mod 100 is 2 + return NumberPluralizationFormTwo; + if ((n % 100) == 1) // n mod 100 is 1 + return NumberPluralizationFormOne; + if (((n % 100) >= 3 && (n % 100) <= 4)) // n mod 100 in 3..4 + return NumberPluralizationFormFew; + break; + + // set11 + case 0x6c6167: // lag + if (n == 0) // n is 0 + return NumberPluralizationFormZero; + if (((n >= 0 && n <= 2)) && (n != 0) && (n != 2)) // n within 0..2 and n is not 0 and n is not 2 + return NumberPluralizationFormOne; + break; + + // set12 + case 0x706c: // pl + if (n == 1) // n is 1 + return NumberPluralizationFormOne; + if ((((n % 10) >= 2 && (n % 10) <= 4)) && (((n % 100) < 12 || (n % 100) > 14))) // n mod 10 in 2..4 and n mod 100 not in 12..14 + return NumberPluralizationFormFew; + if (((n != 1) && (((n % 10) >= 0 && (n % 10) <= 1))) || (((n % 10) >= 5 && (n % 10) <= 9)) || (((n % 100) >= 12 && (n % 100) <= 14))) // n is not 1 and n mod 10 in 0..1 or n mod 10 in 5..9 or n mod 100 in 12..14 + return NumberPluralizationFormMany; + break; + + // set13 + case 0x6764: // gd + if ((n == 2) || (n == 12)) // n in 2,12 + return NumberPluralizationFormTwo; + if ((n == 1) || (n == 11)) // n in 1,11 + return NumberPluralizationFormOne; + if ((n >= 3 && n <= 10) || (n >= 13 && n <= 19)) // n in 3..10,13..19 + return NumberPluralizationFormFew; + break; + + // set14 + case 0x6776: // gv + if ((((n % 10) >= 1 && (n % 10) <= 2)) || ((n % 20) == 0)) // n mod 10 in 1..2 or n mod 20 is 0 + return NumberPluralizationFormOne; + break; + + // set15 + case 0x6d6b: // mk + if (((n % 10) == 1) && (n != 11)) // n mod 10 is 1 and n is not 11 + return NumberPluralizationFormOne; + break; + + // set16 + case 0x6d74: // mt + if (n == 1) // n is 1 + return NumberPluralizationFormOne; + if (((n % 100) >= 11 && (n % 100) <= 19)) // n mod 100 in 11..19 + return NumberPluralizationFormMany; + if ((n == 0) || (((n % 100) >= 2 && (n % 100) <= 10))) // n is 0 or n mod 100 in 2..10 + return NumberPluralizationFormFew; + break; + + // set17 + case 0x6d6f: // mo + case 0x726f: // ro + if (n == 1) // n is 1 + return NumberPluralizationFormOne; + if ((n == 0) || ((n != 1) && (((n % 100) >= 1 && (n % 100) <= 19)))) // n is 0 OR n is not 1 AND n mod 100 in 1..19 + return NumberPluralizationFormFew; + break; + + // set18 + case 0x6761: // ga + if (n == 2) // n is 2 + return NumberPluralizationFormTwo; + if (n == 1) // n is 1 + return NumberPluralizationFormOne; + if ((n >= 3 && n <= 6)) // n in 3..6 + return NumberPluralizationFormFew; + if ((n >= 7 && n <= 10)) // n in 7..10 + return NumberPluralizationFormMany; + break; + + // set19 + case 0x6666: // ff + case 0x6672: // fr + case 0x6b6162: // kab + if (((n >= 0 && n <= 2)) && (n != 2)) // n within 0..2 and n is not 2 + return NumberPluralizationFormOne; + break; + + // set20 + case 0x6975: // iu + case 0x6b77: // kw + case 0x7365: // se + case 0x6e6171: // naq + case 0x736d61: // sma + case 0x736d69: // smi + case 0x736d6a: // smj + case 0x736d6e: // smn + case 0x736d73: // sms + if (n == 2) // n is 2 + return NumberPluralizationFormTwo; + if (n == 1) // n is 1 + return NumberPluralizationFormOne; + break; + + // set21 + case 0x616b: // ak + case 0x616d: // am + case 0x6268: // bh + case 0x6869: // hi + case 0x6c6e: // ln + case 0x6d67: // mg + case 0x7469: // ti + case 0x746c: // tl + case 0x7761: // wa + case 0x66696c: // fil + case 0x677577: // guw + case 0x6e736f: // nso + if ((n >= 0 && n <= 1)) // n in 0..1 + return NumberPluralizationFormOne; + break; + + // set22 + case 0x747a6d: // tzm + if (((n >= 0 && n <= 1)) || ((n >= 11 && n <= 99))) // n in 0..1 or n in 11..99 + return NumberPluralizationFormOne; + break; + + // set23 + case 0x6166: // af + case 0x6267: // bg + case 0x626e: // bn + case 0x6361: // ca + case 0x6461: // da + case 0x6465: // de + case 0x6476: // dv + case 0x6565: // ee + case 0x656c: // el + case 0x656e: // en + case 0x656f: // eo + case 0x6573: // es + case 0x6574: // et + case 0x6575: // eu + case 0x6669: // fi + case 0x666f: // fo + case 0x6679: // fy + case 0x676c: // gl + case 0x6775: // gu + case 0x6861: // ha + case 0x6973: // is + case 0x6974: // it + case 0x6b6b: // kk + case 0x6b6c: // kl + case 0x6b73: // ks + case 0x6b75: // ku + case 0x6b79: // ky + case 0x6c62: // lb + case 0x6c67: // lg + case 0x6d6c: // ml + case 0x6d6e: // mn + case 0x6d72: // mr + case 0x6e62: // nb + case 0x6e64: // nd + case 0x6e65: // ne + case 0x6e6c: // nl + case 0x6e6e: // nn + case 0x6e6f: // no + case 0x6e72: // nr + case 0x6e79: // ny + case 0x6f6d: // om + case 0x6f72: // or + case 0x6f73: // os + case 0x7061: // pa + case 0x7073: // ps + case 0x7074: // pt + case 0x726d: // rm + case 0x736e: // sn + case 0x736f: // so + case 0x7371: // sq + case 0x7373: // ss + case 0x7374: // st + case 0x7376: // sv + case 0x7377: // sw + case 0x7461: // ta + case 0x7465: // te + case 0x746b: // tk + case 0x746e: // tn + case 0x7473: // ts + case 0x7572: // ur + case 0x7665: // ve + case 0x766f: // vo + case 0x7868: // xh + case 0x7a75: // zu + case 0x617361: // asa + case 0x617374: // ast + case 0x62656d: // bem + case 0x62657a: // bez + case 0x627278: // brx + case 0x636767: // cgg + case 0x636872: // chr + case 0x636b62: // ckb + case 0x667572: // fur + case 0x677377: // gsw + case 0x686177: // haw + case 0x6a676f: // jgo + case 0x6a6d63: // jmc + case 0x6b616a: // kaj + case 0x6b6367: // kcg + case 0x6b6b6a: // kkj + case 0x6b7362: // ksb + case 0x6d6173: // mas + case 0x6d676f: // mgo + case 0x6e6168: // nah + case 0x6e6e68: // nnh + case 0x6e796e: // nyn + case 0x706170: // pap + case 0x726f66: // rof + case 0x72776b: // rwk + case 0x736171: // saq + case 0x736568: // seh + case 0x737379: // ssy + case 0x737972: // syr + case 0x74656f: // teo + case 0x746967: // tig + case 0x76756e: // vun + case 0x776165: // wae + case 0x786f67: // xog + if (n == 1) // n is 1 + return NumberPluralizationFormOne; + break; + + // set24 + case 0x6172: // ar + if (n == 2) // n is 2 + return NumberPluralizationFormTwo; + if (n == 1) // n is 1 + return NumberPluralizationFormOne; + if (n == 0) // n is 0 + return NumberPluralizationFormZero; + if (((n % 100) >= 3 && (n % 100) <= 10)) // n mod 100 in 3..10 + return NumberPluralizationFormFew; + if (((n % 100) >= 11 && (n % 100) <= 99)) // n mod 100 in 11..99 + return NumberPluralizationFormMany; + break; + } + + return NumberPluralizationFormOther; +} diff --git a/TelegramUI/NumericFormat.swift b/TelegramUI/NumericFormat.swift index 7c89e91f0a..cd6f1a3cdb 100644 --- a/TelegramUI/NumericFormat.swift +++ b/TelegramUI/NumericFormat.swift @@ -20,3 +20,17 @@ public func compactNumericCountString(_ count: Int) -> String { } } +func timeIntervalString(strings: PresentationStrings, value: Int32) -> String { + if value < 60 { + return strings.MessageTimer_Seconds(value) + } else if value < 60 * 60 { + return strings.MessageTimer_Minutes(value / 60) + } else if value < 60 * 60 * 24 { + return strings.MessageTimer_Hours(value / (60 * 60)) + } else if value < 60 * 60 * 24 * 7 { + return strings.MessageTimer_Days(value / (60 * 60 * 24)) + } else { + return strings.MessageTimer_Weeks(value / (60 * 60 * 24 * 7)) + } +} + diff --git a/TelegramUI/OngoingCallContext.swift b/TelegramUI/OngoingCallContext.swift new file mode 100644 index 0000000000..55fef3ed95 --- /dev/null +++ b/TelegramUI/OngoingCallContext.swift @@ -0,0 +1,74 @@ +import Foundation +import SwiftSignalKit +import TelegramCore +import Postbox + +private func callConnectionDescription(_ connection: CallSessionConnection) -> OngoingCallConnectionDescription { + return OngoingCallConnectionDescription(connectionId: connection.id, ip: connection.ip, ipv6: connection.ipv6, port: connection.port, peerTag: connection.peerTag) +} + +final class OngoingCallContext { + let internalId: CallSessionInternalId + + private let queue = Queue() + private let callSessionManager: CallSessionManager + + private var contextRef: Unmanaged? + + private let contextState = Promise(nil) + + private let audioSessionDisposable = MetaDisposable() + + init(callSessionManager: CallSessionManager, internalId: CallSessionInternalId) { + self.internalId = internalId + self.callSessionManager = callSessionManager + + self.queue.async { + let context = OngoingCallThreadLocalContext() + self.contextRef = Unmanaged.passRetained(context) + context.stateChanged = { [weak self] state in + self?.contextState.set(.single(state)) + } + } + } + + deinit { + let contextRef = self.contextRef + self.queue.async { + contextRef?.release() + } + + self.audioSessionDisposable.dispose() + } + + private func withContext(_ f: @escaping (OngoingCallThreadLocalContext) -> Void) { + self.queue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + f(context) + } + } + } + + func start(key: Data, isOutgoing: Bool, connections: CallSessionConnectionSet, audioSessionActive: Signal) { + self.audioSessionDisposable.set((audioSessionActive |> filter { $0 } |> take(1)).start(next: { [weak self] _ in + if let strongSelf = self { + strongSelf.withContext { context in + context.start(withKey: key, isOutgoing: isOutgoing, primaryConnection: callConnectionDescription(connections.primary), alternativeConnections: connections.alternatives.map(callConnectionDescription)) + } + } + })) + } + + func stop() { + self.withContext { context in + context.stop() + } + } + + func setIsMuted(_ value: Bool) { + self.withContext { context in + context.setIsMuted(value) + } + } +} diff --git a/TelegramUI/OngoingCallThreadLocalContext.h b/TelegramUI/OngoingCallThreadLocalContext.h new file mode 100644 index 0000000000..61e4570211 --- /dev/null +++ b/TelegramUI/OngoingCallThreadLocalContext.h @@ -0,0 +1,36 @@ +#ifndef OngoingCallContext_h +#define OngoingCallContext_h + +#import + +@interface OngoingCallConnectionDescription : NSObject + +@property (nonatomic, readonly) int64_t connectionId; +@property (nonatomic, strong, readonly) NSString * _Nonnull ip; +@property (nonatomic, strong, readonly) NSString * _Nonnull ipv6; +@property (nonatomic, readonly) int32_t port; +@property (nonatomic, strong, readonly) NSData * _Nonnull peerTag; + +- (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId ip:(NSString * _Nonnull)ip ipv6:(NSString * _Nonnull)ipv6 port:(int32_t)port peerTag:(NSData * _Nonnull)peerTag; + +@end + +typedef NS_ENUM(int32_t, OngoingCallState) { + OngoingCallStateInitializing, + OngoingCallStateConnected, + OngoingCallStateFailed +}; + +@interface OngoingCallThreadLocalContext : NSObject + +@property (nonatomic, copy) void (^stateChanged)(OngoingCallState); + +- (instancetype _Nonnull)init; +- (void)startWithKey:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections; +- (void)stop; + +- (void)setIsMuted:(bool)isMuted; + +@end + +#endif diff --git a/TelegramUI/OngoingCallThreadLocalContext.mm b/TelegramUI/OngoingCallThreadLocalContext.mm new file mode 100644 index 0000000000..293a3bfde7 --- /dev/null +++ b/TelegramUI/OngoingCallThreadLocalContext.mm @@ -0,0 +1,185 @@ +#import "OngoingCallThreadLocalContext.h" + +#import "../submodules/libtgvoip/VoIPController.h" + +#import + +static void TGCallAesIgeEncrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { + MTAesEncryptRaw(inBytes, outBytes, length, key, iv); +} + +static void TGCallAesIgeDecrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { + MTAesDecryptRaw(inBytes, outBytes, length, key, iv); +} + +static void TGCallSha1(uint8_t *msg, size_t length, uint8_t *output) { + MTRawSha1(msg, length, output); +} + +static void TGCallSha256(uint8_t *msg, size_t length, uint8_t *output) { + MTRawSha256(msg, length, output); +} + +static void TGCallRandomBytes(uint8_t *buffer, size_t length) { + arc4random_buf(buffer, length); +} + +static void TGCallLoggingFunction(const char *msg) { + NSLog(@"%s", msg); +} + +@implementation OngoingCallConnectionDescription + +- (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId ip:(NSString * _Nonnull)ip ipv6:(NSString * _Nonnull)ipv6 port:(int32_t)port peerTag:(NSData * _Nonnull)peerTag { + self = [super init]; + if (self != nil) { + _connectionId = connectionId; + _ip = ip; + _ipv6 = ipv6; + _port = port; + _peerTag = peerTag; + } + return self; +} + +@end + +@interface OngoingCallThreadLocalContext () { + NSTimeInterval _callReceiveTimeout; + NSTimeInterval _callRingTimeout; + NSTimeInterval _callConnectTimeout; + NSTimeInterval _callPacketTimeout; + int32_t _dataSavingMode; + bool _allowP2P; + + tgvoip::VoIPController *_controller; + + OngoingCallState _state; +} + +- (void)controllerStateChanged:(int)state; + +@end + +static void controllerStateCallback(tgvoip::VoIPController *controller, int state) { + OngoingCallThreadLocalContext *context = (__bridge OngoingCallThreadLocalContext *)controller->implData; + [context controllerStateChanged:state]; +} + +@implementation OngoingCallThreadLocalContext + +- (instancetype)init { + self = [super init]; + if (self != nil) { + _callReceiveTimeout = 20.0; + _callRingTimeout = 90.0; + _callConnectTimeout = 30.0; + _callPacketTimeout = 10.0; + _dataSavingMode = 0; + _allowP2P = true; + + _controller = new tgvoip::VoIPController(); + _controller->implData = (__bridge void *)self; + _controller->SetStateCallback(&controllerStateCallback); + + tgvoip::VoIPController::crypto.sha1 = &TGCallSha1; + tgvoip::VoIPController::crypto.sha256 = &TGCallSha256; + tgvoip::VoIPController::crypto.rand_bytes = &TGCallRandomBytes; + tgvoip::VoIPController::crypto.aes_ige_encrypt = &TGCallAesIgeEncrypt; + tgvoip::VoIPController::crypto.aes_ige_decrypt = &TGCallAesIgeDecrypt; + + _state = OngoingCallStateInitializing; + } + return self; +} + +- (void)startWithKey:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections { + std::vector endpoints; + NSArray *connections = [@[primaryConnection] arrayByAddingObjectsFromArray:alternativeConnections]; + for (OngoingCallConnectionDescription *connection in connections) { + struct in_addr addrIpV4; + if (!inet_aton(connection.ip.UTF8String, &addrIpV4)) { + NSLog(@"CallSession: invalid ipv4 address"); + } + + struct in6_addr addrIpV6; + if (!inet_pton(AF_INET6, connection.ipv6.UTF8String, &addrIpV6)) { + NSLog(@"CallSession: invalid ipv6 address"); + } + + tgvoip::IPv4Address address(std::string(connection.ip.UTF8String)); + tgvoip::IPv6Address addressv6(std::string(connection.ipv6.UTF8String)); + unsigned char peerTag[16]; + [connection.peerTag getBytes:peerTag length:16]; + endpoints.push_back(tgvoip::Endpoint(connection.connectionId, (uint16_t)connection.port, address, addressv6, EP_TYPE_UDP_RELAY, peerTag)); + } + + voip_config_t config; + config.init_timeout = _callConnectTimeout; + config.recv_timeout = _callPacketTimeout; + config.data_saving = _dataSavingMode; + memset(config.logFilePath, 0, sizeof(config.logFilePath)); + config.enableAEC = false; + config.enableNS = true; + config.enableAGC = true; + memset(config.statsDumpFilePath, 0, sizeof(config.statsDumpFilePath)); + + _controller->SetConfig(&config); + + _controller->SetEncryptionKey((char *)key.bytes, isOutgoing); + _controller->SetRemoteEndpoints(endpoints, _allowP2P); + _controller->Start(); + + _controller->Connect(); +} + +- (void)stop { + if (_controller) { + char *buffer = (char *)malloc(_controller->GetDebugLogLength()); + _controller->GetDebugLog(buffer); + NSString *debugLog = [[NSString alloc] initWithUTF8String:buffer]; + + voip_stats_t stats; + _controller->GetStats(&stats); + delete _controller; + _controller = NULL; + } + + /*MTNetworkUsageManager *usageManager = [[MTNetworkUsageManager alloc] initWithInfo:[[TGTelegramNetworking instance] mediaUsageInfoForType:TGNetworkMediaTypeTagCall]]; + [usageManager addIncomingBytes:stats.bytesRecvdMobile interface:MTNetworkUsageManagerInterfaceWWAN]; + [usageManager addIncomingBytes:stats.bytesRecvdWifi interface:MTNetworkUsageManagerInterfaceOther]; + + [usageManager addOutgoingBytes:stats.bytesSentMobile interface:MTNetworkUsageManagerInterfaceWWAN]; + [usageManager addOutgoingBytes:stats.bytesSentWifi interface:MTNetworkUsageManagerInterfaceOther];*/ + + //if (sendDebugLog && self.peerId != 0 && self.accessHash != 0) + // [[TGCallSignals saveCallDebug:self.peerId accessHash:self.accessHash data:debugLog] startWithNext:nil]; +} + +- (void)controllerStateChanged:(int)state { + OngoingCallState callState = OngoingCallStateInitializing; + switch (state) { + case STATE_ESTABLISHED: + callState = OngoingCallStateConnected; + break; + case STATE_FAILED: + callState = OngoingCallStateFailed; + break; + default: + break; + } + + if (callState != _state) { + _state = callState; + + if (_stateChanged) { + _stateChanged(callState); + } + } +} + +- (void)setIsMuted:(bool)isMuted { + _controller->SetMicMute(isMuted); +} + +@end diff --git a/TelegramUI/OverlayMediaController.swift b/TelegramUI/OverlayMediaController.swift new file mode 100644 index 0000000000..6f1464fd81 --- /dev/null +++ b/TelegramUI/OverlayMediaController.swift @@ -0,0 +1,40 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox + +public final class OverlayMediaController: ViewController { + private var controllerNode: OverlayMediaControllerNode { + return self.displayNode as! OverlayMediaControllerNode + } + + public init() { + super.init(navigationBarTheme: nil) + + self.statusBar.statusBarStyle = .Ignore + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = OverlayMediaControllerNode() + self.displayNodeDidLoad() + } + + func addVideoContext(mediaManager: MediaManager, postbox: Postbox, id: ManagedMediaId, resource: MediaResource, priority: Int32) { + self.controllerNode.addVideoContext(mediaManager: mediaManager, postbox: postbox, id: id, resource: resource, priority: priority) + } + + func removeVideoContext(id: ManagedMediaId) { + self.controllerNode.removeVideoContext(id: id) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/TelegramUI/OverlayMediaControllerNode.swift b/TelegramUI/OverlayMediaControllerNode.swift new file mode 100644 index 0000000000..eeae3600e3 --- /dev/null +++ b/TelegramUI/OverlayMediaControllerNode.swift @@ -0,0 +1,178 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox + +private final class NotificationContainerControllerNodeView: UITracingLayerView { + var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return self.hitTestImpl?(point, event) + } +} + +private final class OverlayVideoContext { + var player: MediaPlayer? + let disposable = MetaDisposable() + var playerNode: MediaPlayerNode? + + deinit { + self.disposable.dispose() + } +} + +final class OverlayMediaControllerNode: ASDisplayNode { + private var videoContexts: [WrappedManagedMediaId: OverlayVideoContext] = [:] + private var validLayout: ContainerViewLayout? + + override init() { + super.init(viewBlock: { + return NotificationContainerControllerNodeView() + }, didLoad: nil) + + (self.view as! NotificationContainerControllerNodeView).hitTestImpl = { [weak self] point, event in + return self?.hitTest(point, with: event) + } + } + + deinit { + for (_, context) in self.videoContexts { + context.disposable.dispose() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + for (_, context) in self.videoContexts { + if let playerNode = context.playerNode { + let videoSize = CGSize(width: 100.0, height: 100.0) + transition.updateFrame(node: playerNode, frame: CGRect(origin: CGPoint(x: layout.size.width - 4.0 - videoSize.width, y: 20.0 + 44.0 + 38.0 + 4.0), size: videoSize)) + } + } + } + + func addVideoContext(mediaManager: MediaManager, postbox: Postbox, id: ManagedMediaId, resource: MediaResource, priority: Int32) { + let wrappedId = WrappedManagedMediaId(id: id) + if self.videoContexts[wrappedId] == nil { + let context = OverlayVideoContext() + self.videoContexts[wrappedId] = context + let (player, disposable) = mediaManager.videoContext(postbox: postbox, id: id, resource: resource, preferSoftwareDecoding: false, backgroundThread: false, priority: priority, initiatePlayback: true, activate: { [weak self] playerNode in + if let strongSelf = self, let context = strongSelf.videoContexts[wrappedId] { + if context.playerNode !== playerNode { + if context.playerNode?.supernode === self { + context.playerNode?.removeFromSupernode() + } + + context.playerNode = playerNode + + strongSelf.addSubnode(playerNode) + playerNode.transformArguments = TransformImageArguments(corners: ImageCorners(radius: 50.0), imageSize: CGSize(width: 100.0, height: 100.0), boundingSize: CGSize(width: 100.0, height: 100.0), intrinsicInsets: UIEdgeInsets()) + if let validLayout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(validLayout, transition: .immediate) + playerNode.layer.animatePosition(from: CGPoint(x: 104.0, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + } + }, deactivate: { [weak self] in + if let strongSelf = self, let context = strongSelf.videoContexts[wrappedId], let playerNode = context.playerNode { + if let snapshot = playerNode.view.snapshotView(afterScreenUpdates: false) { + snapshot.frame = playerNode.view.frame + strongSelf.view.addSubview(snapshot) + let fromPosition = playerNode.layer.position + playerNode.layer.position = CGPoint(x: playerNode.layer.position.x + 104.0, y: playerNode.layer.position.y) + snapshot.layer.animatePosition(from: fromPosition, to: playerNode.layer.position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak snapshot] _ in + snapshot?.removeFromSuperview() + }) + } + context.playerNode = nil + if playerNode.supernode === self { + playerNode.removeFromSupernode() + } + return .complete() + } else { + return .complete() + } + + /*return Signal { subscriber in + if let strongSelf = self, let context = strongSelf.videoContexts[wrappedId] { + if let playerNode = context.playerNode { + let fromPosition = playerNode.layer.position + playerNode.layer.position = CGPoint(x: playerNode.layer.position.x + 104.0, y: playerNode.layer.position.y) + context.playerNode = nil + playerNode.layer.animatePosition(from: fromPosition, to: playerNode.layer.position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + subscriber.putCompletion() + }) + } else { + subscriber.putCompletion() + } + } else { + subscriber.putCompletion() + } + return EmptyDisposable + }*/ + }) + context.player = player + context.disposable.set(disposable) + } + } + + /*func addVideoContext(id: ManagedMediaId, contextSignal: Signal) { + let wrappedId = WrappedManagedMediaId(id: id) + if self.videoContexts[wrappedId] == nil { + let context = OverlayVideoContext() + self.videoContexts[wrappedId] = context + + context.disposable.set((contextSignal |> deliverOnMainQueue).start(next: { [weak self] videoContext in + if let strongSelf = self, let context = strongSelf.videoContexts[wrappedId] { + if context.video?.playerNode !== videoContext.playerNode { + if context.video?.playerNode?.supernode === self { + context.video?.playerNode?.removeFromSupernode() + } + + context.video = videoContext + + if let playerNode = videoContext.playerNode { + strongSelf.addSubnode(playerNode) + playerNode.transformArguments = TransformImageArguments(corners: ImageCorners(radius: 50.0), imageSize: CGSize(width: 100.0, height: 100.0), boundingSize: CGSize(width: 100.0, height: 100.0), intrinsicInsets: UIEdgeInsets()) + if let validLayout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(validLayout, transition: .immediate) + playerNode.layer.animatePosition(from: CGPoint(x: 104.0, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + } else { + context.video = videoContext + } + } + })) + } + }*/ + + func removeVideoContext(id: ManagedMediaId) { + let wrappedId = WrappedManagedMediaId(id: id) + if let context = self.videoContexts[wrappedId] { + if let playerNode = context.playerNode { + if let snapshot = playerNode.view.snapshotView(afterScreenUpdates: false) { + snapshot.frame = playerNode.view.frame + self.view.addSubview(snapshot) + let fromPosition = playerNode.layer.position + playerNode.layer.position = CGPoint(x: playerNode.layer.position.x + 104.0, y: playerNode.layer.position.y) + snapshot.layer.animatePosition(from: fromPosition, to: playerNode.layer.position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak snapshot] _ in + snapshot?.removeFromSuperview() + }) + } + + context.playerNode = nil + playerNode.removeFromSupernode() + + } + context.disposable.dispose() + self.videoContexts.removeValue(forKey: wrappedId) + } + } +} diff --git a/TelegramUI/OverlayMediaItem.swift b/TelegramUI/OverlayMediaItem.swift new file mode 100644 index 0000000000..f7e554b6cb --- /dev/null +++ b/TelegramUI/OverlayMediaItem.swift @@ -0,0 +1,10 @@ +import Foundation +import AsyncDisplayKit + +protocol OverlayMediaItem: class { + func node() -> OverlayMediaItemNode +} + +class OverlayMediaItemNode: ASDisplayNode { + +} diff --git a/TelegramUI/OverlayMediaManager.swift b/TelegramUI/OverlayMediaManager.swift new file mode 100644 index 0000000000..2e3dbb7467 --- /dev/null +++ b/TelegramUI/OverlayMediaManager.swift @@ -0,0 +1,35 @@ +import Foundation + +final class OverlayMediaManager { + var controller: OverlayMediaController? + + private var items: [(OverlayMediaItem, OverlayMediaItemNode)] = [] + + init() { + + } + + func attachOverlayMediaController(_ controller: OverlayMediaController) { + self.controller = controller + } + + func addItem(_ item: OverlayMediaItem) { + let node = item.node() + self.items.append((item, node)) + + if let controller = self.controller { + node.frame = CGRect(origin: CGPoint(x: 10.0, y: 80.0), size: CGSize(width: 100.0, height: 60.0)) + controller.displayNode.addSubnode(node) + } + } + + func removeItem(_ item: OverlayMediaItem) { + for i in 0 ..< self.items.count { + if item === self.items[i].0 { + self.items[i].1.removeFromSupernode() + self.items.remove(at: i) + break + } + } + } +} diff --git a/TelegramUI/PasscodeOptionsController.swift b/TelegramUI/PasscodeOptionsController.swift index e82554265a..701761f8f6 100644 --- a/TelegramUI/PasscodeOptionsController.swift +++ b/TelegramUI/PasscodeOptionsController.swift @@ -27,12 +27,12 @@ private enum PasscodeOptionsSection: Int32 { } private enum PasscodeOptionsEntry: ItemListNodeEntry { - case togglePasscode(String, Bool) - case changePasscode(String) - case settingInfo(String) + case togglePasscode(PresentationTheme, String, Bool) + case changePasscode(PresentationTheme, String) + case settingInfo(PresentationTheme, String) - case autoLock(String, String) - case touchId(String, Bool) + case autoLock(PresentationTheme, String, String) + case touchId(PresentationTheme, String, Bool) var section: ItemListSectionId { switch self { @@ -60,32 +60,32 @@ private enum PasscodeOptionsEntry: ItemListNodeEntry { static func ==(lhs: PasscodeOptionsEntry, rhs: PasscodeOptionsEntry) -> Bool { switch lhs { - case let .togglePasscode(text, value): - if case .togglePasscode(text, value) = rhs { + case let .togglePasscode(lhsTheme, lhsText, lhsValue): + if case let .togglePasscode(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .changePasscode(text): - if case .changePasscode(text) = rhs { + case let .changePasscode(lhsTheme, lhsText): + if case let .changePasscode(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .settingInfo(text): - if case .settingInfo(text) = rhs { + case let .settingInfo(lhsTheme, lhsText): + if case let .settingInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .autoLock(text, value): - if case .autoLock(text, value) = rhs { + case let .autoLock(lhsTheme, lhsText, lhsValue): + if case let .autoLock(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .touchId(text, value): - if case .touchId(text, value) = rhs { + case let .touchId(lhsTheme, lhsText, lhsValue): + if case let .touchId(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false @@ -99,26 +99,26 @@ private enum PasscodeOptionsEntry: ItemListNodeEntry { func item(_ arguments: PasscodeOptionsControllerArguments) -> ListViewItem { switch self { - case let .togglePasscode(title, value): - return ItemListActionItem(title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .togglePasscode(theme, title, value): + return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { if value { arguments.turnPasscodeOff() } else { arguments.turnPasscodeOn() } }) - case let .changePasscode(title): - return ItemListActionItem(title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .changePasscode(theme, title): + return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.changePasscode() }) - case let .settingInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .autoLock(title, value): - return ItemListDisclosureItem(title: title, label: value, sectionId: self.section, style: .blocks, action: { + case let .settingInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .autoLock(theme, title, value): + return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.changePasscodeTimeout() }) - case let .touchId(title, value): - return ItemListSwitchItem(title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .touchId(theme, title, value): + return ItemListSwitchItem(theme: theme, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.changeTouchId(value) }) } @@ -173,19 +173,19 @@ private func autolockStringForTimeout(_ timeout: Int32?) -> String { } } -private func passcodeOptionsControllerEntries(state: PasscodeOptionsControllerState, passcodeOptionsData: PasscodeOptionsData) -> [PasscodeOptionsEntry] { +private func passcodeOptionsControllerEntries(presentationData: PresentationData, state: PasscodeOptionsControllerState, passcodeOptionsData: PasscodeOptionsData) -> [PasscodeOptionsEntry] { var entries: [PasscodeOptionsEntry] = [] switch passcodeOptionsData.accessChallenge { case .none: - entries.append(.togglePasscode("Turn Passcode On", false)) - entries.append(.settingInfo("When you set up an additional passcode, a lock icon will appear on the chats page. Tap it to lock and unlock the app.\n\nNote: if you forget the passcode, you'll need to delete and reinstall the app. All secret chats will be lost.")) + entries.append(.togglePasscode(presentationData.theme, presentationData.strings.PasscodeSettings_TurnPasscodeOn, false)) + entries.append(.settingInfo(presentationData.theme, presentationData.strings.PasscodeSettings_Help)) case .numericalPassword, .plaintextPassword: - entries.append(.togglePasscode("Turn Passcode Off", true)) - entries.append(.changePasscode("Change Passcode")) - entries.append(.settingInfo("When you set up an additional passcode, a lock icon will appear on the chats page. Tap it to lock and unlock the app.\n\nNote: if you forget the passcode, you'll need to delete and reinstall the app. All secret chats will be lost.")) - entries.append(.autoLock("Auto-Lock", autolockStringForTimeout(passcodeOptionsData.presentationSettings.autolockTimeout))) - entries.append(.touchId("Unlock with Touch ID", passcodeOptionsData.presentationSettings.enableBiometrics)) + entries.append(.togglePasscode(presentationData.theme, presentationData.strings.PasscodeSettings_TurnPasscodeOff, true)) + entries.append(.changePasscode(presentationData.theme, presentationData.strings.PasscodeSettings_ChangePasscode)) + entries.append(.settingInfo(presentationData.theme, presentationData.strings.PasscodeSettings_Help)) + entries.append(.autoLock(presentationData.theme, presentationData.strings.PasscodeSettings_AutoLock, autolockStringForTimeout(passcodeOptionsData.presentationSettings.autolockTimeout))) + entries.append(.touchId(presentationData.theme, presentationData.strings.PasscodeSettings_UnlockWithTouchId, passcodeOptionsData.presentationSettings.enableBiometrics)) } return entries @@ -330,20 +330,18 @@ func passcodeOptionsController(account: Account) -> ViewController { }) }) - let signal = combineLatest(statePromise.get(), passcodeOptionsDataPromise.get()) |> deliverOnMainQueue - |> map { state, passcodeOptionsData -> (ItemListControllerState, (ItemListNodeState, PasscodeOptionsEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), passcodeOptionsDataPromise.get()) |> deliverOnMainQueue + |> map { presentationData, state, passcodeOptionsData -> (ItemListControllerState, (ItemListNodeState, PasscodeOptionsEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(title: .text("Passcode Lock"), leftNavigationButton: nil, rightNavigationButton: nil, animateChanges: false) - let listState = ItemListNodeState(entries: passcodeOptionsControllerEntries(state: state, passcodeOptionsData: passcodeOptionsData), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.PasscodeSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: passcodeOptionsControllerEntries(presentationData: presentationData, state: state, passcodeOptionsData: passcodeOptionsData), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) - + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/PeerInfoEntries.swift b/TelegramUI/PeerInfoEntries.swift deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/TelegramUI/PeerMediaAudioPlaylist.swift b/TelegramUI/PeerMediaAudioPlaylist.swift index 40f27c0a63..76bf48264f 100644 --- a/TelegramUI/PeerMediaAudioPlaylist.swift +++ b/TelegramUI/PeerMediaAudioPlaylist.swift @@ -62,12 +62,19 @@ final class PeerMessageHistoryAudioPlaylistItem: AudioPlaylistItem { for media in message.media { if let file = media as? TelegramMediaFile { for attribute in file.attributes { - if case let .Audio(isVoice, duration, title, performer, _) = attribute { - if isVoice { - return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .voice) - } else { - return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .music(title: title, performer: performer)) - } + switch attribute { + case let .Audio(isVoice, duration, title, performer, _): + if isVoice { + return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .voice) + } else { + return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .music(title: title, performer: performer)) + } + case let .Video(duration, _, flags): + if flags.contains(.instantRoundVideo) { + return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .video) + } + default: + break } } return nil @@ -120,10 +127,23 @@ func peerMessageHistoryAudioPlaylist(account: Account, messageId: MessageId) -> case let .MessageEntry(message, _, _, _): for media in message.media { if let file = media as? TelegramMediaFile { - if file.isVoice { - tagMask = .VoiceOrInstantVideo - } else { - tagMask = .Music + inner: for attribute in file.attributes { + switch attribute { + case let .Video(_, _, flags): + if flags.contains(.instantRoundVideo) { + tagMask = .VoiceOrInstantVideo + break inner + } + case let .Audio(isVoice, _, _, _, _): + if isVoice { + tagMask = .VoiceOrInstantVideo + } else { + tagMask = .Music + } + break inner + default: + break + } } break } diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 7f3554958d..48ca77a5a8 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -22,7 +22,7 @@ public class PeerMediaCollectionController: ViewController { private var didSetPeerReady = false private let peer = Promise(nil) - private var interfaceState = PeerMediaCollectionInterfaceState() + private var interfaceState: PeerMediaCollectionInterfaceState private var rightNavigationButton: PeerMediaCollectionNavigationButton? @@ -34,20 +34,27 @@ public class PeerMediaCollectionController: ViewController { private let messageContextDisposable = MetaDisposable() + private var presentationData: PresentationData + public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) { self.account = account self.peerId = peerId self.messageId = messageId - super.init() + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + self.interfaceState = PeerMediaCollectionInterfaceState(theme: self.presentationData.theme, strings: self.presentationData.strings) - self.titleView = PeerMediaCollectionTitleView(toggle: { [weak self] in + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + self.titleView = PeerMediaCollectionTitleView(mediaCollectionInterfaceState: self.interfaceState, toggle: { [weak self] in self?.updateInterfaceState { $0.withToggledSelectingMode() } }) + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationItem.titleView = self.titleView - self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.ready.set(.never()) @@ -195,7 +202,10 @@ public class PeerMediaCollectionController: ViewController { }, openHashtag: {_ in }, updateInputState: { _ in }, openMessageShareMenu: { _ in - }, presentController: { _ in }) + }, presentController: { _ in + }, callPeer: { _ in + }, longTap: { _ in + }) self.controllerInteraction = controllerInteraction @@ -293,7 +303,11 @@ public class PeerMediaCollectionController: ViewController { }, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in }, editMessage: { _, _ in }, beginMessageSearch: { - }, navigateToMessage: { _ in + }, dismissMessageSearch: { + }, updateMessageSearch: { _ in + }, navigateMessageSearch: { _ in + }, openCalendarSearch: { + }, navigateToMessage: { _ in }, openPeerInfo: { }, togglePeerNotifications: { }, sendContextResult: { _ in diff --git a/TelegramUI/PeerMediaCollectionControllerNode.swift b/TelegramUI/PeerMediaCollectionControllerNode.swift index 02523a148b..35a8bc2c8c 100644 --- a/TelegramUI/PeerMediaCollectionControllerNode.swift +++ b/TelegramUI/PeerMediaCollectionControllerNode.swift @@ -43,24 +43,33 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in } var requestUpdateMediaCollectionInterfaceState: (Bool, (PeerMediaCollectionInterfaceState) -> PeerMediaCollectionInterfaceState) -> Void = { _ in } - private var mediaCollectionInterfaceState = PeerMediaCollectionInterfaceState() + private var mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState private var modeSelectionNode: PeerMediaCollectionModeSelectionNode? private var selectionPanel: ChatMessageSelectionInputPanelNode? + private var chatPresentationInterfaceState: ChatPresentationInterfaceState + + private var presentationData: PresentationData + init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) { self.account = account self.peerId = peerId self.controllerInteraction = controllerInteraction self.interfaceInteraction = interfaceInteraction + self.presentationData = (account.applicationContext as! TelegramApplicationContext).currentPresentationData.with { $0 } + self.mediaCollectionInterfaceState = PeerMediaCollectionInterfaceState(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.historyNodeImpl = historyNodeImplForMode(self.mediaCollectionInterfaceState.mode, account: account, peerId: peerId, messageId: messageId, controllerInteraction: controllerInteraction) + self.chatPresentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, strings: self.presentationData.strings) + super.init(viewBlock: { return UITracingLayerView() }, didLoad: nil) - self.backgroundColor = UIColor.white + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.addSubnode(self.historyNodeImpl) } @@ -76,17 +85,17 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { insets.top += navigationBarHeight if let selectionState = self.mediaCollectionInterfaceState.selectionState { - let interfaceState = ChatPresentationInterfaceState().updatedPeer({ _ in self.mediaCollectionInterfaceState.peer }) + let interfaceState = self.chatPresentationInterfaceState.updatedPeer({ _ in self.mediaCollectionInterfaceState.peer }) if let selectionPanel = self.selectionPanel { selectionPanel.selectedMessageCount = selectionState.selectedIds.count let panelHeight = selectionPanel.updateLayout(width: layout.size.width, transition: transition, interfaceState: interfaceState) transition.updateFrame(node: selectionPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) } else { - let selectionPanel = ChatMessageSelectionInputPanelNode() + let selectionPanel = ChatMessageSelectionInputPanelNode(theme: self.chatPresentationInterfaceState.theme) selectionPanel.interfaceInteraction = self.interfaceInteraction selectionPanel.selectedMessageCount = selectionState.selectedIds.count - selectionPanel.backgroundColor = UIColor(0xfafafa) + selectionPanel.backgroundColor = self.presentationData.theme.chat.inputPanel.panelBackgroundColor let panelHeight = selectionPanel.updateLayout(width: layout.size.width, transition: .immediate, interfaceState: interfaceState) self.selectionPanel = selectionPanel self.addSubnode(selectionPanel) @@ -149,7 +158,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { modeSelectionNode.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) modeSelectionNode.mediaCollectionInterfaceState = self.mediaCollectionInterfaceState } else { - let modeSelectionNode = PeerMediaCollectionModeSelectionNode() + let modeSelectionNode = PeerMediaCollectionModeSelectionNode(mediaCollectionInterfaceState: self.mediaCollectionInterfaceState) modeSelectionNode.selectedMode = { [weak self] mode in if let requestUpdateMediaCollectionInterfaceState = self?.requestUpdateMediaCollectionInterfaceState { requestUpdateMediaCollectionInterfaceState(true, { $0.withToggledSelectingMode().withMode(mode) }) diff --git a/TelegramUI/PeerMediaCollectionInterfaceState.swift b/TelegramUI/PeerMediaCollectionInterfaceState.swift index a8cd06bf3d..2dc5a66637 100644 --- a/TelegramUI/PeerMediaCollectionInterfaceState.swift +++ b/TelegramUI/PeerMediaCollectionInterfaceState.swift @@ -8,16 +8,16 @@ enum PeerMediaCollectionMode { case webpage } -func titleForPeerMediaCollectionMode(_ mode: PeerMediaCollectionMode) -> String { +func titleForPeerMediaCollectionMode(_ mode: PeerMediaCollectionMode, strings: PresentationStrings) -> String { switch mode { case .photoOrVideo: - return "Shared Media" + return strings.SharedMedia_TitleAll case .file: - return "Shared Files" + return strings.SharedMedia_TitleFile case .music: - return "Shared Music" + return strings.SharedMedia_TitleAudio case .webpage: - return "Shared Links" + return strings.SharedMedia_TitleLink } } @@ -26,19 +26,25 @@ struct PeerMediaCollectionInterfaceState: Equatable { let selectionState: ChatInterfaceSelectionState? let mode: PeerMediaCollectionMode let selectingMode: Bool + let theme: PresentationTheme + let strings: PresentationStrings - init() { + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings self.peer = nil self.selectionState = nil self.mode = .photoOrVideo self.selectingMode = false } - init(peer: Peer?, selectionState: ChatInterfaceSelectionState?, mode: PeerMediaCollectionMode, selectingMode: Bool) { + init(peer: Peer?, selectionState: ChatInterfaceSelectionState?, mode: PeerMediaCollectionMode, selectingMode: Bool, theme: PresentationTheme, strings: PresentationStrings) { self.peer = peer self.selectionState = selectionState self.mode = mode self.selectingMode = selectingMode + self.theme = theme + self.strings = strings } static func ==(lhs: PeerMediaCollectionInterfaceState, rhs: PeerMediaCollectionInterfaceState) -> Bool { @@ -62,6 +68,14 @@ struct PeerMediaCollectionInterfaceState: Equatable { return false } + if lhs.theme !== rhs.theme { + return false + } + + if lhs.strings !== rhs.strings { + return false + } + return true } @@ -71,7 +85,7 @@ struct PeerMediaCollectionInterfaceState: Equatable { selectedIds.formUnion(selectionState.selectedIds) } selectedIds.insert(messageId) - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), mode: self.mode, selectingMode: self.selectingMode) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), mode: self.mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) } func withToggledSelectedMessage(_ messageId: MessageId) -> PeerMediaCollectionInterfaceState { @@ -84,26 +98,26 @@ struct PeerMediaCollectionInterfaceState: Equatable { } else { selectedIds.insert(messageId) } - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), mode: self.mode, selectingMode: self.selectingMode) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), mode: self.mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) } func withSelectionState() -> PeerMediaCollectionInterfaceState { - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState ?? ChatInterfaceSelectionState(selectedIds: Set()), mode: self.mode, selectingMode: self.selectingMode) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState ?? ChatInterfaceSelectionState(selectedIds: Set()), mode: self.mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) } func withoutSelectionState() -> PeerMediaCollectionInterfaceState { - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: nil, mode: self.mode, selectingMode: self.selectingMode) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: nil, mode: self.mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) } func withUpdatedPeer(_ peer: Peer?) -> PeerMediaCollectionInterfaceState { - return PeerMediaCollectionInterfaceState(peer: peer, selectionState: self.selectionState, mode: self.mode, selectingMode: self.selectingMode) + return PeerMediaCollectionInterfaceState(peer: peer, selectionState: self.selectionState, mode: self.mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) } func withToggledSelectingMode() -> PeerMediaCollectionInterfaceState { - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState, mode: self.mode, selectingMode: !self.selectingMode) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState, mode: self.mode, selectingMode: !self.selectingMode, theme: self.theme, strings: self.strings) } func withMode(_ mode: PeerMediaCollectionMode) -> PeerMediaCollectionInterfaceState { - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState, mode: mode, selectingMode: self.selectingMode) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState, mode: mode, selectingMode: self.selectingMode, theme: self.theme, strings: self.strings) } } diff --git a/TelegramUI/PeerMediaCollectionModeSelectionNode.swift b/TelegramUI/PeerMediaCollectionModeSelectionNode.swift index ab2d09c403..7383cf7e09 100644 --- a/TelegramUI/PeerMediaCollectionModeSelectionNode.swift +++ b/TelegramUI/PeerMediaCollectionModeSelectionNode.swift @@ -2,9 +2,9 @@ import Foundation import AsyncDisplayKit import Display -private let checkmarkImage = generateTintedImage(image: UIImage(bundleImageName: "List Menu/Checkmark")?.precomposed(), color: UIColor(0x007ee5)) - private final class PeerMediaCollectionModeSelectionCaseNode: ASDisplayNode { + private let theme: PresentationTheme + private let strings: PresentationStrings fileprivate let mode: PeerMediaCollectionMode private let selected: () -> Void @@ -17,23 +17,25 @@ private final class PeerMediaCollectionModeSelectionCaseNode: ASDisplayNode { var isSelected = false { didSet { if self.isSelected != oldValue { - self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(self.mode), font: Font.regular(17.0), textColor: isSelected ? UIColor(0x007ee5) : UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(self.mode, strings: self.strings), font: Font.regular(17.0), textColor: isSelected ? self.theme.list.itemAccentColor : self.theme.list.itemPrimaryTextColor) self.checkmarkView.isHidden = !self.isSelected } } } - init(mode: PeerMediaCollectionMode, selected: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, mode: PeerMediaCollectionMode, selected: @escaping () -> Void) { + self.theme = theme + self.strings = strings self.mode = mode self.selected = selected self.button = HighlightTrackingButton() self.selectionBackgroundNode = ASDisplayNode() - self.selectionBackgroundNode.backgroundColor = UIColor(0xd9d9d9) + self.selectionBackgroundNode.backgroundColor = self.theme.list.itemHighlightedBackgroundColor self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) + self.separatorNode.backgroundColor = self.theme.list.itemSeparatorColor self.titleNode = ASTextNode() self.titleNode.displaysAsynchronously = false @@ -41,7 +43,7 @@ private final class PeerMediaCollectionModeSelectionCaseNode: ASDisplayNode { self.titleNode.truncationMode = .byTruncatingTail self.titleNode.isOpaque = false - self.checkmarkView = UIImageView(image: checkmarkImage) + self.checkmarkView = UIImageView(image: PresentationResourcesItemList.checkIconImage(self.theme)) super.init() @@ -50,7 +52,7 @@ private final class PeerMediaCollectionModeSelectionCaseNode: ASDisplayNode { self.selectionBackgroundNode.alpha = 0.0 self.addSubnode(self.selectionBackgroundNode) - self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(mode), font: Font.regular(17.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(mode, strings: self.strings), font: Font.regular(17.0), textColor: self.theme.list.itemPrimaryTextColor) self.addSubnode(self.titleNode) self.checkmarkView.isHidden = true @@ -100,7 +102,7 @@ final class PeerMediaCollectionModeSelectionNode: ASDisplayNode { var selectedMode: ((PeerMediaCollectionMode) -> Void)? var dismiss: (() -> Void)? - var mediaCollectionInterfaceState = PeerMediaCollectionInterfaceState() { + var mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState { didSet { for caseNode in self.caseNodes { caseNode.isSelected = self.mediaCollectionInterfaceState.mode == caseNode.mode @@ -108,12 +110,14 @@ final class PeerMediaCollectionModeSelectionNode: ASDisplayNode { } } - override init() { + init(mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState) { + self.mediaCollectionInterfaceState = mediaCollectionInterfaceState + self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4) self.backgroundNode = ASDisplayNode() - self.backgroundNode.backgroundColor = UIColor.white + self.backgroundNode.backgroundColor = self.mediaCollectionInterfaceState.theme.list.itemBackgroundColor super.init() @@ -124,7 +128,7 @@ final class PeerMediaCollectionModeSelectionNode: ASDisplayNode { } } self.caseNodes = modes.map { mode in - return PeerMediaCollectionModeSelectionCaseNode(mode: mode, selected: { + return PeerMediaCollectionModeSelectionCaseNode(theme: self.mediaCollectionInterfaceState.theme, strings: self.mediaCollectionInterfaceState.strings, mode: mode, selected: { selected(mode) }) } diff --git a/TelegramUI/PeerMediaCollectionTitleView.swift b/TelegramUI/PeerMediaCollectionTitleView.swift index 19164d4d1c..2937d80c97 100644 --- a/TelegramUI/PeerMediaCollectionTitleView.swift +++ b/TelegramUI/PeerMediaCollectionTitleView.swift @@ -2,8 +2,6 @@ import Foundation import AsyncDisplayKit import Display -private let arrowImage = UIImage(bundleImageName: "Media Grid/TitleViewModeSelectionArrow")?.precomposed() - final class PeerMediaCollectionTitleView: UIView { private let toggle: () -> Void @@ -11,9 +9,10 @@ final class PeerMediaCollectionTitleView: UIView { private let arrowView: UIImageView private let button: HighlightTrackingButton - private var mediaCollectionInterfaceState = PeerMediaCollectionInterfaceState() + private var mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState - init(toggle: @escaping () -> Void) { + init(mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState, toggle: @escaping () -> Void) { + self.mediaCollectionInterfaceState = mediaCollectionInterfaceState self.toggle = toggle self.titleNode = ASTextNode() @@ -22,13 +21,13 @@ final class PeerMediaCollectionTitleView: UIView { self.titleNode.truncationMode = .byTruncatingTail self.titleNode.isOpaque = false - self.arrowView = UIImageView(image: arrowImage) + self.arrowView = UIImageView(image: PresentationResourcesRootController.navigationDropdownArrowImage(self.mediaCollectionInterfaceState.theme)) self.button = HighlightTrackingButton(frame: CGRect()) super.init(frame: CGRect()) - self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(self.mediaCollectionInterfaceState.mode), font: Font.medium(17.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(self.mediaCollectionInterfaceState.mode, strings: self.mediaCollectionInterfaceState.strings), font: Font.medium(17.0), textColor: self.mediaCollectionInterfaceState.theme.rootController.navigationBar.primaryTextColor) self.addSubnode(self.titleNode) self.addSubview(self.arrowView) @@ -74,7 +73,7 @@ final class PeerMediaCollectionTitleView: UIView { func updateMediaCollectionInterfaceState(_ mediaCollectionInterfaceState: PeerMediaCollectionInterfaceState, animated: Bool) { if self.mediaCollectionInterfaceState != mediaCollectionInterfaceState { if mediaCollectionInterfaceState.mode != self.mediaCollectionInterfaceState.mode { - self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(mediaCollectionInterfaceState.mode), font: Font.medium(17.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: titleForPeerMediaCollectionMode(mediaCollectionInterfaceState.mode, strings: mediaCollectionInterfaceState.strings), font: Font.medium(17.0), textColor: mediaCollectionInterfaceState.theme.rootController.navigationBar.primaryTextColor) self.setNeedsLayout() } diff --git a/TelegramUI/PeerNotificationSoundStrings.swift b/TelegramUI/PeerNotificationSoundStrings.swift index 5591a7454b..d13ca03ad6 100644 --- a/TelegramUI/PeerNotificationSoundStrings.swift +++ b/TelegramUI/PeerNotificationSoundStrings.swift @@ -27,10 +27,10 @@ private let classicSounds: [String] = [ "Telegraph" ] -func localizedPeerNotificationSoundString(_ sound: PeerMessageSound) -> String { +func localizedPeerNotificationSoundString(strings: PresentationStrings, sound: PeerMessageSound) -> String { switch sound { case .none: - return "None" + return strings.Settings_UsernameEmpty case let .bundledModern(id): if id >= 0 && Int(id) < modernSounds.count { return modernSounds[Int(id)] diff --git a/TelegramUI/PeerSelectionController.swift b/TelegramUI/PeerSelectionController.swift index 3492e3360d..10a8db1435 100644 --- a/TelegramUI/PeerSelectionController.swift +++ b/TelegramUI/PeerSelectionController.swift @@ -18,7 +18,7 @@ public final class PeerSelectionController: ViewController { public init(account: Account) { self.account = account - super.init() + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme)) self.title = "Forward" diff --git a/TelegramUI/PeerSelectionControllerNode.swift b/TelegramUI/PeerSelectionControllerNode.swift index bb21eeb3d7..cc7ade6080 100644 --- a/TelegramUI/PeerSelectionControllerNode.swift +++ b/TelegramUI/PeerSelectionControllerNode.swift @@ -3,6 +3,7 @@ import AsyncDisplayKit import Display import Postbox import TelegramCore +import SwiftSignalKit final class PeerSelectionControllerNode: ASDisplayNode { private let account: Account @@ -19,16 +20,42 @@ final class PeerSelectionControllerNode: ASDisplayNode { var requestOpenPeerFromSearch: ((Peer) -> Void)? var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)? + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + init(account: Account, dismiss: @escaping () -> Void) { self.account = account self.dismiss = dismiss - self.chatListNode = ChatListNode(account: account, mode: .peers) + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.chatListNode = ChatListNode(account: account, mode: .peers, theme: presentationData.theme, strings: presentationData.strings) super.init(viewBlock: { return UITracingLayerView() }, didLoad: nil) self.addSubnode(self.chatListNode) + 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() + } + } + }) + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.chatListNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -89,7 +116,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(contentNode: ChatListSearchContainerNode(account: self.account, openPeer: { [weak self] peer in + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: ChatListSearchContainerNode(account: self.account, openPeer: { [weak self] peer in if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peer) } diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index b1ecbfe178..37db3c0f7c 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -1291,9 +1291,8 @@ func chatMessageFileCancelInteractiveFetch(account: Account, file: TelegramMedia account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) } -private func avatarGalleryPhotoDatas(account: Account, representations: [TelegramMediaImageRepresentation]) -> Signal<(Data?, Data?, Bool), NoError> { +private func avatarGalleryPhotoDatas(account: Account, representations: [TelegramMediaImageRepresentation], autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { if let smallestRepresentation = smallestImageRepresentation(representations), let largestRepresentation = largestImageRepresentation(representations) { - let autoFetchFullSize = false let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in @@ -1352,8 +1351,8 @@ private func avatarGalleryPhotoDatas(account: Account, representations: [Telegra } } -func chatAvatarGalleryPhoto(account: Account, representations: [TelegramMediaImageRepresentation]) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = avatarGalleryPhotoDatas(account: account, representations: representations) +func chatAvatarGalleryPhoto(account: Account, representations: [TelegramMediaImageRepresentation], autoFetchFullSize: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = avatarGalleryPhotoDatas(account: account, representations: representations, autoFetchFullSize: autoFetchFullSize) return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in @@ -1441,3 +1440,46 @@ func chatAvatarGalleryPhoto(account: Account, representations: [TelegramMediaIma } } } + +private func builtinWallpaperData() -> Signal { + return Signal { subscriber in + if let image = UIImage(bundleImageName: "Chat/Wallpapers/Builtin0") { + subscriber.putNext(image) + } + subscriber.putCompletion() + + return EmptyDisposable + } |> runOn(Queue.concurrentDefaultQueue()) +} + +func settingsBuiltinWallpaperImage(account: Account) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return builtinWallpaperData() |> map { fullSizeImage in + return { arguments in + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + let drawingRect = arguments.drawingRect + var fittedSize = fullSizeImage.size.aspectFilled(drawingRect.size) + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if let fullSizeImage = fullSizeImage.cgImage { + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + } + } + + addCorners(context, arguments: arguments) + + return context + } + } +} + diff --git a/TelegramUI/PreferencesKeys.swift b/TelegramUI/PreferencesKeys.swift index 32d9788601..537fe81161 100644 --- a/TelegramUI/PreferencesKeys.swift +++ b/TelegramUI/PreferencesKeys.swift @@ -7,6 +7,7 @@ private enum ApplicationSpecificPreferencesKeyValues: Int32 { case automaticMediaDownloadSettings = 2 case generatedMediaStoreSettings = 3 case voiceCallSettings = 4 + case presentationThemeSettings = 5 } public struct ApplicationSpecificPreferencesKeys { @@ -15,4 +16,5 @@ public struct ApplicationSpecificPreferencesKeys { public static let automaticMediaDownloadSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.automaticMediaDownloadSettings.rawValue) public static let generatedMediaStoreSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.generatedMediaStoreSettings.rawValue) public static let voiceCallSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voiceCallSettings.rawValue) + public static let presentationThemeSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.presentationThemeSettings.rawValue) } diff --git a/TelegramUI/PresenceStrings.swift b/TelegramUI/PresenceStrings.swift index cc2e7d209a..66e9e696d4 100644 --- a/TelegramUI/PresenceStrings.swift +++ b/TelegramUI/PresenceStrings.swift @@ -10,60 +10,60 @@ func stringForTimestamp(day: Int32, month: Int32) -> String { return String(format: "%d.%02d", day, month) } -func shortStringForDayOfWeek(_ day: Int32) -> String { +func shortStringForDayOfWeek(strings: PresentationStrings, day: Int32) -> String { switch day { case 0: - return "Sun" + return strings.Weekday_ShortSunday case 1: - return "Mon" + return strings.Weekday_ShortMonday case 2: - return "Tue" + return strings.Weekday_ShortTuesday case 3: - return "Wed" + return strings.Weekday_ShortWednesday case 4: - return "Thu" + return strings.Weekday_ShortThursday case 5: - return "Fri" + return strings.Weekday_ShortFriday case 6: - return "Sat" + return strings.Weekday_ShortSaturday default: return "" } } -func stringForMonth(_ month: Int32) -> String { +func stringForMonth(strings: PresentationStrings, month: Int32) -> String { switch month { case 0: - return "January" + return strings.Month_GenJanuary case 1: - return "February" + return strings.Month_GenFebruary case 2: - return "March" + return strings.Month_GenMarch case 3: - return "April" + return strings.Month_GenApril case 4: - return "May" + return strings.Month_GenMay case 5: - return "June" + return strings.Month_GenJune case 6: - return "July" + return strings.Month_GenJuly case 7: - return "August" + return strings.Month_GenAugust case 8: - return "September" + return strings.Month_GenSeptember case 9: - return "October" + return strings.Month_GenOctober case 10: - return "November" + return strings.Month_GenNovember case 11: - return "December" + return strings.Month_GenDecember default: return "" } } -func stringForMonth(_ month: Int32, ofYear year: Int32) -> String { - return stringForMonth(month) + " \(1900 + year)" +func stringForMonth(strings: PresentationStrings, month: Int32, ofYear year: Int32) -> String { + return stringForMonth(strings: strings, month: month) + " \(1900 + year)" } func stringForTime(hours: Int32, minutes: Int32) -> String { @@ -75,29 +75,29 @@ enum RelativeTimestampFormatDay { case yesterday } -func stringForUserPresence(day: RelativeTimestampFormatDay, hours: Int32, minutes: Int32) -> String { +func stringForUserPresence(strings: PresentationStrings, day: RelativeTimestampFormatDay, hours: Int32, minutes: Int32) -> String { let dayString: String switch day { case .today: - dayString = "today" + dayString = strings.LastSeen_AtDate(strings.Time_TodayAt(stringForTime(hours: hours, minutes: minutes)).0).0 case .yesterday: - dayString = "yesterday" + dayString = strings.LastSeen_AtDate(strings.Time_YesterdayAt(stringForTime(hours: hours, minutes: minutes)).0).0 } - return "last seen \(dayString) at \(stringForTime(hours: hours, minutes: minutes))" + return dayString } -private func humanReadableStringForTimestamp(day: RelativeTimestampFormatDay, hours: Int32, minutes: Int32) -> String { +private func humanReadableStringForTimestamp(strings: PresentationStrings, day: RelativeTimestampFormatDay, hours: Int32, minutes: Int32) -> String { let dayString: String switch day { - case .today: - dayString = "today" - case .yesterday: - dayString = "yesterday" + case .today: + dayString = strings.Time_TodayAt(stringForTime(hours: hours, minutes: minutes)).0 + case .yesterday: + dayString = strings.Time_YesterdayAt(stringForTime(hours: hours, minutes: minutes)).0 } - return "\(dayString) at \(stringForTime(hours: hours, minutes: minutes))" + return dayString } -func humanReadableStringForTimestamp(timestamp: Int32) -> String { +func humanReadableStringForTimestamp(strings: PresentationStrings, timestamp: Int32) -> String { var t: time_t = time_t(timestamp) var timeinfo: tm = tm() localtime_r(&t, &timeinfo) @@ -119,7 +119,7 @@ func humanReadableStringForTimestamp(timestamp: Int32) -> String { } else { day = .yesterday } - return humanReadableStringForTimestamp(day: day, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min) + return humanReadableStringForTimestamp(strings: strings, day: day, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min) } else { return "\(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year))" } @@ -163,7 +163,7 @@ func relativeUserPresenceStatus(_ presence: TelegramUserPresence, relativeTo tim } } -func stringForRelativeTimestamp(_ relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String { +func stringForRelativeTimestamp(strings: PresentationStrings, relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String { var t: time_t = time_t(relativeTimestamp) var timeinfo: tm = tm() localtime_r(&t, &timeinfo) @@ -181,31 +181,27 @@ func stringForRelativeTimestamp(_ relativeTimestamp: Int32, relativeTo timestamp if dayDifference == 0 { return stringForTime(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min) } else { - return shortStringForDayOfWeek(timeinfo.tm_wday) + return shortStringForDayOfWeek(strings: strings, day: timeinfo.tm_wday) } } else { return stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1) } } -func stringAndActivityForUserPresence(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> (String, Bool) { +func stringAndActivityForUserPresence(strings: PresentationStrings, presence: TelegramUserPresence, relativeTo timestamp: Int32) -> (String, Bool) { switch presence.status { case .none: - return ("offline", false) + return (strings.Presence_offline, false) case let .present(statusTimestamp): if statusTimestamp >= timestamp { - return ("online", true) + return (strings.Presence_online, true) } else { let difference = timestamp - statusTimestamp - if difference < 30 { - return ("last seen just now", false) + if difference < 60 { + return (strings.LastSeen_JustNow, false) } else if difference < 60 * 60 { let minutes = difference / 60 - if minutes <= 1 { - return ("last seen 1 minute ago", false) - } else { - return ("last seen \(minutes) minutes ago", false) - } + return (strings.LastSeen_MinutesAgo(minutes), false) } else { var t: time_t = time_t(statusTimestamp) var timeinfo: tm = tm() @@ -216,7 +212,7 @@ func stringAndActivityForUserPresence(_ presence: TelegramUserPresence, relative localtime_r(&now, &timeinfoNow) if timeinfo.tm_year != timeinfoNow.tm_year { - return ("last seen \(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year))", false) + return (strings.LastSeen_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year)).0, false) } let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday @@ -227,18 +223,18 @@ func stringAndActivityForUserPresence(_ presence: TelegramUserPresence, relative } else { day = .yesterday } - return (stringForUserPresence(day: day, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min), false) + return (stringForUserPresence(strings: strings, day: day, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min), false) } else { - return ("last seen \(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year))", false) + return (strings.LastSeen_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year)).0, false) } } } case .recently: - return ("last seen recently", false) + return (strings.LastSeen_Lately, false) case .lastWeek: - return ("last seen last week", false) + return (strings.LastSeen_WithinAWeek, false) case .lastMonth: - return ("last seen last month", false) + return (strings.LastSeen_WithinAMonth, false) } } diff --git a/TelegramUI/PresentationCall.swift b/TelegramUI/PresentationCall.swift new file mode 100644 index 0000000000..7a6d4e98f6 --- /dev/null +++ b/TelegramUI/PresentationCall.swift @@ -0,0 +1,287 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +public enum PresentationCallState: Equatable { + case waiting + case ringing + case requesting(Bool) + case connecting + case active(Double, Data) + case terminating + case terminated + + public static func ==(lhs: PresentationCallState, rhs: PresentationCallState) -> Bool { + switch lhs { + case .waiting: + if case .waiting = rhs { + return true + } else { + return false + } + case .ringing: + if case .ringing = rhs { + return true + } else { + return false + } + case let .requesting(ringing): + if case .requesting(ringing) = rhs { + return true + } else { + return false + } + case .connecting: + if case .connecting = rhs { + return true + } else { + return false + } + case let .active(timestamp, keyVisualHash): + if case .active(timestamp, keyVisualHash) = rhs { + return true + } else { + return false + } + case .terminating: + if case .terminating = rhs { + return true + } else { + return false + } + case .terminated: + if case .terminated = rhs { + return true + } else { + return false + } + } + } +} + +public final class PresentationCall { + private let audioSession: ManagedAudioSession + private let callSessionManager: CallSessionManager + private let callKitIntegration: CallKitIntegration? + + let internalId: CallSessionInternalId + let peerId: PeerId + let isOutgoing: Bool + let peer: Peer? + + private var sessionState: CallSession? + private var ongoingGontext: OngoingCallContext + + private var sessionStateDisposable: Disposable? + + private let statePromise = ValuePromise(.waiting, ignoreRepeated: true) + public var state: Signal { + return self.statePromise.get() + } + + private let isMutedPromise = ValuePromise(false) + private var isMutedValue = false + public var isMuted: Signal { + return self.isMutedPromise.get() + } + + private let speakerModePromise = ValuePromise(false) + private var speakerModeValue = false + public var speakerMode: Signal { + return self.speakerModePromise.get() + } + + private let canBeRemovedPromise = Promise(false) + private var didSetCanBeRemoved = false + var canBeRemoved: Signal { + return self.canBeRemovedPromise.get() + } + + private let hungUpPromise = ValuePromise() + + private var activeTimestamp: Double? + + private var audioSessionControl: ManagedAudioSessionControl? + private var audioSessionDisposable: Disposable? + + init(audioSession: ManagedAudioSession, callSessionManager: CallSessionManager, callKitIntegration: CallKitIntegration?, internalId: CallSessionInternalId, peerId: PeerId, isOutgoing: Bool, peer: Peer?) { + self.audioSession = audioSession + self.callSessionManager = callSessionManager + self.callKitIntegration = callKitIntegration + + self.internalId = internalId + self.peerId = peerId + self.isOutgoing = isOutgoing + self.peer = peer + + self.ongoingGontext = OngoingCallContext(callSessionManager: self.callSessionManager, internalId: self.internalId) + + self.sessionStateDisposable = (callSessionManager.callState(internalId: internalId) + |> deliverOnMainQueue).start(next: { [weak self] sessionState in + if let strongSelf = self { + strongSelf.updateSessionState(sessionState: sessionState, audioSessionControl: strongSelf.audioSessionControl) + } + }) + + self.audioSessionDisposable = audioSession.push(audioSessionType: .voiceCall, manualActivate: { [weak self] control in + Queue.mainQueue().async { + if let strongSelf = self { + if let sessionState = strongSelf.sessionState { + strongSelf.updateSessionState(sessionState: sessionState, audioSessionControl: control) + } else { + strongSelf.audioSessionControl = control + } + } + } + }, deactivate: { [weak self] in + return Signal { subscriber in + Queue.mainQueue().async { + if let strongSelf = self { + if let sessionState = strongSelf.sessionState { + strongSelf.updateSessionState(sessionState: sessionState, audioSessionControl: nil) + } else { + strongSelf.audioSessionControl = nil + } + } + subscriber.putCompletion() + } + return EmptyDisposable + } + }) + } + + deinit { + self.sessionStateDisposable?.dispose() + self.audioSessionDisposable?.dispose() + } + + private func updateSessionState(sessionState: CallSession, audioSessionControl: ManagedAudioSessionControl?) { + let previous = self.sessionState + let previousControl = self.audioSessionControl + self.sessionState = sessionState + self.audioSessionControl = audioSessionControl + + let presentationState: PresentationCallState + + var wasActive = false + var wasTerminated = false + if let previous = previous { + switch previous.state { + case .active: + wasActive = true + case .terminated: + wasTerminated = true + default: + break + } + } + + if let audioSessionControl = audioSessionControl, previous == nil || previousControl == nil { + audioSessionControl.setSpeaker(self.speakerModeValue) + audioSessionControl.setup(synchronous: true) + } + + switch sessionState.state { + case .ringing: + presentationState = .ringing + if let _ = audioSessionControl, previous == nil || previousControl == nil { + self.callKitIntegration?.reportIncomingCall(uuid: self.internalId, handle: "\(self.peerId.id)", displayTitle: self.peer?.displayTitle ?? "Unknown", completion: { [weak self] error in + if error != nil { + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.callSessionManager.drop(internalId: strongSelf.internalId, reason: .hangUp) + } + } + } + }) + } + case .accepting: + presentationState = .connecting + case .dropping: + presentationState = .terminating + case .terminated: + presentationState = .terminated + case let .requesting(ringing): + presentationState = .requesting(ringing) + case let .active(_, keyVisualHash, _): + let timestamp: Double + if let activeTimestamp = self.activeTimestamp { + timestamp = activeTimestamp + } else { + timestamp = CFAbsoluteTimeGetCurrent() + self.activeTimestamp = timestamp + } + presentationState = .active(timestamp, keyVisualHash) + } + + switch sessionState.state { + case let .active(key, _, connections): + if let audioSessionControl = audioSessionControl, !wasActive || previousControl == nil { + let audioSessionActive: Signal + if let callKitIntegration = self.callKitIntegration { + audioSessionActive = callKitIntegration.audioSessionActive |> filter { $0 } |> timeout(2.0, queue: Queue.mainQueue(), alternate: Signal { [weak self] subscriber in + if let strongSelf = self, let audioSessionControl = strongSelf.audioSessionControl { + audioSessionControl.activate() + } + subscriber.putNext(true) + subscriber.putCompletion() + return EmptyDisposable + }) + } else { + audioSessionControl.activate() + audioSessionActive = .single(true) + } + + self.ongoingGontext.start(key: key, isOutgoing: sessionState.isOutgoing, connections: connections, audioSessionActive: audioSessionActive) + if sessionState.isOutgoing { + self.callKitIntegration?.reportOutgoingCallConnected(uuid: sessionState.id, at: Date()) + } + } + default: + if wasActive { + self.ongoingGontext.stop() + } + } + if case .terminated = sessionState.state, !wasTerminated { + if !self.didSetCanBeRemoved { + self.didSetCanBeRemoved = true + self.canBeRemovedPromise.set(.single(true) |> delay(2.0, queue: Queue.mainQueue())) + } + self.hungUpPromise.set(true) + self.callKitIntegration?.dropCall(uuid: self.internalId) + } + self.statePromise.set(presentationState) + } + + func answer() { + self.callSessionManager.accept(internalId: self.internalId) + self.callKitIntegration?.answerCall(uuid: self.internalId) + } + + func hangUp() -> Signal { + self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp) + self.ongoingGontext.stop() + + return self.hungUpPromise.get() + } + + func rejectBusy() { + self.callSessionManager.drop(internalId: self.internalId, reason: .busy) + self.ongoingGontext.stop() + } + + func toggleIsMuted() { + self.isMutedValue = !self.isMutedValue + self.isMutedPromise.set(self.isMutedValue) + self.ongoingGontext.setIsMuted(self.isMutedValue) + } + + func toggleSpeaker() { + self.speakerModeValue = !self.speakerModeValue + self.speakerModePromise.set(self.speakerModeValue) + if let audioSessionControl = self.audioSessionControl { + audioSessionControl.setSpeaker(self.speakerModeValue) + } + } +} diff --git a/TelegramUI/PresentationCallManager.swift b/TelegramUI/PresentationCallManager.swift new file mode 100644 index 0000000000..6314db545c --- /dev/null +++ b/TelegramUI/PresentationCallManager.swift @@ -0,0 +1,202 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +private enum CurrentCall { + case none + case incomingRinging(CallSessionRingingState) + case ongoing(CallSession, OngoingCallContext) + + var internalId: CallSessionInternalId? { + switch self { + case .none: + return nil + case let .incomingRinging(ringingState): + return ringingState.id + case let .ongoing(session, _): + return session.id + } + } +} + +public enum RequestCallResult { + case requested + case alreadyInProgress(PeerId) +} + +public final class PresentationCallManager { + private let postbox: Postbox + private let audioSession: ManagedAudioSession + private let callSessionManager: CallSessionManager + private let callKitIntegration: CallKitIntegration? + + private var currentCall: PresentationCall? + private let removeCurrentCallDisposable = MetaDisposable() + + private var ringingStatesDisposable: Disposable? + + private let hasActiveCallsPromise = ValuePromise(false, ignoreRepeated: true) + public var hasActiveCalls: Signal { + return self.hasActiveCallsPromise.get() + } + + private let currentCallPromise = Promise(nil) + public var currentCallSignal: Signal { + return self.currentCallPromise.get() + } + + private let startCallDisposable = MetaDisposable() + + public init(postbox: Postbox, audioSession: ManagedAudioSession, callSessionManager: CallSessionManager) { + self.postbox = postbox + self.audioSession = audioSession + self.callSessionManager = callSessionManager + + var startCallImpl: ((UUID, String) -> Signal)? + var answerCallImpl: ((UUID) -> Void)? + var endCallImpl: ((UUID) -> Signal)? + var audioSessionActivationChangedImpl: ((Bool) -> Void)? + + self.callKitIntegration = CallKitIntegration(startCall: { uuid, handle in + if let startCallImpl = startCallImpl { + return startCallImpl(uuid, handle) + } else { + return .single(false) + } + }, answerCall: { uuid in + answerCallImpl?(uuid) + }, endCall: { uuid in + if let endCallImpl = endCallImpl { + return endCallImpl(uuid) + } else { + return .single(false) + } + }, audioSessionActivationChanged: { value in + audioSessionActivationChangedImpl?(value) + }) + + self.ringingStatesDisposable = (callSessionManager.ringingStates() |> mapToSignal { ringingStates -> Signal<[(Peer, CallSessionRingingState)], NoError> in + if ringingStates.isEmpty { + return .single([]) + } else { + return postbox.modify { modifier -> [(Peer, CallSessionRingingState)] in + var result: [(Peer, CallSessionRingingState)] = [] + for state in ringingStates { + if let peer = modifier.getPeer(state.peerId) { + result.append((peer, state)) + } + } + return result + } + } + } + |> deliverOnMainQueue).start(next: { [weak self] ringingStates in + self?.ringingStatesUpdated(ringingStates) + }) + + startCallImpl = { [weak self] uuid, handle in + if let strongSelf = self, let userId = Int32(handle) { + return strongSelf.startCall(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), internalId: uuid) + |> take(1) + |> map { _ -> Bool in + return true + } + } else { + return .single(false) + } + } + + answerCallImpl = { [weak self] uuid in + if let strongSelf = self { + strongSelf.currentCall?.answer() + } + } + + endCallImpl = { [weak self] uuid in + if let strongSelf = self, let currentCall = strongSelf.currentCall { + return currentCall.hangUp() + } else { + return .single(false) + } + } + + audioSessionActivationChangedImpl = { [weak self] value in + if value { + self?.audioSession.callKitActivatedAudioSession() + } else { + self?.audioSession.callKitDeactivatedAudioSession() + } + } + } + + deinit { + self.ringingStatesDisposable?.dispose() + self.removeCurrentCallDisposable.dispose() + self.startCallDisposable.dispose() + } + + private func ringingStatesUpdated(_ ringingStates: [(Peer, CallSessionRingingState)]) { + if let firstState = ringingStates.first { + if self.currentCall == nil { + let call = PresentationCall(audioSession: self.audioSession, callSessionManager: self.callSessionManager, callKitIntegration: self.callKitIntegration, internalId: firstState.1.id, peerId: firstState.1.peerId, isOutgoing: false, peer: firstState.0) + self.currentCall = call + self.currentCallPromise.set(.single(call)) + self.hasActiveCallsPromise.set(true) + self.removeCurrentCallDisposable.set((call.canBeRemoved + |> deliverOnMainQueue).start(next: { [weak self, weak call] value in + if value, let strongSelf = self, let call = call { + if strongSelf.currentCall === call { + strongSelf.currentCall = nil + strongSelf.currentCallPromise.set(.single(nil)) + strongSelf.hasActiveCallsPromise.set(false) + } + } + })) + } + } + } + + public func requestCall(peerId: PeerId, endCurrentIfAny: Bool) -> RequestCallResult { + if let call = self.currentCall, !endCurrentIfAny { + return .alreadyInProgress(call.peerId) + } + if let _ = self.callKitIntegration { + startCallDisposable.set((postbox.loadedPeerWithId(peerId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] peer in + if let strongSelf = self { + strongSelf.callKitIntegration?.startCall(peerId: peerId, displayTitle: peer.displayTitle) + } + })) + } else { + let _ = self.startCall(peerId: peerId).start() + } + return .requested + } + + private func startCall(peerId: PeerId, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal { + return (self.callSessionManager.request(peerId: peerId, internalId: internalId) |> deliverOnMainQueue |> beforeNext { [weak self] internalId in + if let strongSelf = self { + if let currentCall = strongSelf.currentCall { + currentCall.rejectBusy() + } + let call = PresentationCall(audioSession: strongSelf.audioSession, callSessionManager: strongSelf.callSessionManager, callKitIntegration: strongSelf.callKitIntegration, internalId: internalId, peerId: peerId, isOutgoing: true, peer: nil) + strongSelf.currentCall = call + strongSelf.currentCallPromise.set(.single(call)) + strongSelf.hasActiveCallsPromise.set(true) + strongSelf.removeCurrentCallDisposable.set((call.canBeRemoved + |> deliverOnMainQueue).start(next: { [weak call] value in + if value, let strongSelf = self, let call = call { + if strongSelf.currentCall === call { + strongSelf.currentCall = nil + strongSelf.currentCallPromise.set(.single(nil)) + strongSelf.hasActiveCallsPromise.set(false) + } + } + })) + + } + }) |> mapToSignal { _ -> Signal in return .single(true) } + } +} diff --git a/TelegramUI/PresentationData.swift b/TelegramUI/PresentationData.swift index cae4e8ec4c..9eeac5a576 100644 --- a/TelegramUI/PresentationData.swift +++ b/TelegramUI/PresentationData.swift @@ -1,5 +1,127 @@ import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore -final class PresentationData { +public final class PresentationData: Equatable { + public let strings: PresentationStrings + public let theme: PresentationTheme + public let chatWallpaper: TelegramWallpaper + public init(strings: PresentationStrings, theme: PresentationTheme, chatWallpaper: TelegramWallpaper) { + self.strings = strings + self.theme = theme + self.chatWallpaper = chatWallpaper + } + + public static func ==(lhs: PresentationData, rhs: PresentationData) -> Bool { + return lhs.strings === rhs.strings && lhs.theme == rhs.theme && lhs.chatWallpaper == rhs.chatWallpaper + } +} + +private func dictFromLocalization(_ value: Localization) -> [String: String] { + var dict: [String: String] = [:] + for entry in value.entries { + switch entry { + case let .string(key, value): + dict[key] = value + case let .pluralizedString(key, zero, one, two, few, many, other): + if let zero = zero { + dict["\(key)_zero"] = zero + } + if let one = one { + dict["\(key)_1"] = one + } + if let two = two { + dict["\(key)_2"] = two + } + if let few = few { + dict["\(key)_3_10"] = few + } + if let many = many { + dict["\(key)_many"] = many + } + dict["\(key)_any"] = other + } + } + return dict +} + +public func currentPresentationData(postbox: Postbox) -> Signal { + return postbox.modify { modifier -> (PresentationThemeSettings, LocalizationSettings?) in + let themeSettings: PresentationThemeSettings + if let current = modifier.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings) as? PresentationThemeSettings { + themeSettings = current + } else { + themeSettings = PresentationThemeSettings.defaultSettings + } + + let localizationSettings: LocalizationSettings? + if let current = modifier.getPreferencesEntry(key: PreferencesKeys.localizationSettings) as? LocalizationSettings { + localizationSettings = current + } else { + localizationSettings = nil + } + + return (themeSettings, localizationSettings) + } |> map { (themeSettings, localizationSettings) -> PresentationData in + let themeValue: PresentationTheme + switch themeSettings.theme { + case let .builtin(reference): + switch reference { + case .light: + themeValue = defaultPresentationTheme + case .dark: + themeValue = defaultDarkPresentationTheme + } + } + let stringsValue: PresentationStrings + if let localizationSettings = localizationSettings { + stringsValue = PresentationStrings(languageCode: localizationSettings.languageCode, dict: dictFromLocalization(localizationSettings.localization)) + } else { + stringsValue = defaultPresentationStrings + } + return PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: themeSettings.chatWallpaper) + } +} + +private var first = true + +public func updatedPresentationData(postbox: Postbox) -> Signal { + let preferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.presentationThemeSettings, PreferencesKeys.localizationSettings])) + return postbox.combinedView(keys: [preferencesKey]) + |> map { view -> PresentationData in + let themeSettings: PresentationThemeSettings + if let current = (view.views[preferencesKey] as! PreferencesView).values[ApplicationSpecificPreferencesKeys.presentationThemeSettings] as? PresentationThemeSettings { + themeSettings = current + } else { + themeSettings = PresentationThemeSettings.defaultSettings + } + let themeValue: PresentationTheme + switch themeSettings.theme { + case let .builtin(reference): + switch reference { + case .light: + themeValue = defaultPresentationTheme + case .dark: + themeValue = defaultDarkPresentationTheme + } + } + + let localizationSettings: LocalizationSettings? + if let current = (view.views[preferencesKey] as! PreferencesView).values[PreferencesKeys.localizationSettings] as? LocalizationSettings { + localizationSettings = current + } else { + localizationSettings = nil + } + + let stringsValue: PresentationStrings + if let localizationSettings = localizationSettings { + stringsValue = PresentationStrings(languageCode: localizationSettings.languageCode, dict: dictFromLocalization(localizationSettings.localization)) + } else { + stringsValue = defaultPresentationStrings + } + + return PresentationData(strings: stringsValue, theme: themeValue, chatWallpaper: themeSettings.chatWallpaper) + } } diff --git a/TelegramUI/PresentationPasscodeSettings.swift b/TelegramUI/PresentationPasscodeSettings.swift index a06e10243b..f1bb88a00a 100644 --- a/TelegramUI/PresentationPasscodeSettings.swift +++ b/TelegramUI/PresentationPasscodeSettings.swift @@ -16,8 +16,8 @@ public struct PresentationPasscodeSettings: PreferencesEntry, Equatable { } public init(decoder: Decoder) { - self.enableBiometrics = (decoder.decodeInt32ForKey("b") as Int32) != 0 - self.autolockTimeout = decoder.decodeInt32ForKey("al") as Int32? + self.enableBiometrics = decoder.decodeInt32ForKey("b", orElse: 0) != 0 + self.autolockTimeout = decoder.decodeOptionalInt32ForKey("al") } public func encode(_ encoder: Encoder) { diff --git a/TelegramUI/PresentationResourceKey.swift b/TelegramUI/PresentationResourceKey.swift new file mode 100644 index 0000000000..9454fba022 --- /dev/null +++ b/TelegramUI/PresentationResourceKey.swift @@ -0,0 +1,127 @@ +import Foundation + +struct PresentationResources { +} + +enum PresentationResourceKey: Int32 { + case rootNavigationIndefiniteActivity + + case rootTabContactsIcon + case rootTabContactsSelectedIcon + case rootTabChatsIcon + case rootTabChatsSelectedIcon + case rootTabSettingsIcon + case rootTabSettingsSelectedIcon + + case navigationComposeIcon + case navigationCallIcon + case navigationPlayerCloseButton + + case navigationPlayerPlayIcon + case navigationPlayerPauseIcon + case navigationPlayerMaximizedPlayIcon + case navigationPlayerMaximizedPauseIcon + case navigationPlayerMaximizedPreviousIcon + case navigationPlayerMaximizedNextIcon + case navigationPlayerMaximizedShuffleIcon + case navigationPlayerMaximizedRepeatIcon + case navigationPlayerHandleIcon + + case navigationDropdownArrowImage + + case itemListDisclosureArrow + case itemListCheckIcon + case itemListPlusIcon + + case itemListStickerItemUnreadDot + + case chatListLockTopLockedImage + case chatListLockBottomLockedImage + case chatListLockTopUnlockedImage + case chatListLockBottomUnlockedImage + case chatListSingleCheck + case chatListDoubleCheck + case chatListBadgeBackgroundActive + case chatListBadgeBackgroundInactive + + case chatPrincipalThemeEssentialGraphics + case chatBubbleVerticalLineIncomingImage + case chatBubbleVerticalLineOutgoingImage + + case chatBubbleCheckBubbleFullImage + case chatBubbleBubblePartialImage + case checkBubbleMediaFullImage + case checkBubbleMediaPartialImage + + case chatBubbleRadialIndicatorFileIconIncoming + case chatBubbleRadialIndicatorFileIconOutgoing + + case chatBubbleConsumableContentIncomingIcon + case chatBubbleConsumableContentOutgoingIcon + + case chatBubbleShareButtonImage + + case chatBubbleMediaOverlayControlSecret + + case chatServiceBubbleFillImage + + case chatBubbleSecretMediaIcon + + case chatFreeformContentAdditionalInfoBackgroundImage + + case chatInstantVideoBackgroundImage + case chatUnreadBarBackgroundImage + + case chatBubbleActionButtonMiddleImage + case chatBubbleActionButtonBottomLeftImage + case chatBubbleActionButtonBottomRightImage + case chatBubbleActionButtonBottomSingleImage + + case chatInfoItemBackgroundImage + case chatEmptyItemBackgroundImage + case chatEmptyItemIconImage + + case chatInputPanelCloseIconImage + case chatInputPanelVerticalSeparatorLineImage + + case chatMediaInputPanelHighlightedIconImage + case chatInputMediaPanelRecentStickersIconImage + case chatInputMediaPanelRecentGifsIconImage + + case chatInputButtonPanelButtonImage + case chatInputButtonPanelButtonHighlightedImage + + case chatInputTextFieldBackgroundImage + case chatInputTextFieldClearImage + case chatInputPanelSendButtonImage + case chatInputPanelVoiceButtonImage + case chatInputPanelAttachmentButtonImage + case chatInputPanelMediaRecordingDotImage + case chatInputPanelMediaRecordingCancelArrowImage + case chatInputTextFieldStickersImage + case chatInputTextFieldInputButtonsImage + case chatInputTextFieldKeyboardImage + case chatInputTextFieldTimerImage + + case chatInputSearchPanelUpImage + case chatInputSearchPanelUpDisabledImage + case chatInputSearchPanelDownImage + case chatInputSearchPanelDownDisabledImage + case chatInputSearchPanelCalendarImage + + case chatHistoryNavigationButtonImage + case chatHistoryNavigationButtonBadgeImage + + case sharedMediaFileDownloadStartIcon + case sharedMediaFileDownloadPauseIcon + + case chatInfoCallButtonImage + + case chatInstantMessageMuteIconImage + + case chatBubbleIncomingCallButtonImage + case chatBubbleOutgoingCallButtonImage + + case callListOutgoingIcon + case callListInfoButton +} diff --git a/TelegramUI/PresentationResourcesCallList.swift b/TelegramUI/PresentationResourcesCallList.swift new file mode 100644 index 0000000000..980b9af496 --- /dev/null +++ b/TelegramUI/PresentationResourcesCallList.swift @@ -0,0 +1,16 @@ +import Foundation +import Display + +struct PresentationResourcesCallList { + static func outgoingIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.callListOutgoingIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call List/OutgoingIcon"), color: theme.list.disclosureArrowColor) + }) + } + + static func infoButton(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.callListInfoButton.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Call List/InfoButton"), color: theme.list.itemAccentColor) + }) + } +} diff --git a/TelegramUI/PresentationResourcesChat.swift b/TelegramUI/PresentationResourcesChat.swift new file mode 100644 index 0000000000..0dac941320 --- /dev/null +++ b/TelegramUI/PresentationResourcesChat.swift @@ -0,0 +1,480 @@ +import Foundation +import Display + +private func generateLineImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 2.0, height: 3.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 1.0), size: CGSize(width: 2.0, height: 2.0))) + })?.stretchableImage(withLeftCapWidth: 0, topCapHeight: 1) +} + +private func generateInstantVideoBackground(fillColor: UIColor, strokeColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 214.0, height: 214.0), rotatedContext: { size, context in + let lineWidth: CGFloat = 0.5 + + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(strokeColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setFillColor(fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: lineWidth, y: lineWidth), size: CGSize(width: size.width - lineWidth * 2.0, height: size.height - lineWidth * 2.0))) + }) +} + +private func generateInputPanelButtonBackgroundImage(fillColor: UIColor, strokeColor: UIColor) -> UIImage? { + let radius: CGFloat = 5.0 + let shadowSize: CGFloat = 1.0 + return generateImage(CGSize(width: radius * 2.0, height: radius * 2.0 + shadowSize), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(strokeColor.cgColor) + context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: radius * 2.0, height: radius * 2.0)) + context.setFillColor(fillColor.cgColor) + context.fillEllipse(in: CGRect(x: 0.0, y: shadowSize, width: radius * 2.0, height: radius * 2.0)) + })?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)) +} + +struct PresentationResourcesChat { + static func principalGraphics(_ theme: PresentationTheme) -> PrincipalThemeEssentialGraphics { + return theme.object(PresentationResourceKey.chatPrincipalThemeEssentialGraphics.rawValue, { theme in + return PrincipalThemeEssentialGraphics(theme.chat) + }) as! PrincipalThemeEssentialGraphics + } + + static func chatBubbleVerticalLineIncomingImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleVerticalLineIncomingImage.rawValue, { theme in + return generateLineImage(color: theme.chat.bubble.incomingAccentColor) + }) + } + + static func chatBubbleVerticalLineOutgoingImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleVerticalLineOutgoingImage.rawValue, { theme in + return generateLineImage(color: theme.chat.bubble.outgoingAccentColor) + }) + } + + static func chatBubbleRadialIndicatorFileIconIncoming(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleRadialIndicatorFileIconIncoming.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocumentIncoming"), color: theme.chat.bubble.incomingFillColor) + }) + } + + static func chatBubbleRadialIndicatorFileIconOutgoing(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleRadialIndicatorFileIconOutgoing.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocumentIncoming"), color: theme.chat.bubble.outgoingFillColor) + }) + } + + static func chatBubbleConsumableContentIncomingIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleConsumableContentIncomingIcon.rawValue, { theme in + return generateFilledCircleImage(diameter: 4.0, color: theme.chat.bubble.incomingAccentColor) + }) + } + + static func chatBubbleConsumableContentOutgoingIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleConsumableContentOutgoingIcon.rawValue, { theme in + return generateFilledCircleImage(diameter: 4.0, color: theme.chat.bubble.outgoingAccentColor) + }) + } + + static func chatBubbleShareButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleShareButtonImage.rawValue, { theme in + return generateImage(CGSize(width: 29.0, height: 29.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chat.bubble.shareButtonFillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + if let image = UIImage(bundleImageName: "Chat/Message/ShareIcon") { + let imageRect = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size) + + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + context.clip(to: imageRect, mask: image.cgImage!) + context.setFillColor(theme.chat.bubble.shareButtonForegroundColor.cgColor) + context.fill(imageRect) + } + }) + }) + } + + static func chatServiceBubbleFillImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatServiceBubbleFillImage.rawValue, { theme in + return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context -> Void in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chat.serviceMessage.serviceMessageFillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8) + }) + } + + static func chatBubbleSecretMediaIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleSecretMediaIcon.rawValue, { theme in + generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: theme.chat.bubble.mediaOverlayControlForegroundColor) + }) + } + + static func chatFreeformContentAdditionalInfoBackgroundImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatFreeformContentAdditionalInfoBackgroundImage.rawValue, { theme in + return generateStretchableFilledCircleImage(radius: 4.0, color: theme.chat.serviceMessage.serviceMessageFillColor) + }) + } + + static func chatInstantVideoBackgroundImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInstantVideoBackgroundImage.rawValue, { theme in + return generateInstantVideoBackground(fillColor: theme.chat.bubble.freeformFillColor, strokeColor: theme.chat.bubble.freeformStrokeColor) + }) + } + + static func chatUnreadBarBackgroundImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatUnreadBarBackgroundImage.rawValue, { theme in + return generateImage(CGSize(width: 1.0, height: 8.0), contextGenerator: { size, context -> Void in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chat.serviceMessage.unreadBarStrokeColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: UIScreenPixel))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + context.setFillColor(theme.chat.serviceMessage.unreadBarFillColor.cgColor) + context.fill(CGRect(x: 0.0, y: UIScreenPixel, width: size.width, height: size.height - UIScreenPixel - UIScreenPixel)) + })?.stretchableImage(withLeftCapWidth: 1, topCapHeight: 4) + }) + } + + static func chatBubbleActionButtonMiddleImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonMiddleImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsFillColor, position: .middle) + }) + } + + static func chatBubbleActionButtonBottomLeftImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonBottomLeftImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsFillColor, position: .bottomLeft) + }) + } + + static func chatBubbleActionButtonBottomRightImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonBottomRightImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsFillColor, position: .bottomRight) + }) + } + + static func chatBubbleActionButtonBottomSingleImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleActionButtonBottomSingleImage.rawValue, { theme in + return messageBubbleActionButtonImage(color: theme.chat.bubble.actionButtonsFillColor, position: .bottomSingle) + }) + } + + static func chatInfoItemBackgroundImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInfoItemBackgroundImage.rawValue, { theme in + return messageSingleBubbleLikeImage(fillColor: theme.chat.bubble.infoFillColor, strokeColor: theme.chat.bubble.infoStrokeColor) + }) + } + + static func chatEmptyItemBackgroundImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatEmptyItemBackgroundImage.rawValue, { theme in + return generateStretchableFilledCircleImage(radius: 14.0, color: theme.chat.serviceMessage.serviceMessageFillColor) + }) + } + + static func chatEmptyItemIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatEmptyItemIconImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/EmptyChatIcon"), color: theme.chat.serviceMessage.serviceMessagePrimaryTextColor) + }) + } + + static func chatInputPanelCloseIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelCloseIconImage.rawValue, { theme in + return generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.chat.inputPanel.panelControlColor.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.move(to: CGPoint(x: 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) + context.strokePath() + }) + }) + } + + static func chatInputPanelVerticalSeparatorLineImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelVerticalSeparatorLineImage.rawValue, { theme in + return generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: theme.chat.inputPanel.panelControlAccentColor) + }) + } + + static func chatMediaInputPanelHighlightedIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatMediaInputPanelHighlightedIconImage.rawValue, { theme in + return generateStretchableFilledCircleImage(radius: 9.0, color: theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor) + }) + } + + static func chatInputMediaPanelRecentStickersIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputMediaPanelRecentStickersIconImage.rawValue, { theme in + return generateImage(CGSize(width: 26.0, height: 26.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.chat.inputMediaPanel.panelIconColor.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + let diameter: CGFloat = 22.0 + context.strokeEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - diameter) / 2.0), y: floor((size.width - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) + context.translateBy(x: 1.5, y: 2.5) + context.move(to: CGPoint(x: 11.0, y: 5.5)) + context.addLine(to: CGPoint(x: 11.0, y: 11.0)) + context.addLine(to: CGPoint(x: 14.5, y: 14.5)) + context.strokePath() + }) + }) + } + + static func chatInputMediaPanelRecentGifsIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputMediaPanelRecentGifsIconImage.rawValue, { theme in + return generateImage(CGSize(width: 26.0, height: 26.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.chat.inputMediaPanel.panelIconColor.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + let diameter: CGFloat = 22.0 + context.strokeEllipse(in: CGRect(origin: CGPoint(x: floor((size.width - diameter) / 2.0), y: floor((size.width - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter))) + context.setFillColor(theme.chat.inputMediaPanel.panelIconColor.cgColor) + UIGraphicsPushContext(context) + + context.setTextDrawingMode(.stroke) + context.setLineWidth(0.65) + + ("GIF" as NSString).draw(in: CGRect(origin: CGPoint(x: 6.0, y: 8.0), size: size), withAttributes: [NSFontAttributeName: Font.regular(8.0), NSForegroundColorAttributeName: theme.chat.inputMediaPanel.panelIconColor]) + + context.setTextDrawingMode(.fill) + context.setLineWidth(0.8) + + ("GIF" as NSString).draw(in: CGRect(origin: CGPoint(x: 6.0, y: 8.0), size: size), withAttributes: [NSFontAttributeName: Font.regular(8.0), NSForegroundColorAttributeName: theme.chat.inputMediaPanel.panelIconColor]) + UIGraphicsPopContext() + }) + }) + } + + static func chatInputButtonPanelButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputButtonPanelButtonImage.rawValue, { theme in + return generateInputPanelButtonBackgroundImage(fillColor: theme.chat.inputButtonPanel.buttonFillColor, strokeColor: theme.chat.inputButtonPanel.buttonStrokeColor) + }) + } + + static func chatInputButtonPanelButtonHighlightedImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputButtonPanelButtonHighlightedImage.rawValue, { theme in + return generateInputPanelButtonBackgroundImage(fillColor: theme.chat.inputButtonPanel.buttonHighlightedFillColor, strokeColor: theme.chat.inputButtonPanel.buttonHighlightedStrokeColor) + }) + } + + static func chatInputTextFieldBackgroundImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputTextFieldBackgroundImage.rawValue, { theme in + let diameter: CGFloat = 35.0 + UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), true, 0.0) + let context = UIGraphicsGetCurrentContext()! + context.setFillColor(theme.chat.inputPanel.panelBackgroundColor.cgColor) + context.fill(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) + context.setFillColor(theme.chat.inputPanel.inputBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) + context.setStrokeColor(theme.chat.inputPanel.inputStrokeColor.cgColor) + let strokeWidth: CGFloat = 0.5 + context.setLineWidth(strokeWidth) + context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: diameter - strokeWidth, height: diameter - strokeWidth)) + let image = UIGraphicsGetImageFromCurrentImageContext()!.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) + UIGraphicsEndImageContext() + + return image + }) + } + + static func chatInputTextFieldClearImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputTextFieldClearImage.rawValue, { theme in + return generateImage(CGSize(width: 14.0, height: 14.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chat.inputPanel.inputControlColor.cgColor) + context.setStrokeColor(theme.chat.inputPanel.inputBackgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(1.5) + context.setLineCap(.round) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.rotate(by: CGFloat.pi / 4.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + let lineHeight: CGFloat = 7.0 + + context.beginPath() + context.move(to: CGPoint(x: size.width / 2.0, y: (size.width - lineHeight) / 2.0)) + context.addLine(to: CGPoint(x: size.width / 2.0, y: (size.width - lineHeight) / 2.0 + lineHeight)) + context.strokePath() + + context.beginPath() + context.move(to: CGPoint(x: (size.width - lineHeight) / 2.0, y: size.width / 2.0)) + context.addLine(to: CGPoint(x: (size.width - lineHeight) / 2.0 + lineHeight, y: size.width / 2.0)) + context.strokePath() + }) + }) + } + + static func chatInputPanelSendButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelSendButtonImage.rawValue, { theme in + return UIImage(bundleImageName: "Chat/Input/Text/IconSend") + }) + } + + static func chatInputPanelVoiceButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelVoiceButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: theme.chat.inputPanel.panelControlColor) + }) + } + + static func chatInputPanelAttachmentButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelAttachmentButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconAttachment"), color: theme.chat.inputPanel.panelControlColor) + }) + } + + static func chatInputPanelMediaRecordingDotImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelMediaRecordingDotImage.rawValue, { theme in + return generateFilledCircleImage(diameter: 9.0, color: theme.chat.inputPanel.mediaRecordingDotColor) + }) + } + + static func chatInputPanelMediaRecordingCancelArrowImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputPanelMediaRecordingCancelArrowImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AudioRecordingCancelArrow"), color: theme.chat.inputPanel.panelControlColor) + }) + } + + static func chatInputTextFieldStickersImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputTextFieldStickersImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconStickers"), color: theme.chat.inputPanel.inputControlColor) + }) + } + + static func chatInputTextFieldInputButtonsImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputTextFieldInputButtonsImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconInputButtons"), color: theme.chat.inputPanel.inputControlColor) + }) + } + + static func chatInputTextFieldKeyboardImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputTextFieldKeyboardImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconKeyboard"), color: theme.chat.inputPanel.inputControlColor) + }) + } + + static func chatInputTextFieldTimerImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputTextFieldTimerImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconTimer"), color: theme.chat.inputPanel.inputControlColor) + }) + } + + static func chatHistoryNavigationButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatHistoryNavigationButtonImage.rawValue, { theme in + return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chat.historyNavigation.fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.5, y: 0.5), size: CGSize(width: size.width - 1.0, height: size.height - 1.0))) + context.setLineWidth(0.5) + context.setStrokeColor(theme.chat.historyNavigation.strokeColor.cgColor) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: 0.25, y: 0.25), size: CGSize(width: size.width - 0.5, height: size.height - 0.5))) + context.setStrokeColor(theme.chat.historyNavigation.foregroundColor.cgColor) + context.setLineWidth(1.5) + + let position = CGPoint(x: 9.0 - 0.5, y: 23.0) + context.move(to: CGPoint(x: position.x + 1.0, y: position.y - 1.0)) + context.addLine(to: CGPoint(x: position.x + 10.0, y: position.y - 10.0)) + context.addLine(to: CGPoint(x: position.x + 19.0, y: position.y - 1.0)) + context.strokePath() + }) + }) + } + + static func chatHistoryNavigationButtonBadgeImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatHistoryNavigationButtonBadgeImage.rawValue, { theme in + return generateStretchableFilledCircleImage(diameter: 18.0, color: theme.chat.historyNavigation.badgeBackgroundColor, backgroundColor: nil) + }) + } + + static func sharedMediaFileDownloadStartIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.sharedMediaFileDownloadStartIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "List Menu/ListDownloadStartIcon"), color: theme.list.itemAccentColor) + }) + } + + static func sharedMediaFileDownloadPauseIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.sharedMediaFileDownloadPauseIcon.rawValue, { theme in + return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(theme.list.itemAccentColor.cgColor) + + context.fill(CGRect(x: 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0)) + context.fill(CGRect(x: 2.0 + 2.0 + 2.0, y: 0.0, width: 2.0, height: 11.0 - 1.0)) + }) + }) + } + + static func chatInfoCallButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInfoCallButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Info/CallButton"), color: theme.list.itemAccentColor) + }) + } + + static func chatInstantMessageMuteIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInstantMessageMuteIconImage.rawValue, { theme in + return generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.chat.bubble.mediaDateAndStatusFillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/InstantVideoMute"), color: .white, backgroundColor: nil) { + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)) + } + }) + }) + } + + static func chatBubbleIncomingCallButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleIncomingCallButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Info/CallButton"), color: theme.chat.bubble.incomingAccentColor) + }) + } + + static func chatBubbleOutgoingCallButtonImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatBubbleOutgoingCallButtonImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Info/CallButton"), color: theme.chat.bubble.outgoingAccentColor) + }) + } + + static func chatInputSearchPanelUpImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputSearchPanelUpImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/UpButton"), color: theme.chat.inputPanel.panelControlAccentColor) + }) + } + + static func chatInputSearchPanelUpDisabledImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputSearchPanelUpDisabledImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/UpButton"), color: theme.chat.inputPanel.panelControlDisabledColor) + }) + } + + static func chatInputSearchPanelDownImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputSearchPanelDownImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: theme.chat.inputPanel.panelControlAccentColor) + }) + } + + static func chatInputSearchPanelDownDisabledImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputSearchPanelDownDisabledImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: theme.chat.inputPanel.panelControlDisabledColor) + }) + } + + static func chatInputSearchPanelCalendarImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatInputSearchPanelCalendarImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/Calendar"), color: theme.chat.inputPanel.panelControlAccentColor) + }) + } +} diff --git a/TelegramUI/PresentationResourcesChatList.swift b/TelegramUI/PresentationResourcesChatList.swift new file mode 100644 index 0000000000..6082dfa7e7 --- /dev/null +++ b/TelegramUI/PresentationResourcesChatList.swift @@ -0,0 +1,85 @@ +import Foundation +import Display + +private func generateStatusCheckImage(theme: PresentationTheme, single: Bool) -> UIImage? { + return generateImage(CGSize(width: single ? 13.0 : 18.0, height: 13.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0 + 1.0, y: -size.height / 2.0 + 1.0) + + context.scaleBy(x: 0.5, y: 0.5) + context.setStrokeColor(theme.chatList.checkmarkColor.cgColor) + context.setLineWidth(2.8) + if single { + let _ = try? drawSvgPath(context, path: "M0,12 L6.75230742,19.080349 L22.4821014,0.277229071 ") + } else { + let _ = try? drawSvgPath(context, path: "M0,12 L6.75230742,19.080349 L22.4821014,0.277229071 ") + let _ = try? drawSvgPath(context, path: "M13.4492402,16.500967 L15.7523074,18.8031199 L31.4821014,0 ") + } + context.strokePath() + }) +} + +private func generateBadgeBackgroundImage(theme: PresentationTheme, active: Bool) -> UIImage? { + return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if active { + context.setFillColor(theme.chatList.unreadBadgeActiveBackgroundColor.cgColor) + } else { + context.setFillColor(theme.chatList.unreadBadgeInactiveBackgroundColor.cgColor) + } + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10) +} + +struct PresentationResourcesChatList { + static func singleCheckImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListSingleCheck.rawValue, { theme in + return generateStatusCheckImage(theme: theme, single: true) + }) + } + + static func doubleCheckImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListDoubleCheck.rawValue, { theme in + return generateStatusCheckImage(theme: theme, single: false) + }) + } + + static func lockTopLockedImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListLockTopLockedImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/LockLockedTop"), color: theme.rootController.navigationBar.accentTextColor) + }) + } + + static func lockBottomLockedImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListLockBottomLockedImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/LockLockedBottom"), color: theme.rootController.navigationBar.accentTextColor) + }) + } + + static func lockTopUnlockedImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListLockTopUnlockedImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/LockUnlockedTop"), color: theme.rootController.navigationBar.primaryTextColor) + }) + } + + static func lockBottomUnlockedImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListLockBottomUnlockedImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/LockUnlockedBottom"), color: theme.rootController.navigationBar.primaryTextColor) + }) + } + + static func badgeBackgroundActive(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListBadgeBackgroundActive.rawValue, { theme in + return generateBadgeBackgroundImage(theme: theme, active: true) + }) + } + + static func badgeBackgroundInactive(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListBadgeBackgroundInactive.rawValue, { theme in + return generateBadgeBackgroundImage(theme: theme, active: false) + }) + } +} diff --git a/TelegramUI/PresentationResourcesItemList.swift b/TelegramUI/PresentationResourcesItemList.swift new file mode 100644 index 0000000000..b757b592d2 --- /dev/null +++ b/TelegramUI/PresentationResourcesItemList.swift @@ -0,0 +1,48 @@ +import Foundation +import Display + +private func generateArrowImage(_ theme: PresentationTheme) -> UIImage? { + return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/DisclosureArrow"), color: theme.list.disclosureArrowColor) +} + +private func generateCheckIcon(_ theme: PresentationTheme) -> UIImage? { + return generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.list.itemAccentColor.cgColor) + context.setLineWidth(2.0) + context.move(to: CGPoint(x: 12.0, y: 1.0)) + context.addLine(to: CGPoint(x: 4.16482734, y: 9.0)) + context.addLine(to: CGPoint(x: 1.0, y: 5.81145833)) + context.strokePath() + }) +} + +private func generatePlusIcon(_ theme: PresentationTheme) -> UIImage? { + return generateImage(CGSize(width: 18.0, height: 18.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.list.itemAccentColor.cgColor) + let lineWidth = min(1.5, UIScreenPixel * 4.0) + context.fill(CGRect(x: floorToScreenPixels((18.0 - lineWidth) / 2.0), y: 0.0, width: lineWidth, height: 18.0)) + context.fill(CGRect(x: 0.0, y: floorToScreenPixels((18.0 - lineWidth) / 2.0), width: 18.0, height: lineWidth)) + }) +} + +struct PresentationResourcesItemList { + static func disclosureArrowImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListDisclosureArrow.rawValue, generateArrowImage) + } + + static func checkIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListCheckIcon.rawValue, generateCheckIcon) + } + + static func plusIconImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListPlusIcon.rawValue, generatePlusIcon) + } + + static func stickerUnreadDotImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListStickerItemUnreadDot.rawValue, { theme in + return generateFilledCircleImage(diameter: 6.0, color: theme.list.itemAccentColor) + }) + } +} diff --git a/TelegramUI/PresentationResourcesRootController.swift b/TelegramUI/PresentationResourcesRootController.swift new file mode 100644 index 0000000000..b5d87be2c0 --- /dev/null +++ b/TelegramUI/PresentationResourcesRootController.swift @@ -0,0 +1,145 @@ +import Foundation +import Display + +private func generateComposeButtonImage(theme: PresentationTheme) -> UIImage? { + return generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.rootController.navigationBar.accentTextColor.cgColor) + try? drawSvgPath(context, path: "M0,4 L15,4 L14,5 L1,5 L1,22 L18,22 L18,9 L19,8 L19,23 L0,23 L0,4 Z M18.5944456,1.70209754 L19.5995507,2.70718758 L10.0510517,12.255543 L9.54849908,13.7631781 L11.0561568,13.2606331 L20.6046559,3.71227763 L21.6097611,4.71736767 L11.5587094,14.7682681 L7.53828874,15.7733582 L9.04594649,11.250453 L18.5944456,1.70209754 Z M19.0969982,1.19955251 L20.0773504,0.21921503 C20.3690844,-0.0725145755 20.8398084,-0.0729335627 21.1298838,0.217137419 L23.0947435,2.18196761 C23.3833646,2.47058439 23.3838887,2.94326675 23.0926659,3.23448517 L22.1123136,4.21482265 L19.0969982,1.19955251 Z ") + }) +} + +struct PresentationResourcesRootController { + static func navigationIndefiniteActivityImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.rootNavigationIndefiniteActivity.rawValue, { theme in + return generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.rootController.navigationBar.accentTextColor.cgColor) + let _ = try? drawSvgPath(context, path: "M11,22 C17.0751322,22 22,17.0751322 22,11 C22,4.92486775 17.0751322,0 11,0 C4.92486775,0 0,4.92486775 0,11 C0,12.4564221 0.28362493,13.8747731 0.827833595,15.1935223 C1.00609922,15.6255031 1.50080164,15.8311798 1.93278238,15.6529142 C2.36476311,15.4746485 2.57043984,14.9799461 2.39217421,14.5479654 C1.93209084,13.4330721 1.69230769,12.233965 1.69230769,11 C1.69230769,5.85950348 5.85950348,1.69230769 11,1.69230769 C16.1404965,1.69230769 20.3076923,5.85950348 20.3076923,11 C20.3076923,16.1404965 16.1404965,20.3076923 11,20.3076923 C10.5326821,20.3076923 10.1538462,20.6865283 10.1538462,21.1538462 C10.1538462,21.621164 10.5326821,22 11,22 Z ") + }) + }) + } + + static func tabContactsIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.rootTabContactsIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: theme.rootController.tabBar.iconColor) + }) + } + + static func tabContactsSelectedIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.rootTabContactsSelectedIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContactsSelected"), color: theme.rootController.tabBar.selectedIconColor) + }) + } + + static func tabChatsIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.rootTabChatsIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconChats"), color: theme.rootController.tabBar.iconColor) + }) + } + + static func tabChatsSelectedIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.rootTabChatsSelectedIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconChatsSelected"), color: theme.rootController.tabBar.selectedIconColor) + }) + } + + static func tabSettingsIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.rootTabSettingsIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconSettings"), color: theme.rootController.tabBar.iconColor) + }) + } + + static func tabSettingsSelectedIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.rootTabSettingsSelectedIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconSettingsSelected"), color: theme.rootController.tabBar.selectedIconColor) + }) + } + + static func navigationComposeIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationComposeIcon.rawValue, generateComposeButtonImage) + } + + static func navigationDropdownArrowImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationDropdownArrowImage.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/TitleViewModeSelectionArrow"), color: theme.rootController.navigationBar.accentTextColor) + }) + } + + static func navigationCallIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationCallIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Info/CallButton"), color: theme.rootController.navigationBar.accentTextColor) + }) + } + + static func navigationPlayerCloseButton(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationPlayerCloseButton.rawValue, { theme in + return generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(theme.rootController.navigationBar.controlColor.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.move(to: CGPoint(x: 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) + context.strokePath() + }) + }) + } + + static func navigationPlayerPlayIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationPlayerPlayIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPlay"), color: theme.rootController.navigationBar.primaryTextColor) + }) + } + + static func navigationPlayerPauseIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationPlayerPauseIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/MinimizedPause"), color: theme.rootController.navigationBar.primaryTextColor) + }) + } + + static func navigationPlayerMaximizedPlayIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationPlayerMaximizedPlayIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Play"), color: theme.rootController.navigationBar.primaryTextColor) + }) + } + + static func navigationPlayerMaximizedPauseIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationPlayerMaximizedPauseIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Pause"), color: theme.rootController.navigationBar.primaryTextColor) + }) + } + + static func navigationPlayerMaximizedPreviousIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationPlayerMaximizedPreviousIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: theme.rootController.navigationBar.primaryTextColor) + }) + } + + static func navigationPlayerMaximizedNextIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationPlayerMaximizedNextIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: theme.rootController.navigationBar.primaryTextColor) + }) + } + + static func navigationPlayerMaximizedShuffleIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationPlayerMaximizedShuffleIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Shuffle"), color: theme.rootController.navigationBar.primaryTextColor) + }) + } + + static func navigationPlayerMaximizedRepeatIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationPlayerMaximizedRepeatIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: theme.rootController.navigationBar.primaryTextColor) + }) + } + + static func navigationPlayerHandleIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.navigationPlayerHandleIcon.rawValue, { theme in + return generateStretchableFilledCircleImage(diameter: 7.0, color: theme.rootController.navigationBar.controlColor) + }) + } +} diff --git a/TelegramUI/PresentationStrings.swift b/TelegramUI/PresentationStrings.swift new file mode 100644 index 0000000000..38a9002d5f --- /dev/null +++ b/TelegramUI/PresentationStrings.swift @@ -0,0 +1,7211 @@ +import Foundation + +private func getValue(_ dict: [String: String], _ key: String) -> String { + if let value = dict[key] { + return value + } else { + return key + } +} + +private extension PluralizationForm { + var canonicalSuffix: String { + switch self { + case .zero: + return "_many" + case .one: + return "_one" + case .two: + return "_two" + case .few: + return "_1_3" + case .many: + return "_any" + case .other: + return "_any" + } + } +} +private func getValueWithForm(_ dict: [String: String], _ key: String, _ form: PluralizationForm) -> String { + if let value = dict[key + form.canonicalSuffix] { + return value + } + return key +} + +private let argumentRegex = try! NSRegularExpression(pattern: "%(((\\d+)\\$)?)([@df])", options: []) +private func extractArgumentRanges(_ value: String) -> [(Int, NSRange)] { + var result: [(Int, NSRange)] = [] + let string = value as NSString + let matches = argumentRegex.matches(in: string as String, options: [], range: NSRange(location: 0, length: string.length)) + var index = 0 + for match in matches { + var currentIndex = index + if match.rangeAt(3).location != NSNotFound { + currentIndex = Int(string.substring(with: match.rangeAt(3)))! - 1 + } + result.append((currentIndex, match.rangeAt(0))) + index += 1 + } + result.sort(by: { $0.1.location < $1.1.location }) + return result +} + +func formatWithArgumentRanges(_ value: String, _ ranges: [(Int, NSRange)], _ arguments: [String]) -> (String, [(Int, NSRange)]) { + let string = value as NSString + + var resultingRanges: [(Int, NSRange)] = [] + + var currentLocation = 0 + + let result = NSMutableString() + for (index, range) in ranges { + if currentLocation < range.location { + result.append(string.substring(with: NSRange(location: currentLocation, length: range.location - currentLocation))) + } + resultingRanges.append((index, NSRange(location: result.length, length: (arguments[index] as NSString).length))) + result.append(arguments[index]) + currentLocation = range.location + range.length + } + if currentLocation != string.length { + result.append(string.substring(with: NSRange(location: currentLocation, length: string.length - currentLocation))) + } + return (result as String, resultingRanges) +} +public final class PresentationStrings { + private let lc: UInt32 + + public let languageCode: String + + public let Channel_BanUser_Title: String + public let Preview_SaveGif: String + public let EnterPasscode_EnterNewPasscodeNew: String + public let Privacy_Calls_WhoCanCallMe: String + public let Watch_NoConnection: String + private let _Group_Username_LinkHint: String + private let _Group_Username_LinkHint_r: [(Int, NSRange)] + public func Group_Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Group_Username_LinkHint, self._Group_Username_LinkHint_r, [_0]) + } + public let Activity_UploadingPhoto: String + public let PrivacySettings_PrivacyTitle: String + private let _DialogList_PinLimitError: String + private let _DialogList_PinLimitError_r: [(Int, NSRange)] + public func DialogList_PinLimitError(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_PinLimitError, self._DialogList_PinLimitError_r, [_0]) + } + public let Settings_LogoutError: String + public let Cache_ClearCache: String + public let Common_Close: String + public let ChangePhoneNumberCode_Called: String + public let Login_PhoneTitle: String + private let _Cache_Clear: String + private let _Cache_Clear_r: [(Int, NSRange)] + public func Cache_Clear(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Cache_Clear, self._Cache_Clear_r, [_0]) + } + public let EnterPasscode_EnterNewPasscodeChange: String + public let Watch_ChatList_Compose: String + public let DialogList_SearchSectionDialogs: String + public let Contacts_TabTitle: String + public let TwoStepAuth_SetupPasswordConfirmPassword: String + public let ChannelIntro_Text: String + public let PrivacySettings_SecurityTitle: String + private let _Login_SmsRequestState1: String + private let _Login_SmsRequestState1_r: [(Int, NSRange)] + public func Login_SmsRequestState1(_ _0: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_SmsRequestState1, self._Login_SmsRequestState1_r, ["\(_0)"]) + } + public let Conversation_Download: String + private let _Call_StatusOngoing: String + private let _Call_StatusOngoing_r: [(Int, NSRange)] + public func Call_StatusOngoing(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Call_StatusOngoing, self._Call_StatusOngoing_r, [_0]) + } + public let Settings_LogoutConfirmationText: String + public let BlockedUsers_Info: String + public let ChatSettings_AutomaticAudioDownload: String + public let Map_OpenInFoursquare: String + public let Privacy_Calls_CustomShareHelp: String + public let Group_MessagePhotoUpdated: String + public let Message_PinnedInvoice: String + public let Login_InfoAvatarAdd: String + public let WebSearch_RecentSectionTitle: String + private let _CHAT_MESSAGE_TEXT: String + private let _CHAT_MESSAGE_TEXT_r: [(Int, NSRange)] + public func CHAT_MESSAGE_TEXT(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_TEXT, self._CHAT_MESSAGE_TEXT_r, [_1, _2, _3]) + } + public let Message_Sticker: String + public let Channel_Management_Remove: String + public let Paint_Regular: String + public let Channel_Username_Help: String + private let _Profile_CreateEncryptedChatOutdatedError: String + private let _Profile_CreateEncryptedChatOutdatedError_r: [(Int, NSRange)] + public func Profile_CreateEncryptedChatOutdatedError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Profile_CreateEncryptedChatOutdatedError, self._Profile_CreateEncryptedChatOutdatedError_r, [_0, _1]) + } + public let Login_InactiveHelp: String + public let ChatSettings_Security: String + private let _Time_PreciseDate_9: String + private let _Time_PreciseDate_9_r: [(Int, NSRange)] + public func Time_PreciseDate_9(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_9, self._Time_PreciseDate_9_r, [_1, _2, _3]) + } + private let _PINNED_STICKER: String + private let _PINNED_STICKER_r: [(Int, NSRange)] + public func PINNED_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_STICKER, self._PINNED_STICKER_r, [_1, _2]) + } + public let Conversation_ShareInlineBotLocationConfirmation: String + private let _Channel_AdminLog_MessageEdited: String + private let _Channel_AdminLog_MessageEdited_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageEdited(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageEdited, self._Channel_AdminLog_MessageEdited_r, [_0]) + } + private let _PHONE_CALL_REQUEST: String + private let _PHONE_CALL_REQUEST_r: [(Int, NSRange)] + public func PHONE_CALL_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PHONE_CALL_REQUEST, self._PHONE_CALL_REQUEST_r, [_1]) + } + public let AccessDenied_MicrophoneRestricted: String + public let Your_cards_expiration_year_is_invalid: String + public let GroupInfo_InviteByLink: String + private let _Notification_LeftChat: String + private let _Notification_LeftChat_r: [(Int, NSRange)] + public func Notification_LeftChat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_LeftChat, self._Notification_LeftChat_r, [_0]) + } + private let _Channel_AdminLog_MessageAdmin: String + private let _Channel_AdminLog_MessageAdmin_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageAdmin(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageAdmin, self._Channel_AdminLog_MessageAdmin_r, [_0, _1, _2]) + } + public let PrivacyLastSeenSettings_NeverShareWith_Placeholder: String + public let TwoStepAuth_SetupEmail: String + public let Login_ResetAccountProtected_Reset: String + private let _MESSAGE_CONTACT: String + private let _MESSAGE_CONTACT_r: [(Int, NSRange)] + public func MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_CONTACT, self._MESSAGE_CONTACT_r, [_1]) + } + private let _Group_Management_ErrorNotMember: String + private let _Group_Management_ErrorNotMember_r: [(Int, NSRange)] + public func Group_Management_ErrorNotMember(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Group_Management_ErrorNotMember, self._Group_Management_ErrorNotMember_r, [_0]) + } + public let MediaPicker_MomentsDateRangeSameMonthYearFormat: String + public let Notification_MessageLifetime1w: String + public let PasscodeSettings_AutoLock_IfAwayFor_5minutes: String + public let ChatSettings_Groups: String + public let State_Connecting: String + private let _Message_ForwardedMessageShort: String + private let _Message_ForwardedMessageShort_r: [(Int, NSRange)] + public func Message_ForwardedMessageShort(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Message_ForwardedMessageShort, self._Message_ForwardedMessageShort_r, [_0]) + } + public let Watch_ConnectionDescription: String + private let _Notification_CallTimeFormat: String + private let _Notification_CallTimeFormat_r: [(Int, NSRange)] + public func Notification_CallTimeFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_CallTimeFormat, self._Notification_CallTimeFormat_r, [_1, _2]) + } + public let Paint_Delete: String + public let Channel_MessagePhotoUpdated: String + public let SharedMedia_All: String + public let Cache_Help: String + private let _Login_EmailPhoneBody: String + private let _Login_EmailPhoneBody_r: [(Int, NSRange)] + public func Login_EmailPhoneBody(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_EmailPhoneBody, self._Login_EmailPhoneBody_r, [_0]) + } + public let Checkout_ShippingAddress: String + public let Channel_BanList_RestrictedTitle: String + public let Checkout_TotalAmount: String + public let Conversation_MessageEditedLabel: String + public let SharedMedia_EmptyLinksText: String + public let Channel_Members_Kick: String + public let GoogleDrive_FolderIsEmpty: String + public let Calls_NoCallsPlaceholder: String + public let Message_PinnedDeletedMessage: String + public let Conversation_PinMessageAlert_OnlyPin: String + public let ReportPeer_ReasonOther_Send: String + public let Conversation_InstantPagePreview: String + public let PasscodeSettings_SimplePasscodeHelp: String + public let Group_ErrorAddTooMuch: String + public let GroupInfo_Title: String + public let State_Updating: String + public let StickerSettings_ContextShow: String + public let Map_GetDirections: String + private let _TwoStepAuth_PendingEmailHelp: String + private let _TwoStepAuth_PendingEmailHelp_r: [(Int, NSRange)] + public func TwoStepAuth_PendingEmailHelp(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_TwoStepAuth_PendingEmailHelp, self._TwoStepAuth_PendingEmailHelp_r, [_0]) + } + public let UserInfo_PhoneCall: String + public let MusicPlayer_VoiceNote: String + public let Paint_Duplicate: String + public let Channel_Username_InvalidTaken: String + private let _Profile_ShareContactGroupFormat: String + private let _Profile_ShareContactGroupFormat_r: [(Int, NSRange)] + public func Profile_ShareContactGroupFormat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Profile_ShareContactGroupFormat, self._Profile_ShareContactGroupFormat_r, [_0]) + } + public let SecretChat_Title: String + public let Group_UpgradeConfirmation: String + public let Checkout_LiabilityAlertTitle: String + public let GroupInfo_GroupNamePlaceholder: String + public let Conversation_InfoBroadcastList: String + private let _Notification_JoinedGroupByLink: String + private let _Notification_JoinedGroupByLink_r: [(Int, NSRange)] + public func Notification_JoinedGroupByLink(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_JoinedGroupByLink, self._Notification_JoinedGroupByLink_r, [_0]) + } + private let _Time_PreciseDate_8: String + private let _Time_PreciseDate_8_r: [(Int, NSRange)] + public func Time_PreciseDate_8(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_8, self._Time_PreciseDate_8_r, [_1, _2, _3]) + } + public let Login_HaveNotReceivedCodeInternal: String + public let LoginPassword_Title: String + public let Conversation_PlayVideo: String + public let PasscodeSettings_SimplePasscode: String + public let Conversation_MicrophoneAccessDisabled: String + public let NewContact_Title: String + public let Username_CheckingUsername: String + public let Login_ResetAccountProtected_TimerTitle: String + public let UserInfo_InviteBotToGroup: String + public let Checkout_Email: String + public let CheckoutInfo_SaveInfo: String + private let _ChangePhoneNumberCode_CallTimer: String + private let _ChangePhoneNumberCode_CallTimer_r: [(Int, NSRange)] + public func ChangePhoneNumberCode_CallTimer(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_ChangePhoneNumberCode_CallTimer, self._ChangePhoneNumberCode_CallTimer_r, [_0]) + } + public let TwoStepAuth_SetupPasswordEnterPasswordNew: String + public let Weekday_Wednesday: String + private let _Channel_AdminLog_MessageToggleSignaturesOff: String + private let _Channel_AdminLog_MessageToggleSignaturesOff_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageToggleSignaturesOff(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageToggleSignaturesOff, self._Channel_AdminLog_MessageToggleSignaturesOff_r, [_0]) + } + public let Month_ShortDecember: String + public let Channel_SignMessages: String + public let Conversation_Moderate_Delete: String + public let Conversation_CloudStorage_ChatStatus: String + public let Login_InfoTitle: String + public let Privacy_GroupsAndChannels_NeverAllow_Placeholder: String + public let Message_Video: String + public let Notification_ChannelInviterSelf: String + private let _VideoPreview_OptionSD: String + private let _VideoPreview_OptionSD_r: [(Int, NSRange)] + public func VideoPreview_OptionSD(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_VideoPreview_OptionSD, self._VideoPreview_OptionSD_r, [_0]) + } + public let Conversation_SecretLinkPreviewAlert: String + public let Channel_AdminLog_BanEmbedLinks: String + public let Cache_Videos: String + public let NetworkUsageSettings_MediaImageDataSection: String + public let TwoStepAuth_GenericHelp: String + private let _DialogList_SingleRecordingAudioSuffix: String + private let _DialogList_SingleRecordingAudioSuffix_r: [(Int, NSRange)] + public func DialogList_SingleRecordingAudioSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_SingleRecordingAudioSuffix, self._DialogList_SingleRecordingAudioSuffix_r, [_0]) + } + public let Checkout_NewCard_CardholderNameTitle: String + public let Settings_FAQ_Button: String + private let _GroupInfo_AddParticipantConfirmation: String + private let _GroupInfo_AddParticipantConfirmation_r: [(Int, NSRange)] + public func GroupInfo_AddParticipantConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_GroupInfo_AddParticipantConfirmation, self._GroupInfo_AddParticipantConfirmation_r, [_0]) + } + public let AccessDenied_PhotosRestricted: String + public let Map_Locating: String + private let _SearchImages_Downloading_Kb: String + private let _SearchImages_Downloading_Kb_r: [(Int, NSRange)] + public func SearchImages_Downloading_Kb(_ _0: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_SearchImages_Downloading_Kb, self._SearchImages_Downloading_Kb_r, ["\(_0)"]) + } + private let _Profile_ShareBotPersonFormat: String + private let _Profile_ShareBotPersonFormat_r: [(Int, NSRange)] + public func Profile_ShareBotPersonFormat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Profile_ShareBotPersonFormat, self._Profile_ShareBotPersonFormat_r, [_0]) + } + public let SearchImages_SearchImages: String + public let SharedMedia_EmptyMusicText: String + public let Cache_ByPeerHeader: String + public let Bot_GroupStatusReadsHistory: String + public let TwoStepAuth_ResetAccountConfirmation: String + public let CallSettings_Always: String + public let SearchImages_DownloadCancelled: String + public let Settings_LogoutConfirmationTitle: String + public let UserInfo_FirstNamePlaceholder: String + public let ChatSettings_AutoPlayAudio: String + public let LoginPassword_ResetAccount: String + public let Privacy_GroupsAndChannels_AlwaysAllow: String + private let _Notification_JoinedChat: String + private let _Notification_JoinedChat_r: [(Int, NSRange)] + public func Notification_JoinedChat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_JoinedChat, self._Notification_JoinedChat_r, [_0]) + } + public let ChannelInfo_DeleteChannel: String + public let NetworkUsageSettings_BytesReceived: String + public let BlockedUsers_BlockTitle: String + public let AccessDenied_PhotosAndVideos: String + public let Channel_Username_Title: String + private let _Channel_AdminLog_MessageToggleSignaturesOn: String + private let _Channel_AdminLog_MessageToggleSignaturesOn_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageToggleSignaturesOn(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageToggleSignaturesOn, self._Channel_AdminLog_MessageToggleSignaturesOn_r, [_0]) + } + private let _Conversation_EncryptionWaiting: String + private let _Conversation_EncryptionWaiting_r: [(Int, NSRange)] + public func Conversation_EncryptionWaiting(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_EncryptionWaiting, self._Conversation_EncryptionWaiting_r, [_0]) + } + public let Calls_NotNow: String + public let Conversation_Report: String + private let _CHANNEL_MESSAGE_DOC: String + private let _CHANNEL_MESSAGE_DOC_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_DOC, self._CHANNEL_MESSAGE_DOC_r, [_1]) + } + public let Channel_AdminLogFilter_EventsAll: String + public let Call_ConnectionErrorTitle: String + public let Settings_ChatSettings: String + public let Group_About_Help: String + private let _CHANNEL_MESSAGE_NOTEXT: String + private let _CHANNEL_MESSAGE_NOTEXT_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_NOTEXT, self._CHANNEL_MESSAGE_NOTEXT_r, [_1]) + } + public let Month_GenSeptember: String + public let PrivacySettings_LastSeenEverybody: String + public let PhotoEditor_BlurToolRadial: String + public let TwoStepAuth_PasswordRemoveConfirmation: String + public let Channel_EditAdmin_PermissionEditMessages: String + public let TwoStepAuth_ChangePassword: String + public let Watch_MessageView_Title: String + private let _Notification_PinnedRoundMessage: String + private let _Notification_PinnedRoundMessage_r: [(Int, NSRange)] + public func Notification_PinnedRoundMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedRoundMessage, self._Notification_PinnedRoundMessage_r, [_0]) + } + public let Conversation_DeleteGroup: String + private let _Time_PreciseDate_7: String + private let _Time_PreciseDate_7_r: [(Int, NSRange)] + public func Time_PreciseDate_7(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_7, self._Time_PreciseDate_7_r, [_1, _2, _3]) + } + public let Channel_Management_LabelCreator: String + private let _Notification_PinnedStickerMessage: String + private let _Notification_PinnedStickerMessage_r: [(Int, NSRange)] + public func Notification_PinnedStickerMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedStickerMessage, self._Notification_PinnedStickerMessage_r, [_0]) + } + public let Settings_SaveEditedPhotos: String + public let PhotoEditor_QualityTool: String + public let Login_NetworkError: String + public let TwoStepAuth_EnterPasswordForgot: String + public let Compose_ChannelMembers: String + public let Common_Yes: String + public let KeyCommand_JumpToPreviousUnreadChat: String + public let CheckoutInfo_ReceiverInfoPhone: String + public let GroupInfo_AddParticipantTitle: String + private let _CHANNEL_MESSAGE_TEXT: String + private let _CHANNEL_MESSAGE_TEXT_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_TEXT, self._CHANNEL_MESSAGE_TEXT_r, [_1, _2]) + } + public let Checkout_PayNone: String + public let CheckoutInfo_ErrorNameInvalid: String + public let Channel_Share: String + public let Notification_PaymentSent: String + public let Settings_Username: String + public let Notification_CallMissedShort: String + public let Call_CallInProgressTitle: String + public let PhotoEditor_Skip: String + public let AuthSessions_TerminateOtherSessionsHelp: String + public let Call_AudioRouteHeadphones: String + public let Contacts_InviteFriends: String + public let Channel_BanUser_PermissionSendMessages: String + public let Notifications_InAppNotificationsVibrate: String + public let StickerPack_Share: String + public let Watch_MessageView_Reply: String + public let Call_AudioRouteSpeaker: String + public let PrivacySettings_DeleteAccountNever: String + private let _WelcomeScreen_ContactsAccessHelp: String + private let _WelcomeScreen_ContactsAccessHelp_r: [(Int, NSRange)] + public func WelcomeScreen_ContactsAccessHelp(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_WelcomeScreen_ContactsAccessHelp, self._WelcomeScreen_ContactsAccessHelp_r, [_0]) + } + private let _MESSAGE_GEO: String + private let _MESSAGE_GEO_r: [(Int, NSRange)] + public func MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_GEO, self._MESSAGE_GEO_r, [_1]) + } + public let Checkout_Title: String + public let Privacy_Calls: String + public let Channel_AdminLogFilter_EventsInfo: String + private let _Channel_AdminLog_MessagePinned: String + private let _Channel_AdminLog_MessagePinned_r: [(Int, NSRange)] + public func Channel_AdminLog_MessagePinned(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessagePinned, self._Channel_AdminLog_MessagePinned_r, [_0]) + } + private let _Channel_AdminLog_MessageToggleInvitesOn: String + private let _Channel_AdminLog_MessageToggleInvitesOn_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageToggleInvitesOn(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageToggleInvitesOn, self._Channel_AdminLog_MessageToggleInvitesOn_r, [_0]) + } + public let Conversation_SearchWebImages: String + public let KeyCommand_ScrollDown: String + public let Conversation_LinkDialogSave: String + public let Presence_offline: String + public let Conversation_SendMessageErrorFlood: String + private let _Conversation_ForwardToPersonFormat: String + private let _Conversation_ForwardToPersonFormat_r: [(Int, NSRange)] + public func Conversation_ForwardToPersonFormat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_ForwardToPersonFormat, self._Conversation_ForwardToPersonFormat_r, [_0]) + } + public let CheckoutInfo_ErrorShippingNotAvailable: String + public let SharedMedia_Incoming: String + private let _Checkout_SavePasswordTimeoutAndTouchId: String + private let _Checkout_SavePasswordTimeoutAndTouchId_r: [(Int, NSRange)] + public func Checkout_SavePasswordTimeoutAndTouchId(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Checkout_SavePasswordTimeoutAndTouchId, self._Checkout_SavePasswordTimeoutAndTouchId_r, [_0]) + } + public let CheckoutInfo_ShippingInfoCountry: String + public let Map_ShowPlaces: String + public let Camera_VideoMode: String + private let _Watch_Time_ShortFullAt: String + private let _Watch_Time_ShortFullAt_r: [(Int, NSRange)] + public func Watch_Time_ShortFullAt(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Watch_Time_ShortFullAt, self._Watch_Time_ShortFullAt_r, [_1, _2]) + } + public let UserInfo_TelegramCall: String + public let PrivacyLastSeenSettings_CustomShareSettingsHelp: String + public let Channel_AdminLog_InfoPanelAlertText: String + public let Watch_State_WaitingForNetwork: String + public let Cache_Photos: String + public let Message_PinnedStickerMessage: String + public let PhotoEditor_QualityMedium: String + public let Privacy_PaymentsClearInfo: String + public let PhotoEditor_CurvesRed: String + public let Privacy_PaymentsTitle: String + public let Login_PhoneNumberHelp: String + public let User_DeletedAccount: String + public let Call_StatusFailed: String + private let _Notification_GroupInviter: String + private let _Notification_GroupInviter_r: [(Int, NSRange)] + public func Notification_GroupInviter(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_GroupInviter, self._Notification_GroupInviter_r, [_0]) + } + public let Localization_ChooseLanguage: String + public let CheckoutInfo_ShippingInfoAddress2Placeholder: String + private let _Notification_SecretChatMessageScreenshot: String + private let _Notification_SecretChatMessageScreenshot_r: [(Int, NSRange)] + public func Notification_SecretChatMessageScreenshot(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_SecretChatMessageScreenshot, self._Notification_SecretChatMessageScreenshot_r, [_0]) + } + private let _DialogList_SingleUploadingPhotoSuffix: String + private let _DialogList_SingleUploadingPhotoSuffix_r: [(Int, NSRange)] + public func DialogList_SingleUploadingPhotoSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_SingleUploadingPhotoSuffix, self._DialogList_SingleUploadingPhotoSuffix_r, [_0]) + } + public let Channel_LeaveChannel: String + public let Compose_NewGroup: String + public let TwoStepAuth_EmailPlaceholder: String + public let PhotoEditor_ExposureTool: String + public let ChatAdmins_AdminLabel: String + public let Contacts_FailedToSendInvitesMessage: String + public let Login_Code: String + public let Channel_Username_InvalidCharacters: String + public let Calls_CallTabTitle: String + public let FeatureDisabled_Oops: String + public let Login_InviteButton: String + public let ShareMenu_Send: String + public let Conversation_InfoGroup: String + public let WatchRemote_AlertTitle: String + public let Preview_ProfilePhotoTitle: String + public let Checkout_Phone: String + public let Channel_SignMessages_Help: String + public let Calls_SubmitRating: String + public let Camera_FlashOn: String + public let Watch_MessageView_Forward: String + private let _Time_PreciseDate_6: String + private let _Time_PreciseDate_6_r: [(Int, NSRange)] + public func Time_PreciseDate_6(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_6, self._Time_PreciseDate_6_r, [_1, _2, _3]) + } + public let DialogList_You: String + public let Weekday_Monday: String + public let Watch_Suggestion_Yes: String + public let AccessDenied_Camera: String + public let WatchRemote_NotificationText: String + public let Activity_Location: String + public let SharedMedia_ViewInChat: String + public let Activity_RecordingAudio: String + public let Watch_Stickers_StickerPacks: String + private let _Target_ShareGameConfirmationPrivate: String + private let _Target_ShareGameConfirmationPrivate_r: [(Int, NSRange)] + public func Target_ShareGameConfirmationPrivate(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Target_ShareGameConfirmationPrivate, self._Target_ShareGameConfirmationPrivate_r, [_0]) + } + public let Checkout_NewCard_PostcodePlaceholder: String + public let Conversation_SearchImages: String + public let DialogList_DeleteConversationConfirmation: String + public let AttachmentMenu_SendAsFile: String + public let Message_GamePreviewLabel: String + public let Checkout_ShippingOption_Header: String + public let Watch_Conversation_Unblock: String + public let Channel_AdminLog_MessagePreviousLink: String + public let CallSettings_PrivacyDescription: String + public let Conversation_ContextMenuCopy: String + public let GroupInfo_UpgradeButton: String + public let PrivacyLastSeenSettings_NeverShareWith: String + public let ConvertToSupergroup_HelpText: String + public let MediaPicker_VideoMuteDescription: String + private let _SearchImages_Downloading_Mb: String + private let _SearchImages_Downloading_Mb_r: [(Int, NSRange)] + public func SearchImages_Downloading_Mb(_ _0: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_SearchImages_Downloading_Mb, self._SearchImages_Downloading_Mb_r, ["\(_0)"]) + } + public let UserInfo_ShareMyContactInfo: String + private let _FileSize_GB: String + private let _FileSize_GB_r: [(Int, NSRange)] + public func FileSize_GB(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_FileSize_GB, self._FileSize_GB_r, [_0]) + } + public let Month_ShortJanuary: String + public let Channel_BanUser_PermissionsHeader: String + public let PhotoEditor_QualityVeryHigh: String + public let Login_TermsOfServiceLabel: String + private let _MESSAGE_TEXT: String + private let _MESSAGE_TEXT_r: [(Int, NSRange)] + public func MESSAGE_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_TEXT, self._MESSAGE_TEXT_r, [_1, _2]) + } + public let DialogList_NoMessagesTitle: String + public let AccessDenied_Contacts: String + public let Your_cards_security_code_is_invalid: String + public let Tour_StartButton: String + public let CheckoutInfo_Title: String + public let ChangePhoneNumberCode_Help: String + public let Web_Error: String + public let ShareFileTip_Title: String + public let Username_InvalidStartsWithNumber: String + public let ChatSettings_RevertLanguage: String + public let Conversation_ReportSpamAndLeave: String + private let _DialogList_EncryptedChatStartedIncoming: String + private let _DialogList_EncryptedChatStartedIncoming_r: [(Int, NSRange)] + public func DialogList_EncryptedChatStartedIncoming(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_EncryptedChatStartedIncoming, self._DialogList_EncryptedChatStartedIncoming_r, [_0]) + } + public let Calls_AddTab: String + public let ChannelMembers_WhoCanAddMembers_Admins: String + public let Tour_Text5: String + public let Watch_Stickers_RecentPlaceholder: String + public let Common_Select: String + private let _Notification_MessageLifetimeRemoved: String + private let _Notification_MessageLifetimeRemoved_r: [(Int, NSRange)] + public func Notification_MessageLifetimeRemoved(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_MessageLifetimeRemoved, self._Notification_MessageLifetimeRemoved_r, [_1]) + } + private let _PINNED_INVOICE: String + private let _PINNED_INVOICE_r: [(Int, NSRange)] + public func PINNED_INVOICE(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_INVOICE, self._PINNED_INVOICE_r, [_1]) + } + public let Month_GenFebruary: String + public let Contacts_SelectAll: String + public let Month_GenOctober: String + public let CheckoutInfo_ErrorPhoneInvalid: String + public let SharedMedia_TitleVideo: String + public let Checkout_PaymentMethod_New: String + public let ShareMenu_Comment: String + public let Channel_Management_LabelEditor: String + public let TwoStepAuth_SetPasswordHelp: String + public let Channel_AdminLogFilter_EventsTitle: String + public let Username_LinkCopied: String + public let DialogList_Conversations: String + public let Channel_EditAdmin_PermissionAddAdmins: String + public let Conversation_SendMessage: String + public let Notification_CallIncoming: String + private let _MESSAGE_FWDS: String + private let _MESSAGE_FWDS_r: [(Int, NSRange)] + public func MESSAGE_FWDS(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_FWDS, self._MESSAGE_FWDS_r, [_1, _2]) + } + private let _Time_PreciseDate_5: String + private let _Time_PreciseDate_5_r: [(Int, NSRange)] + public func Time_PreciseDate_5(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_5, self._Time_PreciseDate_5_r, [_1, _2, _3]) + } + public let Conversation_InputTextCommentPlaceholder: String + public let Map_OpenInYandexMaps: String + public let Month_ShortNovember: String + public let AccessDenied_Settings: String + public let EncryptionKey_Title: String + public let Profile_MessageLifetime1h: String + private let _Map_DistanceAway: String + private let _Map_DistanceAway_r: [(Int, NSRange)] + public func Map_DistanceAway(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Map_DistanceAway, self._Map_DistanceAway_r, [_0]) + } + public let Compose_NewMessage: String + public let Checkout_ErrorPaymentFailed: String + public let Map_OpenInWaze: String + public let Common_ChooseVideo: String + public let Checkout_ShippingMethod: String + public let Login_InfoFirstNamePlaceholder: String + public let DialogList_Broadcast: String + public let Checkout_ErrorProviderAccountInvalid: String + public let CallSettings_TabIconDescription: String + public let Checkout_WebConfirmation_Title: String + public let PasscodeSettings_AutoLock: String + public let Notifications_MessageNotificationsPreview: String + public let Conversation_BlockUser: String + public let MessageTimer_Custom: String + public let Conversation_SilentBroadcastTooltipOff: String + public let Conversation_Mute: String + public let Call_CallBack: String + public let CreateGroup_SoftUserLimitAlert: String + public let AccessDenied_LocationDenied: String + public let Tour_Title6: String + public let Settings_UsernameEmpty: String + public let PrivacySettings_TwoStepAuth: String + public let Conversation_FileICloudDrive: String + public let KeyCommand_SendMessage: String + private let _Channel_AdminLog_MessageDeleted: String + private let _Channel_AdminLog_MessageDeleted_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageDeleted(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageDeleted, self._Channel_AdminLog_MessageDeleted_r, [_0]) + } + public let DialogList_DeleteBotConfirmation: String + public let Common_TakePhotoOrVideo: String + public let Notification_MessageLifetime2s: String + public let Checkout_ErrorGeneric: String + public let Conversation_FileGoogleDrive: String + private let _MediaPicker_Processing: String + private let _MediaPicker_Processing_r: [(Int, NSRange)] + public func MediaPicker_Processing(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MediaPicker_Processing, self._MediaPicker_Processing_r, [_0]) + } + public let Channel_AdminLog_CanBanUsers: String + public let Cache_Indexing: String + private let _ENCRYPTION_REQUEST: String + private let _ENCRYPTION_REQUEST_r: [(Int, NSRange)] + public func ENCRYPTION_REQUEST(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_ENCRYPTION_REQUEST, self._ENCRYPTION_REQUEST_r, [_1]) + } + public let StickerSettings_ContextInfo: String + public let Message_SharedContact: String + public let Channel_BanUser_PermissionEmbedLinks: String + public let Channel_Username_CreateCommentsEnabled: String + public let GroupInfo_InviteLink_LinkSection: String + public let Privacy_Calls_AlwaysAllow_Placeholder: String + public let CheckoutInfo_ShippingInfoPostcode: String + public let PasscodeSettings_EncryptDataHelp: String + public let KeyCommand_FocusOnInputField: String + public let Cache_KeepMedia: String + public let WebPreview_GettingLinkInfo: String + public let Group_Setup_TypePublicHelp: String + public let Channel_Moderator_AccessLevelModeratorHelp: String + public let Map_Satellite: String + public let Username_InvalidTaken: String + private let _Notification_PinnedAudioMessage: String + private let _Notification_PinnedAudioMessage_r: [(Int, NSRange)] + public func Notification_PinnedAudioMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedAudioMessage, self._Notification_PinnedAudioMessage_r, [_0]) + } + public let Notification_MessageLifetime1d: String + public let Profile_MessageLifetime2s: String + private let _TwoStepAuth_RecoveryEmailUnavailable: String + private let _TwoStepAuth_RecoveryEmailUnavailable_r: [(Int, NSRange)] + public func TwoStepAuth_RecoveryEmailUnavailable(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_TwoStepAuth_RecoveryEmailUnavailable, self._TwoStepAuth_RecoveryEmailUnavailable_r, [_0]) + } + public let Calls_RatingFeedback: String + public let Profile_EncryptionKey: String + public let Watch_Suggestion_WhatsUp: String + public let LoginPassword_PasswordPlaceholder: String + public let TwoStepAuth_EnterPasswordPassword: String + private let _CHANNEL_MESSAGE_CONTACT: String + private let _CHANNEL_MESSAGE_CONTACT_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_CONTACT, self._CHANNEL_MESSAGE_CONTACT_r, [_1]) + } + public let PrivacySettings_DeleteAccountHelp: String + private let _Time_PreciseDate_4: String + private let _Time_PreciseDate_4_r: [(Int, NSRange)] + public func Time_PreciseDate_4(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_4, self._Time_PreciseDate_4_r, [_1, _2, _3]) + } + public let Channel_Info_Banned: String + public let Conversation_ShareBotContactConfirmationTitle: String + public let ConversationProfile_UsersTooMuchError: String + public let ChatAdmins_AllMembersAreAdminsOffHelp: String + public let Privacy_GroupsAndChannels_WhoCanAddMe: String + public let Settings_PhoneNumber: String + public let Login_CodeExpiredError: String + private let _DialogList_MultipleTypingSuffix: String + private let _DialogList_MultipleTypingSuffix_r: [(Int, NSRange)] + public func DialogList_MultipleTypingSuffix(_ _0: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_MultipleTypingSuffix, self._DialogList_MultipleTypingSuffix_r, ["\(_0)"]) + } + public let ChannelMembers_Blacklist_EmptyText: String + public let Bot_GenericBotStatus: String + public let Common_edit: String + public let Settings_AppLanguage: String + public let PrivacyLastSeenSettings_WhoCanSeeMyTimestamp: String + private let _Notification_Kicked: String + private let _Notification_Kicked_r: [(Int, NSRange)] + public func Notification_Kicked(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_Kicked, self._Notification_Kicked_r, [_0, _1]) + } + public let Conversation_Send: String + public let ChannelInfo_DeleteChannelConfirmation: String + public let Weekday_ShortSaturday: String + public let Map_SendThisLocation: String + public let DialogList_RecentTitleBots: String + private let _Notification_PinnedDocumentMessage: String + private let _Notification_PinnedDocumentMessage_r: [(Int, NSRange)] + public func Notification_PinnedDocumentMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedDocumentMessage, self._Notification_PinnedDocumentMessage_r, [_0]) + } + public let Conversation_ContextMenuReply: String + public let Channel_BanUser_PermissionSendMedia: String + public let NetworkUsageSettings_Wifi: String + public let Call_Accept: String + public let GroupInfo_SetGroupPhotoDelete: String + public let PhotoEditor_CropAuto: String + public let PhotoEditor_ContrastTool: String + public let MediaPicker_MomentsDateYearFormat: String + public let CheckoutInfo_ReceiverInfoNamePlaceholder: String + public let Privacy_PaymentsClear_ShippingInfo: String + public let TwoStepAuth_GenericError: String + public let Channel_Moderator_AccessLevelEditorHelp: String + public let Compose_NewChannelButton: String + public let ConversationMedia_EmptyTitle: String + public let Date_DialogDateFormat: String + public let ReportPeer_ReasonSpam: String + public let Compose_TokenListPlaceholder: String + private let _PINNED_VIDEO: String + private let _PINNED_VIDEO_r: [(Int, NSRange)] + public func PINNED_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_VIDEO, self._PINNED_VIDEO_r, [_1]) + } + public let StickerPacksSettings_Title: String + public let Privacy_Calls_NeverAllow_Placeholder: String + public let Settings_Support: String + public let Notification_GroupInviterSelf: String + public let MaskStickerSettings_Title: String + public let Watch_Suggestion_ThankYou: String + public let TwoStepAuth_SetPassword: String + public let GoogleDrive_LoadErrorMessage: String + public let GroupInfo_InviteLink_ShareLink: String + public let ChannelMembers_AllMembersMayInviteOnHelp: String + public let Common_Cancel: String + public let Preview_LoadingImages: String + public let ChangePhoneNumberCode_RequestingACall: String + public let PrivacyLastSeenSettings_NeverShareWith_Title: String + public let KeyCommand_JumpToNextChat: String + public let Tour_Text1: String + public let StickerPack_Remove: String + public let Conversation_HoldForVideo: String + public let Checkout_NewCard_Title: String + public let Channel_TitleInfo: String + public let Settings_About_Help: String + private let _Time_PreciseDate_3: String + private let _Time_PreciseDate_3_r: [(Int, NSRange)] + public func Time_PreciseDate_3(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_PreciseDate_3, self._Time_PreciseDate_3_r, [_1, _2, _3]) + } + public let Watch_Conversation_Reply: String + public let ShareMenu_CopyShareLink: String + public let Channel_Setup_TypePrivateHelp: String + public let PhotoEditor_GrainTool: String + public let Watch_Suggestion_TalkLater: String + public let TwoStepAuth_ChangeEmail: String + private let _ENCRYPTION_ACCEPT: String + private let _ENCRYPTION_ACCEPT_r: [(Int, NSRange)] + public func ENCRYPTION_ACCEPT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_ENCRYPTION_ACCEPT, self._ENCRYPTION_ACCEPT_r, [_1]) + } + public let Conversation_ShareBotLocationConfirmationTitle: String + public let NetworkUsageSettings_BytesSent: String + public let Conversation_ForwardContacts: String + private let _Notification_ChangedGroupName: String + private let _Notification_ChangedGroupName_r: [(Int, NSRange)] + public func Notification_ChangedGroupName(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_ChangedGroupName, self._Notification_ChangedGroupName_r, [_0, _1]) + } + private let _MESSAGE_VIDEO: String + private let _MESSAGE_VIDEO_r: [(Int, NSRange)] + public func MESSAGE_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_VIDEO, self._MESSAGE_VIDEO_r, [_1]) + } + private let _Checkout_PayPrice: String + private let _Checkout_PayPrice_r: [(Int, NSRange)] + public func Checkout_PayPrice(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Checkout_PayPrice, self._Checkout_PayPrice_r, [_0]) + } + private let _Notification_PinnedTextMessage: String + private let _Notification_PinnedTextMessage_r: [(Int, NSRange)] + public func Notification_PinnedTextMessage(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedTextMessage, self._Notification_PinnedTextMessage_r, [_0, _1]) + } + public let GroupInfo_InvitationLinkDoesNotExist: String + public let ReportPeer_ReasonOther_Placeholder: String + public let PasscodeSettings_AutoLock_Disabled: String + public let Wallpaper_Title: String + public let Watch_Compose_CreateMessage: String + public let Message_Audio: String + public let Notification_CreatedGroup: String + public let Conversation_SearchNoResults: String + public let ChannelMembers_BanList_EmptyText: String + public let ReportPeer_ReasonViolence: String + public let Group_Username_RemoveExistingUsernamesInfo: String + public let Message_InvoiceLabel: String + private let _LastSeen_AtWeekday: String + private let _LastSeen_AtWeekday_r: [(Int, NSRange)] + public func LastSeen_AtWeekday(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_LastSeen_AtWeekday, self._LastSeen_AtWeekday_r, [_0]) + } + public let Contacts_SearchLabel: String + public let Group_Username_InvalidStartsWithNumber: String + public let Channel_AdminLogFilter_Title: String + public let ChatAdmins_AllMembersAreAdminsOnHelp: String + public let Month_ShortSeptember: String + public let Group_Username_CreatePublicLinkHelp: String + public let Login_CallRequestState2: String + public let TwoStepAuth_RecoveryUnavailable: String + public let Bot_Unblock: String + public let SharedMedia_CategoryMedia: String + public let Conversation_HoldForAudio: String + public let Conversation_ClousStorageInfo_Description1: String + public let Channel_Members_InviteLink: String + public let WebSearch_RecentClearConfirmation: String + public let Core_ServiceUserStatus: String + public let Notification_ChannelMigratedFrom: String + public let Settings_Title: String + public let Call_StatusBusy: String + public let ArchivedPacksAlert_Title: String + public let ConversationMedia_Title: String + private let _Conversation_MessageViaUser: String + private let _Conversation_MessageViaUser_r: [(Int, NSRange)] + public func Conversation_MessageViaUser(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_MessageViaUser, self._Conversation_MessageViaUser_r, [_0]) + } + public let Presence_invisible: String + public let DialogList_Create: String + public let Tour_Title4: String + public let Call_StatusEnded: String + public let Conversation_UnpinMessageAlert: String + private let _Conversation_MessageDialogRetryAll: String + private let _Conversation_MessageDialogRetryAll_r: [(Int, NSRange)] + public func Conversation_MessageDialogRetryAll(_ _1: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_MessageDialogRetryAll, self._Conversation_MessageDialogRetryAll_r, ["\(_1)"]) + } + private let _Checkout_PasswordEntry_Text: String + private let _Checkout_PasswordEntry_Text_r: [(Int, NSRange)] + public func Checkout_PasswordEntry_Text(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Checkout_PasswordEntry_Text, self._Checkout_PasswordEntry_Text_r, [_0]) + } + public let Call_Message: String + public let Contacts_MemberSearchSectionTitleGroup: String + private let _Conversation_BotInteractiveUrlAlert: String + private let _Conversation_BotInteractiveUrlAlert_r: [(Int, NSRange)] + public func Conversation_BotInteractiveUrlAlert(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_BotInteractiveUrlAlert, self._Conversation_BotInteractiveUrlAlert_r, [_0]) + } + public let GroupInfo_SharedMedia: String + public let Channel_Username_InvalidStartsWithNumber: String + public let KeyCommand_JumpToPreviousChat: String + public let Conversation_Call: String + public let KeyCommand_ScrollUp: String + private let _Privacy_GroupsAndChannels_InviteToChannelError: String + private let _Privacy_GroupsAndChannels_InviteToChannelError_r: [(Int, NSRange)] + public func Privacy_GroupsAndChannels_InviteToChannelError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Privacy_GroupsAndChannels_InviteToChannelError, self._Privacy_GroupsAndChannels_InviteToChannelError_r, [_0, _1]) + } + public let Document_TargetConfirmationFormat: String + public let Group_Setup_TypeHeader: String + private let _DialogList_SinglePlayingGameSuffix: String + private let _DialogList_SinglePlayingGameSuffix_r: [(Int, NSRange)] + public func DialogList_SinglePlayingGameSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_SinglePlayingGameSuffix, self._DialogList_SinglePlayingGameSuffix_r, [_0]) + } + public let AttachmentMenu_SendAsFiles: String + public let Profile_MessageLifetime1m: String + public let DialogList_SelectContact: String + public let Settings_AppleWatch: String + public let Conversation_View: String + public let Contacts_Invite: String + public let Channel_AdminLog_MessagePreviousDescription: String + public let Your_card_was_declined: String + public let PhoneNumberHelp_ChangeNumber: String + public let ReportPeer_ReasonPornography: String + public let Notification_CreatedChannel: String + public let PhotoEditor_Original: String + public let Target_SelectGroup: String + public let Channel_AdminLog_InfoPanelAlertTitle: String + public let Notifications_GroupNotificationsPreview: String + public let Message_PinnedLocationMessage: String + public let Settings_Logout: String + public let Profile_Username: String + public let Group_Username_InvalidTooShort: String + public let AuthSessions_TerminateOtherSessions: String + public let PasscodeSettings_TryAgainIn1Minute: String + public let Notifications_InAppNotifications: String + public let Channels_Title: String + public let StickerPack_ViewPack: String + public let EnterPasscode_ChangeTitle: String + public let Call_Decline: String + public let UserInfo_AddPhone: String + public let Web_CopyLink: String + public let Activity_PlayingGame: String + public let CheckoutInfo_ShippingInfoStatePlaceholder: String + public let Notifications_MessageNotificationsSound: String + public let Call_StatusWaiting: String + public let Weekday_ShortWednesday: String + public let DC_UPDATE: String + public let PasscodeSettings_AutoLock_IfAwayFor_5hours: String + public let Notifications_Title: String + public let Conversation_PinnedMessage: String + public let Channel_AdminLog_MessagePreviousMessage: String + public let ConversationProfile_LeaveDeleteAndExit: String + public let State_connecting: String + public let WebPreview_LinkPreview: String + public let Map_OpenInHereMaps: String + public let CheckoutInfo_Pay: String + public let DialogList_Messages: String + public let Login_CountryCode: String + public let CheckoutInfo_ShippingInfoState: String + public let Map_OpenInGooglePlus: String + private let _CHAT_MESSAGE_AUDIO: String + private let _CHAT_MESSAGE_AUDIO_r: [(Int, NSRange)] + public func CHAT_MESSAGE_AUDIO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_AUDIO, self._CHAT_MESSAGE_AUDIO_r, [_1, _2]) + } + public let Login_SmsRequestState2: String + public let Preview_SaveToCameraRoll: String + public let PasscodeSettings_ChangePasscode: String + public let TwoStepAuth_RecoveryCodeInvalid: String + private let _Message_PaymentSent: String + private let _Message_PaymentSent_r: [(Int, NSRange)] + public func Message_PaymentSent(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Message_PaymentSent, self._Message_PaymentSent_r, [_0]) + } + public let Message_PinnedAudioMessage: String + public let Login_InfoDeletePhoto: String + public let Settings_SaveIncomingPhotosHelp: String + public let TwoStepAuth_RecoveryCodeExpired: String + public let TwoStepAuth_EmailTitle: String + public let Privacy_GroupsAndChannels_NeverAllow: String + public let Conversation_AddContact: String + public let PhotoEditor_QualityLow: String + public let Paint_Outlined: String + public let Checkout_PasswordEntry_Title: String + public let Common_Done: String + public let PrivacySettings_LastSeenContacts: String + public let CheckoutInfo_ShippingInfoAddress1: String + public let UserInfo_LastNamePlaceholder: String + public let Conversation_StatusKickedFromChannel: String + public let GroupInfo_InviteLink_RevokeAlert_Text: String + private let _DialogList_SingleTypingSuffix: String + private let _DialogList_SingleTypingSuffix_r: [(Int, NSRange)] + public func DialogList_SingleTypingSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_SingleTypingSuffix, self._DialogList_SingleTypingSuffix_r, [_0]) + } + public let LastSeen_JustNow: String + public let CheckoutInfo_ShippingInfoAddress2: String + public let Watch_Suggestion_No: String + public let BroadcastListInfo_AddRecipient: String + private let _Channel_Management_ErrorNotMember: String + private let _Channel_Management_ErrorNotMember_r: [(Int, NSRange)] + public func Channel_Management_ErrorNotMember(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_Management_ErrorNotMember, self._Channel_Management_ErrorNotMember_r, [_0]) + } + public let Privacy_Calls_NeverAllow: String + public let Settings_About_Title: String + public let PhoneNumberHelp_Help: String + public let Service_NetworkConfigurationUpdatedMessage: String + private let _Time_MonthOfYear_9: String + private let _Time_MonthOfYear_9_r: [(Int, NSRange)] + public func Time_MonthOfYear_9(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_9, self._Time_MonthOfYear_9_r, [_0]) + } + public let Channel_LinkItem: String + public let Camera_Retake: String + public let StickerPack_ShowStickers: String + private let _CHAT_CREATED: String + private let _CHAT_CREATED_r: [(Int, NSRange)] + public func CHAT_CREATED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_CREATED, self._CHAT_CREATED_r, [_1, _2]) + } + public let LastSeen_WithinAMonth: String + private let _PrivacySettings_LastSeenContactsPlus: String + private let _PrivacySettings_LastSeenContactsPlus_r: [(Int, NSRange)] + public func PrivacySettings_LastSeenContactsPlus(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PrivacySettings_LastSeenContactsPlus, self._PrivacySettings_LastSeenContactsPlus_r, [_0]) + } + public let Conversation_FileHowTo: String + public let ChangePhoneNumberNumber_NewNumber: String + public let Compose_NewChannel: String + public let Channel_AdminLog_CanChangeInviteLink: String + private let _Call_CallInProgressMessage: String + private let _Call_CallInProgressMessage_r: [(Int, NSRange)] + public func Call_CallInProgressMessage(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Call_CallInProgressMessage, self._Call_CallInProgressMessage_r, [_1, _2]) + } + public let Conversation_InputTextBroadcastPlaceholder: String + private let _ShareFileTip_Text: String + private let _ShareFileTip_Text_r: [(Int, NSRange)] + public func ShareFileTip_Text(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_ShareFileTip_Text, self._ShareFileTip_Text_r, [_0]) + } + private let _CancelResetAccount_TextSMS: String + private let _CancelResetAccount_TextSMS_r: [(Int, NSRange)] + public func CancelResetAccount_TextSMS(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CancelResetAccount_TextSMS, self._CancelResetAccount_TextSMS_r, [_0]) + } + public let Channel_EditAdmin_PermissionInviteUsers: String + public let Conversation_Document: String + public let SearchImages_RetryDownload: String + public let GroupInfo_DeleteAndExit: String + public let GroupInfo_InviteLink_CopyLink: String + public let Weekday_Friday: String + public let Login_ResetAccountProtected_Title: String + public let Settings_SetProfilePhoto: String + public let Compose_ChannelTokenListPlaceholder: String + public let Channel_EditAdmin_PermissionPinMessages: String + public let Your_card_has_expired: String + private let _CHAT_MESSAGE_INVOICE: String + private let _CHAT_MESSAGE_INVOICE_r: [(Int, NSRange)] + public func CHAT_MESSAGE_INVOICE(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_INVOICE, self._CHAT_MESSAGE_INVOICE_r, [_1, _2, _3]) + } + public let ChannelInfo_ConfirmLeave: String + public let ShareMenu_CopyShareLinkGame: String + public let ReportPeer_ReasonOther: String + private let _Username_UsernameIsAvailable: String + private let _Username_UsernameIsAvailable_r: [(Int, NSRange)] + public func Username_UsernameIsAvailable(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Username_UsernameIsAvailable, self._Username_UsernameIsAvailable_r, [_0]) + } + public let KeyCommand_JumpToNextUnreadChat: String + public let Conversation_EncryptedDescriptionTitle: String + public let DialogList_Pin: String + private let _Notification_RemovedGroupPhoto: String + private let _Notification_RemovedGroupPhoto_r: [(Int, NSRange)] + public func Notification_RemovedGroupPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_RemovedGroupPhoto, self._Notification_RemovedGroupPhoto_r, [_0]) + } + public let Channel_ErrorAddTooMuch: String + public let GroupInfo_SharedMediaNone: String + public let ChatSettings_TextSizeUnits: String + public let ChatSettings_AutoPlayAnimations: String + public let Conversation_FileOpenIn: String + public let Channel_Setup_TypePublic: String + private let _ChangePhone_ErrorOccupied: String + private let _ChangePhone_ErrorOccupied_r: [(Int, NSRange)] + public func ChangePhone_ErrorOccupied(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_ChangePhone_ErrorOccupied, self._ChangePhone_ErrorOccupied_r, [_0]) + } + public let DialogList_RecentTitleGroups: String + public let Privacy_GroupsAndChannels_CustomShareHelp: String + public let KeyCommand_ChatInfo: String + public let Notification_CreatedBroadcastList: String + public let PhotoEditor_HighlightsTint: String + public let Watch_Compose_AddContact: String + public let Coub_TapForSound: String + public let Compose_NewEncryptedChat: String + public let PhotoEditor_CropReset: String + public let Login_InvalidLastNameError: String + public let Channel_Members_AddMembers: String + public let Tour_Title2: String + public let Login_TermsOfServiceHeader: String + public let AuthSessions_OtherSessions: String + public let Watch_UserInfo_Title: String + public let InstantPage_FeedbackButton: String + private let _Generic_OpenHiddenLinkAlert: String + private let _Generic_OpenHiddenLinkAlert_r: [(Int, NSRange)] + public func Generic_OpenHiddenLinkAlert(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Generic_OpenHiddenLinkAlert, self._Generic_OpenHiddenLinkAlert_r, [_0]) + } + public let Conversation_Contact: String + public let NetworkUsageSettings_GeneralDataSection: String + public let Service_ApplyLocalization: String + private let _StickerPack_RemovePrompt: String + private let _StickerPack_RemovePrompt_r: [(Int, NSRange)] + public func StickerPack_RemovePrompt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_StickerPack_RemovePrompt, self._StickerPack_RemovePrompt_r, [_0]) + } + public let Channel_NotificationCommentsDisabled: String + public let EnterPasscode_RepeatNewPasscode: String + public let InstantPage_AutoNightTheme: String + public let CloudStorage_Title: String + public let Month_ShortOctober: String + public let Settings_FAQ: String + public let PrivacySettings_LastSeen: String + public let DialogList_SearchSectionRecent: String + public let ChatSettings_AutomaticVideoMessageDownload: String + public let Conversation_ContextMenuDelete: String + public let Tour_Text6: String + public let PhotoEditor_WarmthTool: String + private let _Time_MonthOfYear_8: String + private let _Time_MonthOfYear_8_r: [(Int, NSRange)] + public func Time_MonthOfYear_8(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_8, self._Time_MonthOfYear_8_r, [_0]) + } + public let Common_TakePhoto: String + public let PhotoEditor_Current: String + public let UserInfo_CreateNewContact: String + public let NetworkUsageSettings_MediaDocumentDataSection: String + public let Login_CodeSentCall: String + public let Watch_PhotoView_Title: String + private let _PrivacySettings_LastSeenContactsMinus: String + private let _PrivacySettings_LastSeenContactsMinus_r: [(Int, NSRange)] + public func PrivacySettings_LastSeenContactsMinus(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PrivacySettings_LastSeenContactsMinus, self._PrivacySettings_LastSeenContactsMinus_r, [_0]) + } + public let Login_InfoUpdatePhoto: String + public let ShareMenu_SelectChats: String + public let Group_ErrorSendRestrictedMedia: String + public let Channel_EditAdmin_PermissinAddAdminOff: String + public let Cache_Files: String + public let PhotoEditor_EnhanceTool: String + public let Conversation_SearchPlaceholder: String + public let Calls_Search: String + public let BroadcastListInfo_Title: String + public let WatchRemote_AlertText: String + public let Channel_AdminLog_CanInviteUsers: String + public let Conversation_Block: String + public let AttachmentMenu_PhotoOrVideo: String + public let Channel_BanUser_PermissionReadMessages: String + public let Month_ShortMarch: String + public let GroupInfo_InviteLink_Title: String + public let Watch_LastSeen_JustNow: String + public let BroadcastLists_Title: String + public let PhoneLabel_Title: String + public let PrivacySettings_Passcode: String + public let Paint_ClearConfirm: String + private let _Checkout_SavePasswordTimeout: String + private let _Checkout_SavePasswordTimeout_r: [(Int, NSRange)] + public func Checkout_SavePasswordTimeout(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Checkout_SavePasswordTimeout, self._Checkout_SavePasswordTimeout_r, [_0]) + } + public let PhotoEditor_BlurToolOff: String + public let AccessDenied_VideoMicrophone: String + public let Weekday_ShortThursday: String + public let UserInfo_ShareContact: String + public let LoginPassword_InvalidPasswordError: String + public let Login_PhoneAndCountryHelp: String + public let CheckoutInfo_ReceiverInfoName: String + private let _LastSeen_TodayAt: String + private let _LastSeen_TodayAt_r: [(Int, NSRange)] + public func LastSeen_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_LastSeen_TodayAt, self._LastSeen_TodayAt_r, [_0]) + } + private let _Time_YesterdayAt: String + private let _Time_YesterdayAt_r: [(Int, NSRange)] + public func Time_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_YesterdayAt, self._Time_YesterdayAt_r, [_0]) + } + public let Weekday_Yesterday: String + public let Conversation_InputTextSilentBroadcastPlaceholder: String + public let Embed_PlayingInPIP: String + public let Call_StatusIncoming: String + public let Conversation_Play: String + public let Settings_PrivacySettings: String + public let Conversation_SilentBroadcastTooltipOn: String + private let _CHAT_MESSAGE_GEO: String + private let _CHAT_MESSAGE_GEO_r: [(Int, NSRange)] + public func CHAT_MESSAGE_GEO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_GEO, self._CHAT_MESSAGE_GEO_r, [_1, _2]) + } + public let DialogList_SearchLabel: String + public let Login_CodeSentInternal: String + public let Channel_AdminLog_BanSendMessages: String + public let Channel_MessagePhotoRemoved: String + public let Conversation_StatusKickedFromGroup: String + public let Compose_NewChannel_AddMemberHelp: String + public let GroupInfo_ChatAdmins: String + public let PhotoEditor_CurvesAll: String + public let Compose_Create: String + private let _LOCKED_MESSAGE: String + private let _LOCKED_MESSAGE_r: [(Int, NSRange)] + public func LOCKED_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_LOCKED_MESSAGE, self._LOCKED_MESSAGE_r, [_1]) + } + public let Conversation_ContextMenuShare: String + private let _Call_GroupFormat: String + private let _Call_GroupFormat_r: [(Int, NSRange)] + public func Call_GroupFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Call_GroupFormat, self._Call_GroupFormat_r, [_1, _2]) + } + public let Forward_ChannelReadOnly: String + public let Privacy_GroupsAndChannels_NeverAllow_Title: String + public let Conversation_StatusGroupDeactivated: String + private let _CHAT_JOINED: String + private let _CHAT_JOINED_r: [(Int, NSRange)] + public func CHAT_JOINED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_JOINED, self._CHAT_JOINED_r, [_1, _2]) + } + public let Conversation_Moderate_Ban: String + public let Group_Status: String + public let Watch_Suggestion_Absolutely: String + public let Conversation_InputTextPlaceholder: String + private let _Time_MonthOfYear_7: String + private let _Time_MonthOfYear_7_r: [(Int, NSRange)] + public func Time_MonthOfYear_7(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_7, self._Time_MonthOfYear_7_r, [_0]) + } + public let SharedMedia_TitleAudio: String + public let TwoStepAuth_RecoveryCode: String + public let SharedMedia_CategoryDocs: String + public let Channel_AdminLog_CanChangeInfo: String + public let Channel_AdminLogFilter_EventsAdmins: String + private let _AuthSessions_AppUnofficial: String + private let _AuthSessions_AppUnofficial_r: [(Int, NSRange)] + public func AuthSessions_AppUnofficial(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_AuthSessions_AppUnofficial, self._AuthSessions_AppUnofficial_r, [_0]) + } + public let Channel_EditAdmin_PermissionsHeader: String + private let _DialogList_SingleUploadingVideoSuffix: String + private let _DialogList_SingleUploadingVideoSuffix_r: [(Int, NSRange)] + public func DialogList_SingleUploadingVideoSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_SingleUploadingVideoSuffix, self._DialogList_SingleUploadingVideoSuffix_r, [_0]) + } + public let Group_UpgradeNoticeHeader: String + private let _CHAT_DELETE_YOU: String + private let _CHAT_DELETE_YOU_r: [(Int, NSRange)] + public func CHAT_DELETE_YOU(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_DELETE_YOU, self._CHAT_DELETE_YOU_r, [_1, _2]) + } + private let _MESSAGE_NOTEXT: String + private let _MESSAGE_NOTEXT_r: [(Int, NSRange)] + public func MESSAGE_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_NOTEXT, self._MESSAGE_NOTEXT_r, [_1]) + } + private let _CHAT_MESSAGE_GIF: String + private let _CHAT_MESSAGE_GIF_r: [(Int, NSRange)] + public func CHAT_MESSAGE_GIF(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_GIF, self._CHAT_MESSAGE_GIF_r, [_1, _2]) + } + public let GroupInfo_InviteLink_CopyAlert_Success: String + public let Channel_Info_Members: String + public let ShareFileTip_CloseTip: String + public let KeyCommand_Find: String + public let Preview_VideoNotYetDownloaded: String + public let Checkout_NewCard_PostcodeTitle: String + private let _Channel_AdminLog_MessageRestricted: String + private let _Channel_AdminLog_MessageRestricted_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageRestricted(_ _0: String, _ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageRestricted, self._Channel_AdminLog_MessageRestricted_r, [_0, _1, _2]) + } + public let Channel_EditAdmin_PermissinAddAdminOn: String + public let WebSearch_GIFs: String + public let TwoStepAuth_EnterPasswordTitle: String + private let _CHANNEL_MESSAGE_GAME: String + private let _CHANNEL_MESSAGE_GAME_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_GAME(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_GAME, self._CHANNEL_MESSAGE_GAME_r, [_1, _2]) + } + public let AccessDenied_CallMicrophone: String + public let Conversation_DeleteMessagesForEveryone: String + public let UserInfo_TapToCall: String + public let Common_Edit: String + public let Conversation_OpenFile: String + public let Message_PinnedDocumentMessage: String + public let Channel_ShareChannel: String + public let PrivacySettings_DeleteAccountNowConfirmation: String + public let Checkout_TotalPaidAmount: String + public let Conversation_UnsupportedMedia: String + private let _Message_ForwardedMessage: String + private let _Message_ForwardedMessage_r: [(Int, NSRange)] + public func Message_ForwardedMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Message_ForwardedMessage, self._Message_ForwardedMessage_r, [_0]) + } + public let Checkout_NewCard_SaveInfoEnableHelp: String + public let Call_AudioRouteHide: String + public let CallSettings_OnMobile: String + public let Conversation_GifTooltip: String + public let CheckoutInfo_ErrorCityInvalid: String + public let Profile_CreateEncryptedChatError: String + public let Map_LocationTitle: String + public let Compose_Recipients: String + public let Message_ReplyActionButtonShowReceipt: String + public let PhotoEditor_ShadowsTool: String + public let Checkout_NewCard_CardholderNamePlaceholder: String + public let Cache_Title: String + public let Month_GenMay: String + private let _Notification_CreatedChat: String + private let _Notification_CreatedChat_r: [(Int, NSRange)] + public func Notification_CreatedChat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_CreatedChat, self._Notification_CreatedChat_r, [_0]) + } + public let Calls_NoMissedCallsPlacehoder: String + public let Watch_UserInfo_Block: String + public let Watch_LastSeen_ALongTimeAgo: String + public let StickerPacksSettings_ManagingHelp: String + public let Privacy_GroupsAndChannels_InviteToChannelMultipleError: String + public let PrivacySettings_TouchIdEnable: String + public let SearchImages_Title: String + public let Channel_BlackList_Title: String + public let Checkout_NewCard_SaveInfo: String + public let Notification_CallMissed: String + public let Profile_ShareContactButton: String + public let Group_ErrorSendRestrictedStickers: String + public let Bot_GroupStatusDoesNotReadHistory: String + public let Notification_Mute1h: String + public let Cache_ClearCacheAlert: String + public let BroadcastLists_NoListsYet: String + public let Settings_TabTitle: String + private let _Time_MonthOfYear_6: String + private let _Time_MonthOfYear_6_r: [(Int, NSRange)] + public func Time_MonthOfYear_6(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_6, self._Time_MonthOfYear_6_r, [_0]) + } + public let NetworkUsageSettings_MediaAudioDataSection: String + public let GroupInfo_DeactivatedStatus: String + private let _CHAT_PHOTO_EDITED: String + private let _CHAT_PHOTO_EDITED_r: [(Int, NSRange)] + public func CHAT_PHOTO_EDITED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_PHOTO_EDITED, self._CHAT_PHOTO_EDITED_r, [_1, _2]) + } + public let Conversation_ContextMenuMore: String + private let _PrivacySettings_LastSeenEverybodyMinus: String + private let _PrivacySettings_LastSeenEverybodyMinus_r: [(Int, NSRange)] + public func PrivacySettings_LastSeenEverybodyMinus(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PrivacySettings_LastSeenEverybodyMinus, self._PrivacySettings_LastSeenEverybodyMinus_r, [_0]) + } + public let Weekday_Today: String + public let Login_InvalidFirstNameError: String + private let _Notification_Joined: String + private let _Notification_Joined_r: [(Int, NSRange)] + public func Notification_Joined(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_Joined, self._Notification_Joined_r, [_0]) + } + private let _VideoPreview_OptionHD: String + private let _VideoPreview_OptionHD_r: [(Int, NSRange)] + public func VideoPreview_OptionHD(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_VideoPreview_OptionHD, self._VideoPreview_OptionHD_r, [_0]) + } + public let Paint_Clear: String + public let TwoStepAuth_RecoveryFailed: String + private let _MESSAGE_AUDIO: String + private let _MESSAGE_AUDIO_r: [(Int, NSRange)] + public func MESSAGE_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_AUDIO, self._MESSAGE_AUDIO_r, [_1]) + } + public let Checkout_PasswordEntry_Pay: String + public let Notifications_MessageNotificationsHelp: String + public let Notification_EncryptedChatRequested: String + public let EnterPasscode_EnterCurrentPasscode: String + public let Channel_Management_LabelModerator: String + private let _MESSAGE_GAME: String + private let _MESSAGE_GAME_r: [(Int, NSRange)] + public func MESSAGE_GAME(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_GAME, self._MESSAGE_GAME_r, [_1, _2]) + } + public let Conversation_Moderate_Report: String + public let MessageTimer_Forever: String + private let _Conversation_EncryptedPlaceholderTitleIncoming: String + private let _Conversation_EncryptedPlaceholderTitleIncoming_r: [(Int, NSRange)] + public func Conversation_EncryptedPlaceholderTitleIncoming(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_EncryptedPlaceholderTitleIncoming, self._Conversation_EncryptedPlaceholderTitleIncoming_r, [_0]) + } + private let _Map_AccurateTo: String + private let _Map_AccurateTo_r: [(Int, NSRange)] + public func Map_AccurateTo(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Map_AccurateTo, self._Map_AccurateTo_r, [_0]) + } + private let _Call_ParticipantVersionOutdatedError: String + private let _Call_ParticipantVersionOutdatedError_r: [(Int, NSRange)] + public func Call_ParticipantVersionOutdatedError(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Call_ParticipantVersionOutdatedError, self._Call_ParticipantVersionOutdatedError_r, [_0]) + } + public let Tour_Text2: String + public let Preview_ViewStickerPack: String + public let Conversation_MessageDialogDelete: String + public let Calls_Clear: String + public let Username_Placeholder: String + private let _Notification_PinnedDeletedMessage: String + private let _Notification_PinnedDeletedMessage_r: [(Int, NSRange)] + public func Notification_PinnedDeletedMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedDeletedMessage, self._Notification_PinnedDeletedMessage_r, [_0]) + } + public let UserInfo_BotHelp: String + public let Contacts_contact: String + public let TwoStepAuth_PasswordSet: String + public let Channel_Moderator_AccessLevelEditor: String + public let EnterPasscode_TouchId: String + private let _CHANNEL_MESSAGE_VIDEO: String + private let _CHANNEL_MESSAGE_VIDEO_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_VIDEO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_VIDEO, self._CHANNEL_MESSAGE_VIDEO_r, [_1]) + } + public let Checkout_ErrorInvoiceAlreadyPaid: String + public let ChatAdmins_Title: String + public let BroadcastLists_NoListsText: String + public let ChannelMembers_WhoCanAddMembers: String + public let ChannelMembers_AllMembersMayInviteOffHelp: String + public let Conversation_InfoPrivate: String + public let PasscodeSettings_Help: String + public let Conversation_EditingMessagePanelTitle: String + private let _NetworkUsageSettings_CellularUsageSince: String + private let _NetworkUsageSettings_CellularUsageSince_r: [(Int, NSRange)] + public func NetworkUsageSettings_CellularUsageSince(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_NetworkUsageSettings_CellularUsageSince, self._NetworkUsageSettings_CellularUsageSince_r, [_0]) + } + public let GroupInfo_ConvertToSupergroup: String + private let _Notification_PinnedContactMessage: String + private let _Notification_PinnedContactMessage_r: [(Int, NSRange)] + public func Notification_PinnedContactMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedContactMessage, self._Notification_PinnedContactMessage_r, [_0]) + } + public let CallSettings_UseLessDataLongDescription: String + public let Conversation_SecretChatContextBotAlert: String + public let Channel_Moderator_AccessLevelRevoke: String + public let CheckoutInfo_ReceiverInfoTitle: String + public let Channel_AdminLogFilter_EventsRestrictions: String + public let GroupInfo_InviteLink_RevokeLink: String + public let Conversation_Unmute: String + public let Checkout_PaymentMethod_Title: String + private let _AppLanguage_LanguageSuggested: String + private let _AppLanguage_LanguageSuggested_r: [(Int, NSRange)] + public func AppLanguage_LanguageSuggested(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_AppLanguage_LanguageSuggested, self._AppLanguage_LanguageSuggested_r, [_0]) + } + public let Notifications_MessageNotifications: String + public let ChannelMembers_WhoCanAddMembersAdminsHelp: String + public let DialogList_DeleteBotConversationConfirmation: String + private let _MediaPicker_AccessDeniedHelp: String + private let _MediaPicker_AccessDeniedHelp_r: [(Int, NSRange)] + public func MediaPicker_AccessDeniedHelp(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MediaPicker_AccessDeniedHelp, self._MediaPicker_AccessDeniedHelp_r, [_0]) + } + private let _GroupInfo_InvitationLinkAccept: String + private let _GroupInfo_InvitationLinkAccept_r: [(Int, NSRange)] + public func GroupInfo_InvitationLinkAccept(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_GroupInfo_InvitationLinkAccept, self._GroupInfo_InvitationLinkAccept_r, [_0]) + } + public let Conversation_ClousStorageInfo_Description2: String + public let Map_Hybrid: String + public let Channel_Setup_Title: String + public let Activity_UploadingVideo: String + public let Channel_Info_Management: String + private let _Notification_MessageLifetimeChangedOutgoing: String + private let _Notification_MessageLifetimeChangedOutgoing_r: [(Int, NSRange)] + public func Notification_MessageLifetimeChangedOutgoing(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_MessageLifetimeChangedOutgoing, self._Notification_MessageLifetimeChangedOutgoing_r, [_1]) + } + public let Conversation_DeleteOneMessage: String + public let PhotoEditor_QualityVeryLow: String + public let Month_ShortFebruary: String + public let Compose_NewBroadcast: String + public let Conversation_ForwardTitle: String + public let Settings_FAQ_URL: String + public let TwoStepAuth_ConfirmationChangeEmail: String + public let Activity_RecordingVideoMessage: String + public let WelcomeScreen_ContactsAccessSettings: String + public let SharedMedia_EmptyFilesText: String + private let _Contacts_AccessDeniedHelpLandscape: String + private let _Contacts_AccessDeniedHelpLandscape_r: [(Int, NSRange)] + public func Contacts_AccessDeniedHelpLandscape(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Contacts_AccessDeniedHelpLandscape, self._Contacts_AccessDeniedHelpLandscape_r, [_0]) + } + public let Channel_NotificationCommentsEnabled: String + public let PasscodeSettings_UnlockWithTouchId: String + public let Contacts_AccessDeniedHelpON: String + private let _Time_MonthOfYear_5: String + private let _Time_MonthOfYear_5_r: [(Int, NSRange)] + public func Time_MonthOfYear_5(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_5, self._Time_MonthOfYear_5_r, [_0]) + } + private let _PrivacySettings_LastSeenContactsMinusPlus: String + private let _PrivacySettings_LastSeenContactsMinusPlus_r: [(Int, NSRange)] + public func PrivacySettings_LastSeenContactsMinusPlus(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PrivacySettings_LastSeenContactsMinusPlus, self._PrivacySettings_LastSeenContactsMinusPlus_r, [_0, _1]) + } + public let NetworkUsageSettings_ResetStats: String + private let _Notification_ChannelInviter: String + private let _Notification_ChannelInviter_r: [(Int, NSRange)] + public func Notification_ChannelInviter(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_ChannelInviter, self._Notification_ChannelInviter_r, [_0]) + } + public let Profile_MessageLifetimeForever: String + public let Conversation_Edit: String + public let TwoStepAuth_ResetAccountHelp: String + public let Month_GenDecember: String + private let _Watch_LastSeen_YesterdayAt: String + private let _Watch_LastSeen_YesterdayAt_r: [(Int, NSRange)] + public func Watch_LastSeen_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Watch_LastSeen_YesterdayAt, self._Watch_LastSeen_YesterdayAt_r, [_0]) + } + public let Channel_ErrorAddBlocked: String + public let Conversation_Unpin: String + public let Call_RecordingDisabledMessage: String + public let Conversation_Stop: String + public let Conversation_UnblockUser: String + public let Conversation_Unblock: String + private let _CHANNEL_MESSAGE_GIF: String + private let _CHANNEL_MESSAGE_GIF_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_GIF, self._CHANNEL_MESSAGE_GIF_r, [_1]) + } + public let Channel_AdminLogFilter_EventsEditedMessages: String + public let Channel_Username_InvalidTooShort: String + public let Watch_LastSeen_WithinAWeek: String + public let BlockedUsers_SelectUserTitle: String + public let Profile_MessageLifetime1w: String + public let DialogList_TabTitle: String + public let UserInfo_GenericPhoneLabel: String + public let MediaPicker_MomentsDateFormat: String + private let _Conversation_DownloadKilobytes: String + private let _Conversation_DownloadKilobytes_r: [(Int, NSRange)] + public func Conversation_DownloadKilobytes(_ _0: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_DownloadKilobytes, self._Conversation_DownloadKilobytes_r, ["\(_0)"]) + } + private let _Username_LinkHint: String + private let _Username_LinkHint_r: [(Int, NSRange)] + public func Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Username_LinkHint, self._Username_LinkHint_r, [_0]) + } + public let NetworkUsageSettings_Title: String + public let CheckoutInfo_ShippingInfoPostcodePlaceholder: String + public let Wallpaper_Wallpaper: String + public let GroupInfo_InviteLink_RevokeAlert_Revoke: String + public let SharedMedia_TitleLink: String + public let Channel_JoinChannel: String + public let StickerPack_Add: String + public let Group_ErrorNotMutualContact: String + public let AccessDenied_LocationDisabled: String + public let Conversation_DownloadPhoto: String + public let Login_UnknownError: String + public let Presence_online: String + public let DialogList_Title: String + public let Stickers_Install: String + public let SearchImages_NoImagesFound: String + private let _Notification_RemovedUserPhoto: String + private let _Notification_RemovedUserPhoto_r: [(Int, NSRange)] + public func Notification_RemovedUserPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_RemovedUserPhoto, self._Notification_RemovedUserPhoto_r, [_0]) + } + private let _Watch_Time_ShortTodayAt: String + private let _Watch_Time_ShortTodayAt_r: [(Int, NSRange)] + public func Watch_Time_ShortTodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Watch_Time_ShortTodayAt, self._Watch_Time_ShortTodayAt_r, [_0]) + } + public let UserInfo_GroupsInCommon: String + public let ChatSettings_Language: String + public let AccessDenied_CameraDisabled: String + public let Message_PinnedContactMessage: String + public let UserInfo_Call: String + public let Conversation_InputTextDisabledPlaceholder: String + public let Map_ForwardViaTelegram: String + public let Month_GenMarch: String + public let Watch_UserInfo_Unmute: String + public let PhotoEditor_BlurTool: String + public let Common_Delete: String + public let Username_Title: String + public let Login_PhoneFloodError: String + public let CheckoutInfo_ErrorPostcodeInvalid: String + private let _CHANNEL_MESSAGE_PHOTO: String + private let _CHANNEL_MESSAGE_PHOTO_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_PHOTO, self._CHANNEL_MESSAGE_PHOTO_r, [_1]) + } + public let Channel_AdminLog_InfoPanelTitle: String + public let Group_ErrorAddTooMuchBots: String + private let _Notification_CallFormat: String + private let _Notification_CallFormat_r: [(Int, NSRange)] + public func Notification_CallFormat(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_CallFormat, self._Notification_CallFormat_r, [_1, _2]) + } + private let _CHAT_MESSAGE_PHOTO: String + private let _CHAT_MESSAGE_PHOTO_r: [(Int, NSRange)] + public func CHAT_MESSAGE_PHOTO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_PHOTO, self._CHAT_MESSAGE_PHOTO_r, [_1, _2]) + } + private let _Channel_AdminLog_MessageToggleInvitesOff: String + private let _Channel_AdminLog_MessageToggleInvitesOff_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageToggleInvitesOff(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageToggleInvitesOff, self._Channel_AdminLog_MessageToggleInvitesOff_r, [_0]) + } + public let UserInfo_ShareBot: String + public let TwoStepAuth_EmailSkip: String + public let Conversation_JumpToDate: String + public let CheckoutInfo_ReceiverInfoEmailPlaceholder: String + public let Message_Photo: String + public let Conversation_ReportSpam: String + public let Camera_FlashAuto: String + private let _Time_MonthOfYear_4: String + private let _Time_MonthOfYear_4_r: [(Int, NSRange)] + public func Time_MonthOfYear_4(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_4, self._Time_MonthOfYear_4_r, [_0]) + } + public let Call_ConnectionErrorMessage: String + public let Compose_NewChannel_AddMember: String + public let Watch_State_Updating: String + public let LastSeen_ALongTimeAgo: String + public let DialogList_SearchSectionGlobal: String + public let ChangePhoneNumberNumber_NumberPlaceholder: String + public let GroupInfo_AddUserLeftError: String + public let GroupInfo_GroupType: String + public let Watch_Suggestion_OnMyWay: String + public let Checkout_NewCard_PaymentCard: String + public let PhotoEditor_CropAspectRatioOriginal: String + public let MediaPicker_MomentsDateRangeFormat: String + public let UserInfo_NotificationsDisabled: String + private let _CONTACT_JOINED: String + private let _CONTACT_JOINED_r: [(Int, NSRange)] + public func CONTACT_JOINED(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CONTACT_JOINED, self._CONTACT_JOINED_r, [_1]) + } + public let PrivacyLastSeenSettings_AlwaysShareWith_Title: String + public let BlockedUsers_LeavePrefix: String + public let NetworkUsageSettings_ResetStatsConfirmation: String + public let Channel_EditAdmin_PermissionPostMessages: String + public let DialogList_EncryptionProcessing: String + public let Conversation_ApplyLocalization: String + public let Conversation_DeleteManyMessages: String + public let CancelResetAccount_Title: String + public let Notification_CallOutgoingShort: String + public let Channel_Moderator_AccessLevelHeader: String + public let SharedMedia_TitleAll: String + public let Conversation_SlideToCancel: String + public let AuthSessions_TerminateSession: String + public let Channel_AdminLogFilter_EventsDeletedMessages: String + public let PrivacyLastSeenSettings_AlwaysShareWith_Placeholder: String + public let Channel_Members_Title: String + public let Channel_AdminLog_CanDeleteMessages: String + public let Group_Setup_TypePrivateHelp: String + private let _Notification_PinnedVideoMessage: String + private let _Notification_PinnedVideoMessage_r: [(Int, NSRange)] + public func Notification_PinnedVideoMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedVideoMessage, self._Notification_PinnedVideoMessage_r, [_0]) + } + public let Conversation_ContextMenuStickerPackAdd: String + public let Channel_AdminLogFilter_EventsNewMembers: String + public let Channel_AdminLogFilter_EventsPinned: String + private let _Conversation_Moderate_DeleteAllMessages: String + private let _Conversation_Moderate_DeleteAllMessages_r: [(Int, NSRange)] + public func Conversation_Moderate_DeleteAllMessages(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_Moderate_DeleteAllMessages, self._Conversation_Moderate_DeleteAllMessages_r, [_0]) + } + public let SharedMedia_CategoryOther: String + public let GoogleDrive_LogoutMessage: String + public let Preview_DeletePhoto: String + public let PasscodeSettings_TurnPasscodeOn: String + public let GroupInfo_ChannelListNamePlaceholder: String + public let DialogList_Unpin: String + public let GroupInfo_SetGroupPhoto: String + public let StickerPacksSettings_ArchivedPacks_Info: String + public let ConvertToSupergroup_Title: String + private let _CHAT_MESSAGE_NOTEXT: String + private let _CHAT_MESSAGE_NOTEXT_r: [(Int, NSRange)] + public func CHAT_MESSAGE_NOTEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_NOTEXT, self._CHAT_MESSAGE_NOTEXT_r, [_1, _2]) + } + public let Channel_Setup_TypeHeader: String + private let _Notification_NewAuthDetected: String + private let _Notification_NewAuthDetected_r: [(Int, NSRange)] + public func Notification_NewAuthDetected(_ _1: String, _ _2: String, _ _3: String, _ _4: String, _ _5: String, _ _6: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_NewAuthDetected, self._Notification_NewAuthDetected_r, [_1, _2, _3, _4, _5, _6]) + } + public let Notification_CallCanceledShort: String + public let PhotoEditor_RevertMessage: String + public let AccessDenied_VideoMessageCamera: String + public let Conversation_Search: String + private let _Channel_Management_PromotedBy: String + private let _Channel_Management_PromotedBy_r: [(Int, NSRange)] + public func Channel_Management_PromotedBy(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_Management_PromotedBy, self._Channel_Management_PromotedBy_r, [_0]) + } + private let _PrivacySettings_LastSeenNobodyPlus: String + private let _PrivacySettings_LastSeenNobodyPlus_r: [(Int, NSRange)] + public func PrivacySettings_LastSeenNobodyPlus(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PrivacySettings_LastSeenNobodyPlus, self._PrivacySettings_LastSeenNobodyPlus_r, [_0]) + } + public let Preview_ForwardViaTelegram: String + public let Notifications_InAppNotificationsSounds: String + public let Call_StatusRequesting: String + private let _CHAT_MESSAGE_CONTACT: String + private let _CHAT_MESSAGE_CONTACT_r: [(Int, NSRange)] + public func CHAT_MESSAGE_CONTACT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_CONTACT, self._CHAT_MESSAGE_CONTACT_r, [_1, _2]) + } + public let Group_UpgradeNoticeText1: String + public let ChatSettings_Other: String + private let _Channel_AdminLog_MessageChangedChannelAbout: String + private let _Channel_AdminLog_MessageChangedChannelAbout_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageChangedChannelAbout(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageChangedChannelAbout, self._Channel_AdminLog_MessageChangedChannelAbout_r, [_0]) + } + private let _Call_EmojiDescription: String + private let _Call_EmojiDescription_r: [(Int, NSRange)] + public func Call_EmojiDescription(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Call_EmojiDescription, self._Call_EmojiDescription_r, [_0]) + } + public let Settings_SaveIncomingPhotos: String + private let _Conversation_Bytes: String + private let _Conversation_Bytes_r: [(Int, NSRange)] + public func Conversation_Bytes(_ _0: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_Bytes, self._Conversation_Bytes_r, ["\(_0)"]) + } + public let GroupInfo_InviteLink_Help: String + private let _Time_MonthOfYear_3: String + private let _Time_MonthOfYear_3_r: [(Int, NSRange)] + public func Time_MonthOfYear_3(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_MonthOfYear_3, self._Time_MonthOfYear_3_r, [_0]) + } + public let Conversation_ContextMenuForward: String + public let Calls_Missed: String + public let Call_StatusRinging: String + public let Invitation_JoinGroup: String + public let Notification_PinnedMessage: String + public let Message_Location: String + private let _Notification_MessageLifetimeChanged: String + private let _Notification_MessageLifetimeChanged_r: [(Int, NSRange)] + public func Notification_MessageLifetimeChanged(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_MessageLifetimeChanged, self._Notification_MessageLifetimeChanged_r, [_1, _2]) + } + public let Message_Contact: String + private let _Watch_LastSeen_TodayAt: String + private let _Watch_LastSeen_TodayAt_r: [(Int, NSRange)] + public func Watch_LastSeen_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Watch_LastSeen_TodayAt, self._Watch_LastSeen_TodayAt_r, [_0]) + } + public let Channel_Moderator_AccessLevelModerator: String + public let GoogleDrive_Logout: String + public let PhotoEditor_RevertToOriginal: String + public let Common_More: String + public let Preview_OpenInInstagram: String + public let PhotoEditor_HighlightsTool: String + private let _Channel_Username_UsernameIsAvailable: String + private let _Channel_Username_UsernameIsAvailable_r: [(Int, NSRange)] + public func Channel_Username_UsernameIsAvailable(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_Username_UsernameIsAvailable, self._Channel_Username_UsernameIsAvailable_r, [_0]) + } + private let _PINNED_GAME: String + private let _PINNED_GAME_r: [(Int, NSRange)] + public func PINNED_GAME(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_GAME, self._PINNED_GAME_r, [_1]) + } + public let GroupInfo_BroadcastListNamePlaceholder: String + public let Conversation_ShareBotContactConfirmation: String + public let Login_CodeSentSms: String + public let Conversation_ReportSpamConfirmation: String + public let ChannelMembers_ChannelAdminsTitle: String + public let CallSettings_UseLessData: String + private let _TwoStepAuth_EnterPasswordHint: String + private let _TwoStepAuth_EnterPasswordHint_r: [(Int, NSRange)] + public func TwoStepAuth_EnterPasswordHint(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_TwoStepAuth_EnterPasswordHint, self._TwoStepAuth_EnterPasswordHint_r, [_0]) + } + public let CallSettings_TabIcon: String + public let Conversation_EditForward: String + public let ConversationProfile_UnknownAddMemberError: String + private let _Conversation_FileHowToText: String + private let _Conversation_FileHowToText_r: [(Int, NSRange)] + public func Conversation_FileHowToText(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_FileHowToText, self._Conversation_FileHowToText_r, [_0]) + } + public let Channel_AdminLog_BanSendMedia: String + public let Tour_Text7: String + public let Contacts_contactsvar: String + public let Watch_UserInfo_Unblock: String + public let Conversation_EditDelete: String + public let Conversation_ViewPhoto: String + public let StickerPacksSettings_ArchivedMasks: String + public let Message_Animation: String + public let Checkout_PaymentMethod: String + public let Channel_AdminLog_TitleSelectedEvents: String + public let Privacy_Calls_NeverAllow_Title: String + public let Cache_Music: String + private let _Login_CallRequestState1: String + private let _Login_CallRequestState1_r: [(Int, NSRange)] + public func Login_CallRequestState1(_ _0: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_CallRequestState1, self._Login_CallRequestState1_r, ["\(_0)"]) + } + private let _SearchImages_ImageNofM: String + private let _SearchImages_ImageNofM_r: [(Int, NSRange)] + public func SearchImages_ImageNofM(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_SearchImages_ImageNofM, self._SearchImages_ImageNofM_r, [_1, _2]) + } + public let Channel_Username_CreatePrivateLinkHelp: String + private let _FileSize_B: String + private let _FileSize_B_r: [(Int, NSRange)] + public func FileSize_B(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_FileSize_B, self._FileSize_B_r, [_0]) + } + private let _Target_ShareGameConfirmationGroup: String + private let _Target_ShareGameConfirmationGroup_r: [(Int, NSRange)] + public func Target_ShareGameConfirmationGroup(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Target_ShareGameConfirmationGroup, self._Target_ShareGameConfirmationGroup_r, [_0]) + } + public let PhotoEditor_SaturationTool: String + public let ImagePicker_NoPhotos: String + public let Call_StatusConnecting: String + public let Channel_BanUser_BlockFor: String + public let Preview_DeleteVideo: String + public let Bot_Start: String + private let _Channel_AdminLog_MessageChangedGroupAbout: String + private let _Channel_AdminLog_MessageChangedGroupAbout_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageChangedGroupAbout(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageChangedGroupAbout, self._Channel_AdminLog_MessageChangedGroupAbout_r, [_0]) + } + public let Notifications_TextTone: String + public let DialogList_Draft: String + private let _Watch_Time_ShortYesterdayAt: String + private let _Watch_Time_ShortYesterdayAt_r: [(Int, NSRange)] + public func Watch_Time_ShortYesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Watch_Time_ShortYesterdayAt, self._Watch_Time_ShortYesterdayAt_r, [_0]) + } + public let Contacts_InviteToTelegram: String + private let _PINNED_DOC: String + private let _PINNED_DOC_r: [(Int, NSRange)] + public func PINNED_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_DOC, self._PINNED_DOC_r, [_1]) + } + private let _ConversationProfile_UserLeftChatError: String + private let _ConversationProfile_UserLeftChatError_r: [(Int, NSRange)] + public func ConversationProfile_UserLeftChatError(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_ConversationProfile_UserLeftChatError, self._ConversationProfile_UserLeftChatError_r, [_0]) + } + public let ChatSettings_PrivateChats: String + public let Settings_CallSettings: String + public let Channel_EditAdmin_PermissionDeleteMessages: String + public let Conversation_CloudStorageInfo_Title: String + public let Channel_BanUser_PermissionSendStickersAndGifs: String + public let Channel_AdminLog_Status: String + public let Notification_RenamedChannel: String + public let BlockedUsers_BlockUser: String + public let ChatSettings_TextSize: String + public let MediaPicker_AccessDeniedError: String + public let ChannelInfo_DeleteGroup: String + private let _BlockedUsers_BlockFormat: String + private let _BlockedUsers_BlockFormat_r: [(Int, NSRange)] + public func BlockedUsers_BlockFormat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_BlockedUsers_BlockFormat, self._BlockedUsers_BlockFormat_r, [_0]) + } + public let PhoneNumberHelp_Alert: String + private let _PINNED_TEXT: String + private let _PINNED_TEXT_r: [(Int, NSRange)] + public func PINNED_TEXT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_TEXT, self._PINNED_TEXT_r, [_1, _2]) + } + public let Watch_ChannelInfo_Title: String + public let WebSearch_RecentSectionClear: String + public let Channel_AdminLogFilter_AdminsAll: String + public let StickerPack_AddStickers: String + public let Channel_Setup_TypePrivate: String + public let PhotoEditor_TintTool: String + public let Watch_Suggestion_CantTalk: String + public let PhotoEditor_QualityHigh: String + private let _CHAT_MESSAGE_STICKER: String + private let _CHAT_MESSAGE_STICKER_r: [(Int, NSRange)] + public func CHAT_MESSAGE_STICKER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_STICKER, self._CHAT_MESSAGE_STICKER_r, [_1, _2, _3]) + } + public let Map_ChooseAPlace: String + public let Tour_Title7: String + public let Watch_Bot_Restart: String + public let StickerPack_ShareStickers: String + public let ChannelMembers_AllMembersMayInvite: String + public let Channel_About_Help: String + public let Web_OpenExternal: String + public let UserInfo_AddContact: String + public let Call_EncryptionKey_Title: String + public let PhotoEditor_BlurToolLinear: String + public let AuthSessions_EmptyText: String + public let Notification_MessageLifetime1m: String + private let _Call_StatusBar: String + private let _Call_StatusBar_r: [(Int, NSRange)] + public func Call_StatusBar(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Call_StatusBar, self._Call_StatusBar_r, [_0]) + } + public let Month_ShortJuly: String + public let Watch_MessageView_ViewOnPhone: String + public let CheckoutInfo_ShippingInfoAddress1Placeholder: String + public let CallSettings_Never: String + public let DialogList_SelectContacts: String + public let Conversation_DownloadProgressMegabytes: String + public let TwoStepAuth_EmailSent: String + private let _Notification_PinnedAnimationMessage: String + private let _Notification_PinnedAnimationMessage_r: [(Int, NSRange)] + public func Notification_PinnedAnimationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedAnimationMessage, self._Notification_PinnedAnimationMessage_r, [_0]) + } + public let TwoStepAuth_RecoveryTitle: String + public let WatchRemote_AlertOpen: String + public let ExplicitContent_AlertChannel: String + public let TwoStepAuth_ConfirmationText: String + public let Widget_AuthRequired: String + private let _ForwardedAuthors2: String + private let _ForwardedAuthors2_r: [(Int, NSRange)] + public func ForwardedAuthors2(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_ForwardedAuthors2, self._ForwardedAuthors2_r, [_0, _1]) + } + public let ChannelInfo_DeleteGroupConfirmation: String + public let Login_SmsRequestState3: String + public let Notifications_AlertTones: String + public let Calls_TabTitle: String + public let Login_InfoAvatarPhoto: String + public let Contacts_MemberSearchSectionTitleChannel: String + public let PhotoEditor_CurvesTool: String + public let Preview_LoadingVideo: String + public let State_updating: String + public let TwoStepAuth_ResetAccount: String + public let Checkout_ShippingOption_Title: String + public let Weekday_Tuesday: String + public let Preview_Tooltip: String + public let Conversation_EncryptionProcessing: String + private let _CHAT_ADD_MEMBER: String + private let _CHAT_ADD_MEMBER_r: [(Int, NSRange)] + public func CHAT_ADD_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_ADD_MEMBER, self._CHAT_ADD_MEMBER_r, [_1, _2, _3]) + } + public let Weekday_ShortSunday: String + public let Month_ShortJune: String + public let Month_GenApril: String + public let StickerPacksSettings_ShowStickersButton: String + public let MediaPicker_MomentsDateRangeSameMonthFormat: String + public let CheckoutInfo_ShippingInfoTitle: String + public let StickerPacksSettings_ShowStickersButtonHelp: String + private let _Compatibility_SecretMediaVersionTooLow: String + private let _Compatibility_SecretMediaVersionTooLow_r: [(Int, NSRange)] + public func Compatibility_SecretMediaVersionTooLow(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Compatibility_SecretMediaVersionTooLow, self._Compatibility_SecretMediaVersionTooLow_r, [_0, _1]) + } + public let CallSettings_RecentCalls: String + public let Conversation_Megabytes: String + public let TwoStepAuth_FloodError: String + public let Paint_Stickers: String + public let Login_InvalidCountryCode: String + public let Privacy_Calls_AlwaysAllow_Title: String + public let Username_InvalidTooShort: String + public let Weekday_ShortFriday: String + public let Conversation_ClearAll: String + public let MediaPicker_Moments: String + public let Call_PhoneCallInProgressMessage: String + public let SharedMedia_EmptyTitle: String + public let Checkout_Name: String + public let Preview_GroupPhotoTitle: String + private let _AUTH_REGION: String + private let _AUTH_REGION_r: [(Int, NSRange)] + public func AUTH_REGION(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_AUTH_REGION, self._AUTH_REGION_r, [_1, _2]) + } + public let Settings_NotificationsAndSounds: String + private let _GroupInfo_InvitationLinkAcceptChannel: String + private let _GroupInfo_InvitationLinkAcceptChannel_r: [(Int, NSRange)] + public func GroupInfo_InvitationLinkAcceptChannel(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_GroupInfo_InvitationLinkAcceptChannel, self._GroupInfo_InvitationLinkAcceptChannel_r, [_0]) + } + public let Conversation_EncryptionCanceled: String + public let AccessDenied_SaveMedia: String + public let Channel_Username_InvalidTooManyUsernames: String + public let Compose_GroupTokenListPlaceholder: String + public let Profile_ImageUploadError: String + public let Conversation_MessageDeliveryFailed: String + public let Privacy_PaymentsClear_PaymentInfo: String + public let Notification_Mute1hMin: String + public let Notifications_GroupNotifications: String + public let CheckoutInfo_SaveInfoHelp: String + public let StickerPacksSettings_ArchivedMasks_Info: String + public let ChannelMembers_WhoCanAddMembers_AllMembers: String + public let Channel_Edit_PrivatePublicLinkAlert: String + public let Watch_Conversation_UserInfo: String + public let Application_Name: String + public let Conversation_AddToReadingList: String + public let Conversation_FileDropbox: String + public let Login_PhonePlaceholder: String + public let ExplicitContent_AlertUser: String + public let Profile_MessageLifetime1d: String + public let Calls_CallTabDescription: String + public let CheckoutInfo_ShippingInfoCityPlaceholder: String + public let Resolve_ErrorNotFound: String + public let PhotoEditor_FadeTool: String + public let Channel_TitleShowDiscussion: String + public let Channel_Setup_TypePublicHelp: String + public let GroupInfo_InviteLink_RevokeAlert_Success: String + public let Channel_Setup_PublicNoLink: String + public let Conversation_Info: String + public let ChannelInfo_InvitationLinkDoesNotExist: String + private let _Time_TodayAt: String + private let _Time_TodayAt_r: [(Int, NSRange)] + public func Time_TodayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Time_TodayAt, self._Time_TodayAt_r, [_0]) + } + public let Conversation_Processing: String + private let _InstantPage_AuthorAndDateTitle: String + private let _InstantPage_AuthorAndDateTitle_r: [(Int, NSRange)] + public func InstantPage_AuthorAndDateTitle(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_InstantPage_AuthorAndDateTitle, self._InstantPage_AuthorAndDateTitle_r, [_1, _2]) + } + private let _Watch_LastSeen_AtDate: String + private let _Watch_LastSeen_AtDate_r: [(Int, NSRange)] + public func Watch_LastSeen_AtDate(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Watch_LastSeen_AtDate, self._Watch_LastSeen_AtDate_r, [_0]) + } + public let Conversation_Location: String + public let DialogList_PasscodeLockHelp: String + public let Channel_Management_Title: String + public let Notifications_InAppNotificationsPreview: String + public let PrivacySettings_FloodControlError: String + public let EnterPasscode_EnterTitle: String + public let ReportPeer_ReasonOther_Title: String + public let Month_GenJanuary: String + public let Conversation_ForwardChats: String + public let SharedMedia_TitlePhoto: String + public let Channel_UpdatePhotoItem: String + public let GroupInfo_InvitationLinkAlreadyAccepted: String + public let UserInfo_StartSecretChat: String + public let Watch_State_Connecting: String + public let PrivacySettings_LastSeenNobody: String + private let _FileSize_MB: String + private let _FileSize_MB_r: [(Int, NSRange)] + public func FileSize_MB(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_FileSize_MB, self._FileSize_MB_r, [_0]) + } + public let ChatSearch_SearchPlaceholder: String + public let TwoStepAuth_ConfirmationAbort: String + public let GroupInfo_KickedStatus: String + public let TwoStepAuth_SetupPasswordConfirmFailed: String + private let _LastSeen_YesterdayAt: String + private let _LastSeen_YesterdayAt_r: [(Int, NSRange)] + public func LastSeen_YesterdayAt(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_LastSeen_YesterdayAt, self._LastSeen_YesterdayAt_r, [_0]) + } + public let AppleWatch_ReplyPresetsHelp: String + public let Localization_LanguageName: String + public let Map_OpenIn: String + public let Message_File: String + private let _Channel_AdminLog_MessageChangedGroupUsername: String + private let _Channel_AdminLog_MessageChangedGroupUsername_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageChangedGroupUsername(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageChangedGroupUsername, self._Channel_AdminLog_MessageChangedGroupUsername_r, [_0]) + } + private let _CHAT_MESSAGE_GAME: String + private let _CHAT_MESSAGE_GAME_r: [(Int, NSRange)] + public func CHAT_MESSAGE_GAME(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_GAME, self._CHAT_MESSAGE_GAME_r, [_1, _2, _3]) + } + public let Month_ShortMay: String + private let _WelcomeScreen_Greeting: String + private let _WelcomeScreen_Greeting_r: [(Int, NSRange)] + public func WelcomeScreen_Greeting(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_WelcomeScreen_Greeting, self._WelcomeScreen_Greeting_r, [_0]) + } + public let Tour_Text3: String + public let Contacts_GlobalSearch: String + public let Watch_Suggestion_CallSoon: String + public let DialogList_LanguageTooltip: String + public let Map_LoadError: String + public let WelcomeScreen_Logout: String + private let _Service_ApplyLocalizationWithWarnings: String + private let _Service_ApplyLocalizationWithWarnings_r: [(Int, NSRange)] + public func Service_ApplyLocalizationWithWarnings(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Service_ApplyLocalizationWithWarnings, self._Service_ApplyLocalizationWithWarnings_r, [_0]) + } + public let AccessDenied_VoiceMicrophone: String + private let _CHANNEL_MESSAGE_STICKER: String + private let _CHANNEL_MESSAGE_STICKER_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_STICKER, self._CHANNEL_MESSAGE_STICKER_r, [_1, _2]) + } + public let PrivacySettings_Title: String + public let PasscodeSettings_TurnPasscodeOff: String + public let MediaPicker_AddCaption: String + public let Channel_AdminLog_BanReadMessages: String + public let SharedMedia_Outgoing: String + public let Channel_About_Error: String + public let Channel_Status: String + public let Map_ChooseLocationTitle: String + public let Map_OpenInYandexNavigator: String + public let SearchImages_SkipImage: String + public let State_WaitingForNetwork: String + public let TwoStepAuth_EmailHelp: String + public let PhotoEditor_SharpenTool: String + public let Common_of: String + public let AuthSessions_Title: String + public let PrivacyLastSeenSettings_AlwaysShareWith: String + public let EnterPasscode_EnterPasscode: String + public let Notifications_Reset: String + public let GroupInfo_InvitationLinkGroupFull: String + private let _Channel_AdminLog_MessageChangedChannelUsername: String + private let _Channel_AdminLog_MessageChangedChannelUsername_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageChangedChannelUsername(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageChangedChannelUsername, self._Channel_AdminLog_MessageChangedChannelUsername_r, [_0]) + } + public let GoogleDrive_LogoutLogout: String + private let _CHAT_MESSAGE_DOC: String + private let _CHAT_MESSAGE_DOC_r: [(Int, NSRange)] + public func CHAT_MESSAGE_DOC(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_DOC, self._CHAT_MESSAGE_DOC_r, [_1, _2]) + } + public let Watch_AppName: String + private let _Channel_NotificationSelfAdded: String + private let _Channel_NotificationSelfAdded_r: [(Int, NSRange)] + public func Channel_NotificationSelfAdded(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_NotificationSelfAdded, self._Channel_NotificationSelfAdded_r, [_0]) + } + public let ConvertToSupergroup_HelpTitle: String + public let Conversation_TapAndHoldToRecord: String + public let Channel_ShareNoLink: String + private let _MESSAGE_GIF: String + private let _MESSAGE_GIF_r: [(Int, NSRange)] + public func MESSAGE_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_GIF, self._MESSAGE_GIF_r, [_1]) + } + private let _DialogList_EncryptedChatStartedOutgoing: String + private let _DialogList_EncryptedChatStartedOutgoing_r: [(Int, NSRange)] + public func DialogList_EncryptedChatStartedOutgoing(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_EncryptedChatStartedOutgoing, self._DialogList_EncryptedChatStartedOutgoing_r, [_0]) + } + public let Checkout_PayWithTouchId: String + private let _Notification_InvitedMany: String + private let _Notification_InvitedMany_r: [(Int, NSRange)] + public func Notification_InvitedMany(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_InvitedMany, self._Notification_InvitedMany_r, [_0, _1]) + } + private let _CHAT_ADD_YOU: String + private let _CHAT_ADD_YOU_r: [(Int, NSRange)] + public func CHAT_ADD_YOU(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_ADD_YOU, self._CHAT_ADD_YOU_r, [_1, _2]) + } + public let CheckoutInfo_ShippingInfoCity: String + public let Conversation_DiscardVoiceMessageTitle: String + public let Conversation_ClousStorageInfo_Description3: String + public let Profile_MessageLifetime: String + public let GoogleDrive_LogoutTitle: String + public let Conversation_PinMessageAlertGroup: String + public let Settings_FAQ_Intro: String + public let PrivacySettings_AuthSessions: String + public let Tour_Title5: String + public let ChatAdmins_AllMembersAreAdmins: String + public let Group_Management_AddModeratorHelp: String + public let Channel_Username_CheckingUsername: String + public let Activity_UploadingAudio: String + private let _DialogList_SingleRecordingVideoMessageSuffix: String + private let _DialogList_SingleRecordingVideoMessageSuffix_r: [(Int, NSRange)] + public func DialogList_SingleRecordingVideoMessageSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_SingleRecordingVideoMessageSuffix, self._DialogList_SingleRecordingVideoMessageSuffix_r, [_0]) + } + private let _Contacts_AccessDeniedHelpPortrait: String + private let _Contacts_AccessDeniedHelpPortrait_r: [(Int, NSRange)] + public func Contacts_AccessDeniedHelpPortrait(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Contacts_AccessDeniedHelpPortrait, self._Contacts_AccessDeniedHelpPortrait_r, [_0]) + } + public let Channel_Info_BlackList: String + private let _Checkout_LiabilityAlert: String + private let _Checkout_LiabilityAlert_r: [(Int, NSRange)] + public func Checkout_LiabilityAlert(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Checkout_LiabilityAlert, self._Checkout_LiabilityAlert_r, [_1, _1]) + } + public let Profile_BotInfo: String + public let StickerPack_RemoveStickers: String + public let Compose_NewChannel_Members: String + public let Notification_Reply: String + public let Watch_Stickers_Recents: String + public let GroupInfo_SetGroupPhotoStop: String + public let Conversation_PinMessageAlertChannel: String + public let AttachmentMenu_File: String + private let _MESSAGE_STICKER: String + private let _MESSAGE_STICKER_r: [(Int, NSRange)] + public func MESSAGE_STICKER(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_STICKER, self._MESSAGE_STICKER_r, [_1, _2]) + } + public let Profile_MessageLifetime5s: String + private let _PINNED_PHOTO: String + private let _PINNED_PHOTO_r: [(Int, NSRange)] + public func PINNED_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_PHOTO, self._PINNED_PHOTO_r, [_1]) + } + public let Channel_EditAdmin_PermissionChangeInviteLink: String + public let Channel_AdminLog_CanAddAdmins: String + public let WelcomeScreen_Title: String + public let TwoStepAuth_SetupHint: String + public let Conversation_StatusLeftGroup: String + public let Conversation_ShareBotLocationConfirmation: String + public let Conversation_DeleteMessagesForMe: String + public let Message_PinnedAnimationMessage: String + public let Checkout_ErrorPrecheckoutFailed: String + public let Camera_PhotoMode: String + public let Channel_About_Placeholder: String + public let Channel_About_Title: String + private let _MESSAGE_PHOTO: String + private let _MESSAGE_PHOTO_r: [(Int, NSRange)] + public func MESSAGE_PHOTO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_PHOTO, self._MESSAGE_PHOTO_r, [_1]) + } + public let Calls_RatingTitle: String + public let SharedMedia_EmptyText: String + public let Channel_Username_CreateCommentsHelp: String + public let Login_PadPhoneHelp: String + public let StickerPacksSettings_ArchivedPacks: String + public let Channel_ErrorAccessDenied: String + public let Generic_ErrorMoreInfo: String + public let Notification_GroupDeactivated: String + public let Channel_AdminLog_TitleAllEvents: String + public let PrivacySettings_TouchIdTitle: String + public let ChannelMembers_WhoCanAddMembersAllHelp: String + public let ChangePhoneNumberCode_CodePlaceholder: String + public let Camera_SquareMode: String + private let _Conversation_EncryptedPlaceholderTitleOutgoing: String + private let _Conversation_EncryptedPlaceholderTitleOutgoing_r: [(Int, NSRange)] + public func Conversation_EncryptedPlaceholderTitleOutgoing(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_EncryptedPlaceholderTitleOutgoing, self._Conversation_EncryptedPlaceholderTitleOutgoing_r, [_0]) + } + public let NetworkUsageSettings_CallDataSection: String + public let Login_PadPhoneHelpTitle: String + public let Profile_CreateNewContact: String + public let AccessDenied_VideoMessageMicrophone: String + public let PhotoEditor_VignetteTool: String + public let LastSeen_WithinAWeek: String + public let Widget_NoUsers: String + public let Channel_Edit_EnableComments: String + public let DialogList_NoMessagesText: String + private let _CHANNEL_MESSAGE_AUDIO: String + private let _CHANNEL_MESSAGE_AUDIO_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_AUDIO, self._CHANNEL_MESSAGE_AUDIO_r, [_1]) + } + public let Calls_NewCall: String + public let SharedMedia_TitleFile: String + public let MaskStickerSettings_Info: String + public let Conversation_FilePhotoOrVideo: String + private let _Watch_LastSeen_AtWeekday: String + private let _Watch_LastSeen_AtWeekday_r: [(Int, NSRange)] + public func Watch_LastSeen_AtWeekday(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Watch_LastSeen_AtWeekday, self._Watch_LastSeen_AtWeekday_r, [_0]) + } + public let Channel_AdminLog_BanSendStickers: String + public let Common_Next: String + public let Watch_Notification_Joined: String + public let ImagePicker_NoVideos: String + public let GroupInfo_DeleteAndExitConfirmation: String + public let ChatSettings_Cache: String + public let TwoStepAuth_EmailInvalid: String + private let _CHAT_MESSAGE_VIDEO: String + private let _CHAT_MESSAGE_VIDEO_r: [(Int, NSRange)] + public func CHAT_MESSAGE_VIDEO(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_VIDEO, self._CHAT_MESSAGE_VIDEO_r, [_1, _2]) + } + public let Month_GenJune: String + private let _Login_EmailCodeSubject: String + private let _Login_EmailCodeSubject_r: [(Int, NSRange)] + public func Login_EmailCodeSubject(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_EmailCodeSubject, self._Login_EmailCodeSubject_r, [_0]) + } + private let _CHAT_TITLE_EDITED: String + private let _CHAT_TITLE_EDITED_r: [(Int, NSRange)] + public func CHAT_TITLE_EDITED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_TITLE_EDITED, self._CHAT_TITLE_EDITED_r, [_1, _2]) + } + public let Watch_UnlockRequired: String + private let _NetworkUsageSettings_WifiUsageSince: String + private let _NetworkUsageSettings_WifiUsageSince_r: [(Int, NSRange)] + public func NetworkUsageSettings_WifiUsageSince(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_NetworkUsageSettings_WifiUsageSince, self._NetworkUsageSettings_WifiUsageSince_r, [_0]) + } + public let Watch_LastSeen_Lately: String + public let Watch_Compose_CurrentLocation: String + private let _CHANNEL_MESSAGE_FWDS: String + private let _CHANNEL_MESSAGE_FWDS_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_FWDS(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_FWDS, self._CHANNEL_MESSAGE_FWDS_r, [_1, _2]) + } + public let DialogList_RecentTitlePeople: String + public let Conversation_ViewLocation: String + public let GroupInfo_Notifications: String + private let _MESSAGE_DOC: String + private let _MESSAGE_DOC_r: [(Int, NSRange)] + public func MESSAGE_DOC(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_DOC, self._MESSAGE_DOC_r, [_1]) + } + public let Group_Username_CreatePrivateLinkHelp: String + public let Notifications_GroupNotificationsSound: String + public let AuthSessions_EmptyTitle: String + public let Privacy_GroupsAndChannels_AlwaysAllow_Title: String + private let _MediaPicker_Nof: String + private let _MediaPicker_Nof_r: [(Int, NSRange)] + public func MediaPicker_Nof(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MediaPicker_Nof, self._MediaPicker_Nof_r, [_0]) + } + public let Common_Create: String + public let Message_InvoiceShipmentLabel: String + public let Contacts_TopSection: String + public let Your_cards_number_is_invalid: String + private let _MESSAGE_INVOICE: String + private let _MESSAGE_INVOICE_r: [(Int, NSRange)] + public func MESSAGE_INVOICE(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_INVOICE, self._MESSAGE_INVOICE_r, [_1, _2]) + } + private let _Channel_AdminLog_MessageRemovedChannelUsername: String + private let _Channel_AdminLog_MessageRemovedChannelUsername_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageRemovedChannelUsername(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageRemovedChannelUsername, self._Channel_AdminLog_MessageRemovedChannelUsername_r, [_0]) + } + public let Group_MessagePhotoRemoved: String + public let UserInfo_AddToExisting: String + private let _LastSeen_AtDate: String + private let _LastSeen_AtDate_r: [(Int, NSRange)] + public func LastSeen_AtDate(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_LastSeen_AtDate, self._LastSeen_AtDate_r, [_0]) + } + public let Conversation_MessageDialogRetry: String + public let Watch_ChatList_NoConversationsTitle: String + public let BlockedUsers_Title: String + public let MediaPicker_MomentsDateRangeYearFormat: String + public let Cache_ClearNone: String + public let Login_InvalidCodeError: String + public let Contacts_contacts: String + public let Channel_BanList_BlockedTitle: String + public let NetworkUsageSettings_Cellular: String + public let Watch_Location_Access: String + private let _CONTACT_ACTIVATED: String + private let _CONTACT_ACTIVATED_r: [(Int, NSRange)] + public func CONTACT_ACTIVATED(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CONTACT_ACTIVATED, self._CONTACT_ACTIVATED_r, [_1]) + } + public let BlockedUsers_AlreadyBlocked: String + public let PrivacySettings_DeleteAccountIfAwayFor: String + public let PrivacySettings_DeleteAccountTitle: String + public let PrivacyLastSeenSettings_CustomShareSettings_Delete: String + private let _ENCRYPTED_MESSAGE: String + private let _ENCRYPTED_MESSAGE_r: [(Int, NSRange)] + public func ENCRYPTED_MESSAGE(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_ENCRYPTED_MESSAGE, self._ENCRYPTED_MESSAGE_r, [_1]) + } + public let Watch_LastSeen_WithinAMonth: String + public let PrivacyLastSeenSettings_CustomHelp: String + public let TwoStepAuth_EnterPasswordHelp: String + public let Bot_Stop: String + public let Privacy_GroupsAndChannels_AlwaysAllow_Placeholder: String + private let _AUTH_UNKNOWN: String + private let _AUTH_UNKNOWN_r: [(Int, NSRange)] + public func AUTH_UNKNOWN(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_AUTH_UNKNOWN, self._AUTH_UNKNOWN_r, [_1]) + } + public let UserInfo_BotSettings: String + public let Your_cards_expiration_month_is_invalid: String + public let PrivacyLastSeenSettings_EmpryUsersPlaceholder: String + private let _CHANNEL_MESSAGE_ROUND: String + private let _CHANNEL_MESSAGE_ROUND_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_ROUND, self._CHANNEL_MESSAGE_ROUND_r, [_1]) + } + public let GoogleDrive_FolderLoadError: String + public let Message_VideoMessage: String + public let Conversation_ContextMenuStickerPackInfo: String + public let Login_ResetAccountProtected_LimitExceeded: String + public let Watch_Suggestion_TextInABit: String + private let _CHAT_DELETE_MEMBER: String + private let _CHAT_DELETE_MEMBER_r: [(Int, NSRange)] + public func CHAT_DELETE_MEMBER(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_DELETE_MEMBER, self._CHAT_DELETE_MEMBER_r, [_1, _2, _3]) + } + public let Conversation_EncryptedForwardingAlert: String + public let Conversation_DiscardVoiceMessageAction: String + public let PhotoEditor_CurvesBlue: String + public let Message_PinnedVideoMessage: String + private let _Settings_OpenSystemPrivacySettings: String + private let _Settings_OpenSystemPrivacySettings_r: [(Int, NSRange)] + public func Settings_OpenSystemPrivacySettings(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Settings_OpenSystemPrivacySettings, self._Settings_OpenSystemPrivacySettings_r, [_0]) + } + private let _Login_EmailPhoneSubject: String + private let _Login_EmailPhoneSubject_r: [(Int, NSRange)] + public func Login_EmailPhoneSubject(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_EmailPhoneSubject, self._Login_EmailPhoneSubject_r, [_0]) + } + public let Group_EditAdmin_PermissionChangeInfo: String + public let TwoStepAuth_Email: String + public let Map_SendMyCurrentLocation: String + private let _MESSAGE_ROUND: String + private let _MESSAGE_ROUND_r: [(Int, NSRange)] + public func MESSAGE_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_MESSAGE_ROUND, self._MESSAGE_ROUND_r, [_1]) + } + public let Map_Unknown: String + public let Wallpaper_Set: String + public let SharedMedia_CategoryLinks: String + public let AccessDenied_Title: String + public let Conversation_ClearAllConfirmation: String + public let TwoStepAuth_EmailSkipAlert: String + public let ChatSettings_Stickers: String + public let Camera_FlashOff: String + public let TwoStepAuth_Title: String + public let TwoStepAuth_SetupPasswordEnterPasswordChange: String + public let WebSearch_Images: String + public let Checkout_ErrorProviderAccountTimeout: String + public let Conversation_typing: String + public let Common_Back: String + public let Common_Search: String + private let _CancelResetAccount_Success: String + private let _CancelResetAccount_Success_r: [(Int, NSRange)] + public func CancelResetAccount_Success(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CancelResetAccount_Success, self._CancelResetAccount_Success_r, [_0]) + } + public let Common_No: String + public let Login_EmailNotConfiguredError: String + public let Watch_Suggestion_OK: String + public let Profile_AddToExisting: String + private let _PINNED_NOTEXT: String + private let _PINNED_NOTEXT_r: [(Int, NSRange)] + public func PINNED_NOTEXT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_NOTEXT, self._PINNED_NOTEXT_r, [_1]) + } + private let _Login_EmailCodeBody: String + private let _Login_EmailCodeBody_r: [(Int, NSRange)] + public func Login_EmailCodeBody(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_EmailCodeBody, self._Login_EmailCodeBody_r, [_0]) + } + public let Profile_About: String + private let _EncryptionKey_Description: String + private let _EncryptionKey_Description_r: [(Int, NSRange)] + public func EncryptionKey_Description(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_EncryptionKey_Description, self._EncryptionKey_Description_r, [_1, _2]) + } + public let Conversation_UnreadMessages: String + public let Tour_Title3: String + public let PrivacyLastSeenSettings_GroupsAndChannelsHelp: String + public let Watch_Contacts_NoResults: String + public let Watch_UserInfo_MuteTitle: String + public let MediaPicker_Choose: String + public let Conversation_DownloadMegabytes: String + private let _Privacy_GroupsAndChannels_InviteToGroupError: String + private let _Privacy_GroupsAndChannels_InviteToGroupError_r: [(Int, NSRange)] + public func Privacy_GroupsAndChannels_InviteToGroupError(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Privacy_GroupsAndChannels_InviteToGroupError, self._Privacy_GroupsAndChannels_InviteToGroupError_r, [_0, _1]) + } + private let _Message_PinnedTextMessage: String + private let _Message_PinnedTextMessage_r: [(Int, NSRange)] + public func Message_PinnedTextMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Message_PinnedTextMessage, self._Message_PinnedTextMessage_r, [_0]) + } + private let _Watch_Time_ShortWeekdayAt: String + private let _Watch_Time_ShortWeekdayAt_r: [(Int, NSRange)] + public func Watch_Time_ShortWeekdayAt(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Watch_Time_ShortWeekdayAt, self._Watch_Time_ShortWeekdayAt_r, [_1, _2]) + } + public let DialogList_Typing: String + public let Notification_CallBack: String + public let Map_LocatingError: String + public let MediaPicker_Send: String + public let ChannelIntro_Title: String + public let SearchImages_ErrorDownloadingImage: String + private let _PINNED_GIF: String + private let _PINNED_GIF_r: [(Int, NSRange)] + public func PINNED_GIF(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_GIF, self._PINNED_GIF_r, [_1]) + } + public let Profile_PhonebookAccessDisabled: String + public let LoginPassword_PasswordHelp: String + public let BlockedUsers_Unblock: String + public let Conversation_ViewFile: String + public let Notifications_GroupNotificationsAlert: String + public let Paint_Masks: String + public let StickerPack_ErrorNotFound: String + private let _PINNED_CONTACT: String + private let _PINNED_CONTACT_r: [(Int, NSRange)] + public func PINNED_CONTACT(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_CONTACT, self._PINNED_CONTACT_r, [_1]) + } + private let _Conversation_ForwardToGroupFormat: String + private let _Conversation_ForwardToGroupFormat_r: [(Int, NSRange)] + public func Conversation_ForwardToGroupFormat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_ForwardToGroupFormat, self._Conversation_ForwardToGroupFormat_r, [_0]) + } + private let _FileSize_KB: String + private let _FileSize_KB_r: [(Int, NSRange)] + public func FileSize_KB(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_FileSize_KB, self._FileSize_KB_r, [_0]) + } + public let Watch_GroupInfo_Title: String + public let PhotoEditor_Set: String + private let _Notification_Invited: String + private let _Notification_Invited_r: [(Int, NSRange)] + public func Notification_Invited(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_Invited, self._Notification_Invited_r, [_0, _1]) + } + public let Watch_AuthRequired: String + public let Conversation_EncryptedDescription1: String + public let AppleWatch_ReplyPresets: String + public let Conversation_EncryptedDescription2: String + public let NetworkUsageSettings_MediaVideoDataSection: String + public let Paint_Edit: String + public let Conversation_EncryptedDescription3: String + public let Login_CodeFloodError: String + private let _Call_EncryptionKey_Description: String + private let _Call_EncryptionKey_Description_r: [(Int, NSRange)] + public func Call_EncryptionKey_Description(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Call_EncryptionKey_Description, self._Call_EncryptionKey_Description_r, [_1, _2]) + } + public let Conversation_EncryptedDescription4: String + public let AppleWatch_Title: String + public let Conversation_StatusTyping: String + public let Contacts_AccessDeniedError: String + public let GoogleDrive_LoadErrorTitle: String + public let Share_Title: String + public let Map_Send: String + public let TwoStepAuth_ConfirmationTitle: String + public let Conversation_SupportPlaceholder: String + public let ChatSettings_Title: String + public let AuthSessions_CurrentSession: String + public let Watch_Microphone_Access: String + private let _Notification_RenamedChat: String + private let _Notification_RenamedChat_r: [(Int, NSRange)] + public func Notification_RenamedChat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_RenamedChat, self._Notification_RenamedChat_r, [_0]) + } + public let Watch_Conversation_GroupInfo: String + public let UserInfo_Title: String + public let Service_LocalizationDownloadError: String + public let Login_InfoHelp: String + public let ShareMenu_ShareTo: String + public let Message_PinnedGame: String + public let Channel_AdminLog_CanSendMessages: String + public let Notification_RenamedGroup: String + public let Weekday_Thursday: String + private let _Call_PrivacyErrorMessage: String + private let _Call_PrivacyErrorMessage_r: [(Int, NSRange)] + public func Call_PrivacyErrorMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Call_PrivacyErrorMessage, self._Call_PrivacyErrorMessage_r, [_0]) + } + public let ChangePhoneNumberNumber_Title: String + public let TwoStepAuth_EnterPasswordInvalid: String + public let DialogList_SearchSectionMessages: String + private let _Profile_ShareBotGroupFormat: String + private let _Profile_ShareBotGroupFormat_r: [(Int, NSRange)] + public func Profile_ShareBotGroupFormat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Profile_ShareBotGroupFormat, self._Profile_ShareBotGroupFormat_r, [_0]) + } + public let Preview_DeleteGif: String + public let Weekday_Saturday: String + public let UserInfo_DeleteContact: String + public let Notifications_ResetAllNotifications: String + public let Notification_MessageLifetimeRemovedOutgoing: String + public let Map_More: String + public let Login_ContinueWithLocalization: String + public let GroupInfo_AddParticipant: String + public let Watch_Location_Current: String + public let Map_MapTitle: String + public let Checkout_NewCard_SaveInfoHelp: String + public let MediaPicker_CameraRoll: String + private let _TwoStepAuth_RecoverySent: String + private let _TwoStepAuth_RecoverySent_r: [(Int, NSRange)] + public func TwoStepAuth_RecoverySent(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_TwoStepAuth_RecoverySent, self._TwoStepAuth_RecoverySent_r, [_0]) + } + public let Channel_AdminLog_CanPinMessages: String + public let KeyCommand_NewMessage: String + public let Compose_NewBroadcastButton: String + public let NetworkUsageSettings_TotalSection: String + private let _PINNED_AUDIO: String + private let _PINNED_AUDIO_r: [(Int, NSRange)] + public func PINNED_AUDIO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_AUDIO, self._PINNED_AUDIO_r, [_1]) + } + public let Privacy_GroupsAndChannels: String + public let Conversation_DiscardVoiceMessageDescription: String + private let _Notification_ChangedGroupPhoto: String + private let _Notification_ChangedGroupPhoto_r: [(Int, NSRange)] + public func Notification_ChangedGroupPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_ChangedGroupPhoto, self._Notification_ChangedGroupPhoto_r, [_0]) + } + public let TwoStepAuth_RemovePassword: String + public let Privacy_GroupsAndChannels_CustomHelp: String + public let Notification_GroupMigratedToChannel: String + public let UserInfo_NotificationsDisable: String + public let Watch_UserInfo_Service: String + public let Privacy_Calls_CustomHelp: String + public let ChangePhoneNumberCode_Code: String + public let UserInfo_Invite: String + public let CheckoutInfo_ErrorStateInvalid: String + public let DialogList_ClearHistoryConfirmation: String + public let CheckoutInfo_ErrorEmailInvalid: String + public let Month_GenNovember: String + public let PhotoEditor_TintIntensity: String + public let UserInfo_NotificationsEnable: String + private let _Target_InviteToGroupConfirmation: String + private let _Target_InviteToGroupConfirmation_r: [(Int, NSRange)] + public func Target_InviteToGroupConfirmation(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Target_InviteToGroupConfirmation, self._Target_InviteToGroupConfirmation_r, [_0]) + } + public let Map_Map: String + public let Map_OpenInMaps: String + public let Common_OK: String + public let TwoStepAuth_SetupHintTitle: String + public let Watch_Suggestion_Nope: String + public let GroupInfo_LeftStatus: String + public let Cache_ClearProgress: String + public let Login_InvalidPhoneError: String + public let Cache_ClearEmpty: String + public let Map_Search: String + public let ChannelMembers_GroupAdminsTitle: String + private let _Channel_AdminLog_MessageRemovedGroupUsername: String + private let _Channel_AdminLog_MessageRemovedGroupUsername_r: [(Int, NSRange)] + public func Channel_AdminLog_MessageRemovedGroupUsername(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_AdminLog_MessageRemovedGroupUsername, self._Channel_AdminLog_MessageRemovedGroupUsername_r, [_0]) + } + public let ChatSettings_AutomaticPhotoDownload: String + public let Update_Update: String + public let Group_ErrorAddTooMuchAdmins: String + public let Login_SelectCountry_Title: String + public let Notification_EncryptedChatAccepted: String + public let Notifications_GroupNotificationsHelp: String + public let PhotoEditor_CropAspectRatioSquare: String + public let Notification_CallOutgoing: String + public let Weekday_ShortMonday: String + public let Channel_Edit_AboutItem: String + public let Checkout_Receipt_Title: String + public let Login_InfoLastNamePlaceholder: String + public let Contacts_InvitationText: String + public let Channel_Members_AddMembersHelp: String + public let ReportPeer_Report: String + public let Channel_EditMessageErrorGeneric: String + public let LoginPassword_FloodError: String + public let EncryptionKey_TapToEmojify: String + public let Conversation_InfoChannel: String + public let TwoStepAuth_SetupPasswordTitle: String + public let PhotoEditor_DiscardChanges: String + public let Group_UpgradeNoticeText2: String + private let _PINNED_ROUND: String + private let _PINNED_ROUND_r: [(Int, NSRange)] + public func PINNED_ROUND(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_ROUND, self._PINNED_ROUND_r, [_1]) + } + private let _ChannelInfo_ChannelForbidden: String + private let _ChannelInfo_ChannelForbidden_r: [(Int, NSRange)] + public func ChannelInfo_ChannelForbidden(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_ChannelInfo_ChannelForbidden, self._ChannelInfo_ChannelForbidden_r, [_0]) + } + public let Conversation_ShareMyContactInfo: String + private let _Profile_ShareContactPersonFormat: String + private let _Profile_ShareContactPersonFormat_r: [(Int, NSRange)] + public func Profile_ShareContactPersonFormat(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Profile_ShareContactPersonFormat, self._Profile_ShareContactPersonFormat_r, [_0]) + } + private let _CHANNEL_MESSAGE_GEO: String + private let _CHANNEL_MESSAGE_GEO_r: [(Int, NSRange)] + public func CHANNEL_MESSAGE_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHANNEL_MESSAGE_GEO, self._CHANNEL_MESSAGE_GEO_r, [_1]) + } + public let Group_Info_AdminLog: String + public let StickerPacksSettings_FeaturedPacks: String + public let Month_GenAugust: String + public let Channel_Username_CreatePublicLinkHelp: String + public let StickerPack_Send: String + public let Watch_Suggestion_HoldOn: String + public let StickerSettings_MaskContextInfo: String + public let AttachmentMenu_ImageSearch: String + private let _PINNED_GEO: String + private let _PINNED_GEO_r: [(Int, NSRange)] + public func PINNED_GEO(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PINNED_GEO, self._PINNED_GEO_r, [_1]) + } + public let PasscodeSettings_EncryptData: String + public let Notification_CallCanceled: String + public let Common_NotNow: String + public let PasscodeSettings_Title: String + public let StickerPack_BuiltinPackName: String + public let Watch_Suggestion_BRB: String + public let Login_CodeTitle: String + private let _CHAT_MESSAGE_ROUND: String + private let _CHAT_MESSAGE_ROUND_r: [(Int, NSRange)] + public func CHAT_MESSAGE_ROUND(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_ROUND, self._CHAT_MESSAGE_ROUND_r, [_1, _2]) + } + public let Notifications_MessageNotificationsAlert: String + public let Username_InvalidCharacters: String + public let GroupInfo_LabelAdmin: String + public let GroupInfo_Sound: String + public let Channel_EditAdmin_PermissionBanUsers: String + public let Wallpaper_PhotoLibrary: String + public let Settings_About: String + private let _CHAT_LEFT: String + private let _CHAT_LEFT_r: [(Int, NSRange)] + public func CHAT_LEFT(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_LEFT, self._CHAT_LEFT_r, [_1, _2]) + } + public let LoginPassword_ForgotPassword: String + private let _DialogList_AwaitingEncryption: String + private let _DialogList_AwaitingEncryption_r: [(Int, NSRange)] + public func DialogList_AwaitingEncryption(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_AwaitingEncryption, self._DialogList_AwaitingEncryption_r, [_0]) + } + public let ChatSettings_Appearance: String + public let Tour_Title1: String + private let _Notification_ChangedUserPhoto: String + private let _Notification_ChangedUserPhoto_r: [(Int, NSRange)] + public func Notification_ChangedUserPhoto(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_ChangedUserPhoto, self._Notification_ChangedUserPhoto_r, [_0]) + } + public let Conversation_LinkDialogCopy: String + private let _Notification_PinnedLocationMessage: String + private let _Notification_PinnedLocationMessage_r: [(Int, NSRange)] + public func Notification_PinnedLocationMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedLocationMessage, self._Notification_PinnedLocationMessage_r, [_0]) + } + private let _Notification_PinnedPhotoMessage: String + private let _Notification_PinnedPhotoMessage_r: [(Int, NSRange)] + public func Notification_PinnedPhotoMessage(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_PinnedPhotoMessage, self._Notification_PinnedPhotoMessage_r, [_0]) + } + private let _DownloadingStatus: String + private let _DownloadingStatus_r: [(Int, NSRange)] + public func DownloadingStatus(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DownloadingStatus, self._DownloadingStatus_r, [_0, _1]) + } + public let Calls_All: String + private let _Channel_MessageTitleUpdated: String + private let _Channel_MessageTitleUpdated_r: [(Int, NSRange)] + public func Channel_MessageTitleUpdated(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_MessageTitleUpdated, self._Channel_MessageTitleUpdated_r, [_0]) + } + public let Call_CallAgain: String + public let TwoStepAuth_RecoveryCodeHelp: String + public let UserInfo_SendMessage: String + private let _Channel_Username_LinkHint: String + private let _Channel_Username_LinkHint_r: [(Int, NSRange)] + public func Channel_Username_LinkHint(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Channel_Username_LinkHint, self._Channel_Username_LinkHint_r, [_0]) + } + public let Paint_RecentStickers: String + public let Login_CallRequestState3: String + public let Channel_Edit_LinkItem: String + public let CallSettings_Title: String + public let ChangePhoneNumberNumber_Help: String + public let Watch_Suggestion_Thanks: String + public let Channel_Moderator_Title: String + public let Message_PinnedPhotoMessage: String + public let Notification_SecretChatScreenshot: String + private let _Conversation_DeleteMessagesFor: String + private let _Conversation_DeleteMessagesFor_r: [(Int, NSRange)] + public func Conversation_DeleteMessagesFor(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_DeleteMessagesFor, self._Conversation_DeleteMessagesFor_r, [_0]) + } + public let Activity_UploadingDocument: String + public let Watch_ChatList_NoConversationsText: String + public let ReportPeer_AlertSuccess: String + public let Tour_Text4: String + public let Channel_Info_Description: String + public let AccessDenied_LocationTracking: String + public let MessageTimer_Title: String + public let Watch_Compose_Send: String + public let Preview_CopyAddress: String + public let Settings_BlockedUsers: String + public let Month_ShortAugust: String + public let Channel_AdminLogFilter_AdminsTitle: String + public let Channel_EditAdmin_PermissionChangeInfo: String + public let Notifications_ResetAllNotificationsHelp: String + public let DialogList_EncryptionRejected: String + public let AccessDenied_CameraRestricted: String + public let Target_InviteToGroupErrorAlreadyInvited: String + public let Watch_Message_ForwardedFrom: String + public let Channel_AboutItem: String + public let PhotoEditor_CurvesGreen: String + public let CheckoutInfo_ShippingInfoCountryPlaceholder: String + public let Month_GenJuly: String + public let Conversation_DeleteChat: String + private let _DialogList_SingleUploadingFileSuffix: String + private let _DialogList_SingleUploadingFileSuffix_r: [(Int, NSRange)] + public func DialogList_SingleUploadingFileSuffix(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_DialogList_SingleUploadingFileSuffix, self._DialogList_SingleUploadingFileSuffix_r, [_0]) + } + public let ChannelIntro_CreateChannel: String + public let WelcomeScreen_ContactsAccessDisabled: String + public let Channel_Management_AddModerator: String + public let Common_ChoosePhoto: String + public let Group_Username_Help: String + public let Conversation_Pin: String + public let Channel_AdminLog_CanStartCalls: String + private let _Login_ResetAccountProtected_Text: String + private let _Login_ResetAccountProtected_Text_r: [(Int, NSRange)] + public func Login_ResetAccountProtected_Text(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_ResetAccountProtected_Text, self._Login_ResetAccountProtected_Text_r, [_0]) + } + public let Camera_TapAndHoldForVideo: String + public let Bot_DescriptionTitle: String + public let FeaturedStickerPacks_Title: String + public let Map_OpenInGoogleMaps: String + public let Notification_MessageLifetime5s: String + public let EnterPasscode_SetupTitle: String + public let Contacts_Title: String + public let Channel_Management_AddModeratorHelp: String + private let _CHAT_MESSAGE_FWDS: String + private let _CHAT_MESSAGE_FWDS_r: [(Int, NSRange)] + public func CHAT_MESSAGE_FWDS(_ _1: String, _ _2: String, _ _3: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_MESSAGE_FWDS, self._CHAT_MESSAGE_FWDS_r, [_1, _2, _3]) + } + public let WelcomeScreen_UpdatingTitle: String + private let _Login_CodeHelp: String + private let _Login_CodeHelp_r: [(Int, NSRange)] + public func Login_CodeHelp(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Login_CodeHelp, self._Login_CodeHelp_r, [_0]) + } + public let Conversation_MessageDialogEdit: String + public let PrivacyLastSeenSettings_Title: String + public let Notifications_ClassicTones: String + public let GoogleDrive_Title: String + public let Conversation_LinkDialogOpen: String + public let Conversation_ClousStorageInfo_Description4: String + public let Privacy_Calls_AlwaysAllow: String + public let Privacy_PaymentsClearInfoHelp: String + public let Notification_MessageLifetime1h: String + private let _Notification_CreatedChatWithTitle: String + private let _Notification_CreatedChatWithTitle_r: [(Int, NSRange)] + public func Notification_CreatedChatWithTitle(_ _0: String, _ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Notification_CreatedChatWithTitle, self._Notification_CreatedChatWithTitle_r, [_0, _1]) + } + public let CheckoutInfo_ReceiverInfoEmail: String + public let LastSeen_Lately: String + public let Month_ShortApril: String + public let ConversationProfile_ErrorCreatingConversation: String + private let _PHONE_CALL_MISSED: String + private let _PHONE_CALL_MISSED_r: [(Int, NSRange)] + public func PHONE_CALL_MISSED(_ _1: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_PHONE_CALL_MISSED, self._PHONE_CALL_MISSED_r, [_1]) + } + public let Map_AccessDeniedError: String + private let _Conversation_Kilobytes: String + private let _Conversation_Kilobytes_r: [(Int, NSRange)] + public func Conversation_Kilobytes(_ _0: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_Kilobytes, self._Conversation_Kilobytes_r, ["\(_0)"]) + } + public let Group_ErrorAddBlocked: String + public let MediaPicker_Videos: String + public let BlockedUsers_AddNew: String + public let StickerPacksSettings_StickerPacksSection: String + public let Channel_NotificationLoading: String + private let _CHAT_RETURNED: String + private let _CHAT_RETURNED_r: [(Int, NSRange)] + public func CHAT_RETURNED(_ _1: String, _ _2: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_CHAT_RETURNED, self._CHAT_RETURNED_r, [_1, _2]) + } + public let PhotoEditor_ShadowsTint: String + public let ExplicitContent_AlertTitle: String + public let Channel_AdminLogFilter_EventsLeaving: String + public let StickerPack_HideStickers: String + private let _Group_MessageTitleUpdated: String + private let _Group_MessageTitleUpdated_r: [(Int, NSRange)] + public func Group_MessageTitleUpdated(_ _0: String) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Group_MessageTitleUpdated, self._Group_MessageTitleUpdated_r, [_0]) + } + public let Checkout_EnterPassword: String + public let UserInfo_NotificationsEnabled: String + public let Weekday_ShortTuesday: String + public let Notification_CallIncomingShort: String + public let ConvertToSupergroup_Note: String + public let Conversation_EmptyPlaceholder: String + public let Conversation_BroadcastTitle: String + public let Username_Help: String + public let StickerSettings_ContextHide: String + public let Weekday_Sunday: String + public let Preview_LoadingImage: String + private let _Conversation_DownloadProgressKilobytes: String + private let _Conversation_DownloadProgressKilobytes_r: [(Int, NSRange)] + public func Conversation_DownloadProgressKilobytes(_ _1: Int, _ _2: Int) -> (String, [(Int, NSRange)]) { + return formatWithArgumentRanges(_Conversation_DownloadProgressKilobytes, self._Conversation_DownloadProgressKilobytes_r, ["\(_1)", "\(_2)"]) + } + public let Settings_ChatBackground: String + private let _MessageTimer_Seconds_zero: String + private let _MessageTimer_Seconds_one: String + private let _MessageTimer_Seconds_two: String + private let _MessageTimer_Seconds_few: String + private let _MessageTimer_Seconds_many: String + private let _MessageTimer_Seconds_other: String + public func MessageTimer_Seconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Seconds_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Seconds_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Seconds_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Seconds_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Seconds_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Seconds_other, "\(value)") + } + } + private let _Call_Seconds_zero: String + private let _Call_Seconds_one: String + private let _Call_Seconds_two: String + private let _Call_Seconds_few: String + private let _Call_Seconds_many: String + private let _Call_Seconds_other: String + public func Call_Seconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Call_Seconds_zero, "\(value)") + case .one: + return String(format: self._Call_Seconds_one, "\(value)") + case .two: + return String(format: self._Call_Seconds_two, "\(value)") + case .few: + return String(format: self._Call_Seconds_few, "\(value)") + case .many: + return String(format: self._Call_Seconds_many, "\(value)") + case .other: + return String(format: self._Call_Seconds_other, "\(value)") + } + } + private let _MessageTimer_ShortSeconds_zero: String + private let _MessageTimer_ShortSeconds_one: String + private let _MessageTimer_ShortSeconds_two: String + private let _MessageTimer_ShortSeconds_few: String + private let _MessageTimer_ShortSeconds_many: String + private let _MessageTimer_ShortSeconds_other: String + public func MessageTimer_ShortSeconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortSeconds_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortSeconds_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortSeconds_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortSeconds_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortSeconds_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortSeconds_other, "\(value)") + } + } + private let _Notification_GameScoreSimple_zero: String + private let _Notification_GameScoreSimple_one: String + private let _Notification_GameScoreSimple_two: String + private let _Notification_GameScoreSimple_few: String + private let _Notification_GameScoreSimple_many: String + private let _Notification_GameScoreSimple_other: String + public func Notification_GameScoreSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreSimple_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreSimple_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreSimple_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreSimple_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreSimple_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreSimple_other, "\(value)") + } + } + private let _Notification_GameScoreExtended_zero: String + private let _Notification_GameScoreExtended_one: String + private let _Notification_GameScoreExtended_two: String + private let _Notification_GameScoreExtended_few: String + private let _Notification_GameScoreExtended_many: String + private let _Notification_GameScoreExtended_other: String + public func Notification_GameScoreExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreExtended_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreExtended_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreExtended_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreExtended_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreExtended_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreExtended_other, "\(value)") + } + } + private let _PasscodeSettings_FailedAttempts_zero: String + private let _PasscodeSettings_FailedAttempts_one: String + private let _PasscodeSettings_FailedAttempts_two: String + private let _PasscodeSettings_FailedAttempts_few: String + private let _PasscodeSettings_FailedAttempts_many: String + private let _PasscodeSettings_FailedAttempts_other: String + public func PasscodeSettings_FailedAttempts(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._PasscodeSettings_FailedAttempts_zero, "\(value)") + case .one: + return String(format: self._PasscodeSettings_FailedAttempts_one, "\(value)") + case .two: + return String(format: self._PasscodeSettings_FailedAttempts_two, "\(value)") + case .few: + return String(format: self._PasscodeSettings_FailedAttempts_few, "\(value)") + case .many: + return String(format: self._PasscodeSettings_FailedAttempts_many, "\(value)") + case .other: + return String(format: self._PasscodeSettings_FailedAttempts_other, "\(value)") + } + } + private let _MuteFor_Hours_zero: String + private let _MuteFor_Hours_one: String + private let _MuteFor_Hours_two: String + private let _MuteFor_Hours_few: String + private let _MuteFor_Hours_many: String + private let _MuteFor_Hours_other: String + public func MuteFor_Hours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteFor_Hours_zero, "\(value)") + case .one: + return String(format: self._MuteFor_Hours_one, "\(value)") + case .two: + return String(format: self._MuteFor_Hours_two, "\(value)") + case .few: + return String(format: self._MuteFor_Hours_few, "\(value)") + case .many: + return String(format: self._MuteFor_Hours_many, "\(value)") + case .other: + return String(format: self._MuteFor_Hours_other, "\(value)") + } + } + private let _MessageTimer_ShortMinutes_zero: String + private let _MessageTimer_ShortMinutes_one: String + private let _MessageTimer_ShortMinutes_two: String + private let _MessageTimer_ShortMinutes_few: String + private let _MessageTimer_ShortMinutes_many: String + private let _MessageTimer_ShortMinutes_other: String + public func MessageTimer_ShortMinutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortMinutes_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortMinutes_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortMinutes_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortMinutes_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortMinutes_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortMinutes_other, "\(value)") + } + } + private let _Notification_GameScoreSelfExtended_zero: String + private let _Notification_GameScoreSelfExtended_one: String + private let _Notification_GameScoreSelfExtended_two: String + private let _Notification_GameScoreSelfExtended_few: String + private let _Notification_GameScoreSelfExtended_many: String + private let _Notification_GameScoreSelfExtended_other: String + public func Notification_GameScoreSelfExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreSelfExtended_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreSelfExtended_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreSelfExtended_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreSelfExtended_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreSelfExtended_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreSelfExtended_other, "\(value)") + } + } + private let _MessageTimer_ShortDays_zero: String + private let _MessageTimer_ShortDays_one: String + private let _MessageTimer_ShortDays_two: String + private let _MessageTimer_ShortDays_few: String + private let _MessageTimer_ShortDays_many: String + private let _MessageTimer_ShortDays_other: String + public func MessageTimer_ShortDays(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortDays_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortDays_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortDays_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortDays_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortDays_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortDays_other, "\(value)") + } + } + private let _GroupInfo_ParticipantCount_zero: String + private let _GroupInfo_ParticipantCount_one: String + private let _GroupInfo_ParticipantCount_two: String + private let _GroupInfo_ParticipantCount_few: String + private let _GroupInfo_ParticipantCount_many: String + private let _GroupInfo_ParticipantCount_other: String + public func GroupInfo_ParticipantCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._GroupInfo_ParticipantCount_zero, "\(value)") + case .one: + return String(format: self._GroupInfo_ParticipantCount_one, "\(value)") + case .two: + return String(format: self._GroupInfo_ParticipantCount_two, "\(value)") + case .few: + return String(format: self._GroupInfo_ParticipantCount_few, "\(value)") + case .many: + return String(format: self._GroupInfo_ParticipantCount_many, "\(value)") + case .other: + return String(format: self._GroupInfo_ParticipantCount_other, "\(value)") + } + } + private let _ForwardedPhotos_zero: String + private let _ForwardedPhotos_one: String + private let _ForwardedPhotos_two: String + private let _ForwardedPhotos_few: String + private let _ForwardedPhotos_many: String + private let _ForwardedPhotos_other: String + public func ForwardedPhotos(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedPhotos_zero, "\(value)") + case .one: + return String(format: self._ForwardedPhotos_one, "\(value)") + case .two: + return String(format: self._ForwardedPhotos_two, "\(value)") + case .few: + return String(format: self._ForwardedPhotos_few, "\(value)") + case .many: + return String(format: self._ForwardedPhotos_many, "\(value)") + case .other: + return String(format: self._ForwardedPhotos_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreSelfExtended_zero: String + private let _ServiceMessage_GameScoreSelfExtended_one: String + private let _ServiceMessage_GameScoreSelfExtended_two: String + private let _ServiceMessage_GameScoreSelfExtended_few: String + private let _ServiceMessage_GameScoreSelfExtended_many: String + private let _ServiceMessage_GameScoreSelfExtended_other: String + public func ServiceMessage_GameScoreSelfExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreSelfExtended_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreSelfExtended_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreSelfExtended_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreSelfExtended_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreSelfExtended_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreSelfExtended_other, "\(value)") + } + } + private let _Call_ShortSeconds_zero: String + private let _Call_ShortSeconds_one: String + private let _Call_ShortSeconds_two: String + private let _Call_ShortSeconds_few: String + private let _Call_ShortSeconds_many: String + private let _Call_ShortSeconds_other: String + public func Call_ShortSeconds(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Call_ShortSeconds_zero, "\(value)") + case .one: + return String(format: self._Call_ShortSeconds_one, "\(value)") + case .two: + return String(format: self._Call_ShortSeconds_two, "\(value)") + case .few: + return String(format: self._Call_ShortSeconds_few, "\(value)") + case .many: + return String(format: self._Call_ShortSeconds_many, "\(value)") + case .other: + return String(format: self._Call_ShortSeconds_other, "\(value)") + } + } + private let _Time_PreciseDate_zero: String + private let _Time_PreciseDate_one: String + private let _Time_PreciseDate_two: String + private let _Time_PreciseDate_few: String + private let _Time_PreciseDate_many: String + private let _Time_PreciseDate_other: String + public func Time_PreciseDate(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Time_PreciseDate_zero, "\(value)") + case .one: + return String(format: self._Time_PreciseDate_one, "\(value)") + case .two: + return String(format: self._Time_PreciseDate_two, "\(value)") + case .few: + return String(format: self._Time_PreciseDate_few, "\(value)") + case .many: + return String(format: self._Time_PreciseDate_many, "\(value)") + case .other: + return String(format: self._Time_PreciseDate_other, "\(value)") + } + } + private let _SharedMedia_File_zero: String + private let _SharedMedia_File_one: String + private let _SharedMedia_File_two: String + private let _SharedMedia_File_few: String + private let _SharedMedia_File_many: String + private let _SharedMedia_File_other: String + public func SharedMedia_File(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_File_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_File_one, "\(value)") + case .two: + return String(format: self._SharedMedia_File_two, "\(value)") + case .few: + return String(format: self._SharedMedia_File_few, "\(value)") + case .many: + return String(format: self._SharedMedia_File_many, "\(value)") + case .other: + return String(format: self._SharedMedia_File_other, "\(value)") + } + } + private let _PasscodeSettings_AutoLock_IfAwayFor_zero: String + private let _PasscodeSettings_AutoLock_IfAwayFor_one: String + private let _PasscodeSettings_AutoLock_IfAwayFor_two: String + private let _PasscodeSettings_AutoLock_IfAwayFor_few: String + private let _PasscodeSettings_AutoLock_IfAwayFor_many: String + private let _PasscodeSettings_AutoLock_IfAwayFor_other: String + public func PasscodeSettings_AutoLock_IfAwayFor(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_zero, "\(value)") + case .one: + return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_one, "\(value)") + case .two: + return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_two, "\(value)") + case .few: + return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_few, "\(value)") + case .many: + return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_many, "\(value)") + case .other: + return String(format: self._PasscodeSettings_AutoLock_IfAwayFor_other, "\(value)") + } + } + private let _ForwardedAudios_zero: String + private let _ForwardedAudios_one: String + private let _ForwardedAudios_two: String + private let _ForwardedAudios_few: String + private let _ForwardedAudios_many: String + private let _ForwardedAudios_other: String + public func ForwardedAudios(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedAudios_zero, "\(value)") + case .one: + return String(format: self._ForwardedAudios_one, "\(value)") + case .two: + return String(format: self._ForwardedAudios_two, "\(value)") + case .few: + return String(format: self._ForwardedAudios_few, "\(value)") + case .many: + return String(format: self._ForwardedAudios_many, "\(value)") + case .other: + return String(format: self._ForwardedAudios_other, "\(value)") + } + } + private let _PrivacyLastSeenSettings_AddUsers_zero: String + private let _PrivacyLastSeenSettings_AddUsers_one: String + private let _PrivacyLastSeenSettings_AddUsers_two: String + private let _PrivacyLastSeenSettings_AddUsers_few: String + private let _PrivacyLastSeenSettings_AddUsers_many: String + private let _PrivacyLastSeenSettings_AddUsers_other: String + public func PrivacyLastSeenSettings_AddUsers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._PrivacyLastSeenSettings_AddUsers_zero, "\(value)") + case .one: + return String(format: self._PrivacyLastSeenSettings_AddUsers_one, "\(value)") + case .two: + return String(format: self._PrivacyLastSeenSettings_AddUsers_two, "\(value)") + case .few: + return String(format: self._PrivacyLastSeenSettings_AddUsers_few, "\(value)") + case .many: + return String(format: self._PrivacyLastSeenSettings_AddUsers_many, "\(value)") + case .other: + return String(format: self._PrivacyLastSeenSettings_AddUsers_other, "\(value)") + } + } + private let _MuteFor_Weeks_zero: String + private let _MuteFor_Weeks_one: String + private let _MuteFor_Weeks_two: String + private let _MuteFor_Weeks_few: String + private let _MuteFor_Weeks_many: String + private let _MuteFor_Weeks_other: String + public func MuteFor_Weeks(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteFor_Weeks_zero, "\(value)") + case .one: + return String(format: self._MuteFor_Weeks_one, "\(value)") + case .two: + return String(format: self._MuteFor_Weeks_two, "\(value)") + case .few: + return String(format: self._MuteFor_Weeks_few, "\(value)") + case .many: + return String(format: self._MuteFor_Weeks_many, "\(value)") + case .other: + return String(format: self._MuteFor_Weeks_other, "\(value)") + } + } + private let _ForwardedVideoMessages_zero: String + private let _ForwardedVideoMessages_one: String + private let _ForwardedVideoMessages_two: String + private let _ForwardedVideoMessages_few: String + private let _ForwardedVideoMessages_many: String + private let _ForwardedVideoMessages_other: String + public func ForwardedVideoMessages(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedVideoMessages_zero, "\(value)") + case .one: + return String(format: self._ForwardedVideoMessages_one, "\(value)") + case .two: + return String(format: self._ForwardedVideoMessages_two, "\(value)") + case .few: + return String(format: self._ForwardedVideoMessages_few, "\(value)") + case .many: + return String(format: self._ForwardedVideoMessages_many, "\(value)") + case .other: + return String(format: self._ForwardedVideoMessages_other, "\(value)") + } + } + private let _SharedMedia_Generic_zero: String + private let _SharedMedia_Generic_one: String + private let _SharedMedia_Generic_two: String + private let _SharedMedia_Generic_few: String + private let _SharedMedia_Generic_many: String + private let _SharedMedia_Generic_other: String + public func SharedMedia_Generic(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_Generic_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_Generic_one, "\(value)") + case .two: + return String(format: self._SharedMedia_Generic_two, "\(value)") + case .few: + return String(format: self._SharedMedia_Generic_few, "\(value)") + case .many: + return String(format: self._SharedMedia_Generic_many, "\(value)") + case .other: + return String(format: self._SharedMedia_Generic_other, "\(value)") + } + } + private let _Conversation_StatusMembers_zero: String + private let _Conversation_StatusMembers_one: String + private let _Conversation_StatusMembers_two: String + private let _Conversation_StatusMembers_few: String + private let _Conversation_StatusMembers_many: String + private let _Conversation_StatusMembers_other: String + public func Conversation_StatusMembers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_StatusMembers_zero, "\(value)") + case .one: + return String(format: self._Conversation_StatusMembers_one, "\(value)") + case .two: + return String(format: self._Conversation_StatusMembers_two, "\(value)") + case .few: + return String(format: self._Conversation_StatusMembers_few, "\(value)") + case .many: + return String(format: self._Conversation_StatusMembers_many, "\(value)") + case .other: + return String(format: self._Conversation_StatusMembers_other, "\(value)") + } + } + private let _Invitation_Members_zero: String + private let _Invitation_Members_one: String + private let _Invitation_Members_two: String + private let _Invitation_Members_few: String + private let _Invitation_Members_many: String + private let _Invitation_Members_other: String + public func Invitation_Members(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Invitation_Members_zero, "\(value)") + case .one: + return String(format: self._Invitation_Members_one, "\(value)") + case .two: + return String(format: self._Invitation_Members_two, "\(value)") + case .few: + return String(format: self._Invitation_Members_few, "\(value)") + case .many: + return String(format: self._Invitation_Members_many, "\(value)") + case .other: + return String(format: self._Invitation_Members_other, "\(value)") + } + } + private let _ForwardedFiles_zero: String + private let _ForwardedFiles_one: String + private let _ForwardedFiles_two: String + private let _ForwardedFiles_few: String + private let _ForwardedFiles_many: String + private let _ForwardedFiles_other: String + public func ForwardedFiles(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedFiles_zero, "\(value)") + case .one: + return String(format: self._ForwardedFiles_one, "\(value)") + case .two: + return String(format: self._ForwardedFiles_two, "\(value)") + case .few: + return String(format: self._ForwardedFiles_few, "\(value)") + case .many: + return String(format: self._ForwardedFiles_many, "\(value)") + case .other: + return String(format: self._ForwardedFiles_other, "\(value)") + } + } + private let _ForwardedStickers_zero: String + private let _ForwardedStickers_one: String + private let _ForwardedStickers_two: String + private let _ForwardedStickers_few: String + private let _ForwardedStickers_many: String + private let _ForwardedStickers_other: String + public func ForwardedStickers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedStickers_zero, "\(value)") + case .one: + return String(format: self._ForwardedStickers_one, "\(value)") + case .two: + return String(format: self._ForwardedStickers_two, "\(value)") + case .few: + return String(format: self._ForwardedStickers_few, "\(value)") + case .many: + return String(format: self._ForwardedStickers_many, "\(value)") + case .other: + return String(format: self._ForwardedStickers_other, "\(value)") + } + } + private let _StickerPack_StickerCount_zero: String + private let _StickerPack_StickerCount_one: String + private let _StickerPack_StickerCount_two: String + private let _StickerPack_StickerCount_few: String + private let _StickerPack_StickerCount_many: String + private let _StickerPack_StickerCount_other: String + public func StickerPack_StickerCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_StickerCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_StickerCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_StickerCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_StickerCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_StickerCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_StickerCount_other, "\(value)") + } + } + private let _SharedMedia_Video_zero: String + private let _SharedMedia_Video_one: String + private let _SharedMedia_Video_two: String + private let _SharedMedia_Video_few: String + private let _SharedMedia_Video_many: String + private let _SharedMedia_Video_other: String + public func SharedMedia_Video(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_Video_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_Video_one, "\(value)") + case .two: + return String(format: self._SharedMedia_Video_two, "\(value)") + case .few: + return String(format: self._SharedMedia_Video_few, "\(value)") + case .many: + return String(format: self._SharedMedia_Video_many, "\(value)") + case .other: + return String(format: self._SharedMedia_Video_other, "\(value)") + } + } + private let _ForwardedAuthorsOthers_zero: String + private let _ForwardedAuthorsOthers_one: String + private let _ForwardedAuthorsOthers_two: String + private let _ForwardedAuthorsOthers_few: String + private let _ForwardedAuthorsOthers_many: String + private let _ForwardedAuthorsOthers_other: String + public func ForwardedAuthorsOthers(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedAuthorsOthers_zero, "\(value)") + case .one: + return String(format: self._ForwardedAuthorsOthers_one, "\(value)") + case .two: + return String(format: self._ForwardedAuthorsOthers_two, "\(value)") + case .few: + return String(format: self._ForwardedAuthorsOthers_few, "\(value)") + case .many: + return String(format: self._ForwardedAuthorsOthers_many, "\(value)") + case .other: + return String(format: self._ForwardedAuthorsOthers_other, "\(value)") + } + } + private let _MuteFor_Minutes_zero: String + private let _MuteFor_Minutes_one: String + private let _MuteFor_Minutes_two: String + private let _MuteFor_Minutes_few: String + private let _MuteFor_Minutes_many: String + private let _MuteFor_Minutes_other: String + public func MuteFor_Minutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteFor_Minutes_zero, "\(value)") + case .one: + return String(format: self._MuteFor_Minutes_one, "\(value)") + case .two: + return String(format: self._MuteFor_Minutes_two, "\(value)") + case .few: + return String(format: self._MuteFor_Minutes_few, "\(value)") + case .many: + return String(format: self._MuteFor_Minutes_many, "\(value)") + case .other: + return String(format: self._MuteFor_Minutes_other, "\(value)") + } + } + private let _AttachmentMenu_SendVideo_zero: String + private let _AttachmentMenu_SendVideo_one: String + private let _AttachmentMenu_SendVideo_two: String + private let _AttachmentMenu_SendVideo_few: String + private let _AttachmentMenu_SendVideo_many: String + private let _AttachmentMenu_SendVideo_other: String + public func AttachmentMenu_SendVideo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._AttachmentMenu_SendVideo_zero, "\(value)") + case .one: + return String(format: self._AttachmentMenu_SendVideo_one, "\(value)") + case .two: + return String(format: self._AttachmentMenu_SendVideo_two, "\(value)") + case .few: + return String(format: self._AttachmentMenu_SendVideo_few, "\(value)") + case .many: + return String(format: self._AttachmentMenu_SendVideo_many, "\(value)") + case .other: + return String(format: self._AttachmentMenu_SendVideo_other, "\(value)") + } + } + private let _Call_Minutes_zero: String + private let _Call_Minutes_one: String + private let _Call_Minutes_two: String + private let _Call_Minutes_few: String + private let _Call_Minutes_many: String + private let _Call_Minutes_other: String + public func Call_Minutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Call_Minutes_zero, "\(value)") + case .one: + return String(format: self._Call_Minutes_one, "\(value)") + case .two: + return String(format: self._Call_Minutes_two, "\(value)") + case .few: + return String(format: self._Call_Minutes_few, "\(value)") + case .many: + return String(format: self._Call_Minutes_many, "\(value)") + case .other: + return String(format: self._Call_Minutes_other, "\(value)") + } + } + private let _ForwardedContacts_zero: String + private let _ForwardedContacts_one: String + private let _ForwardedContacts_two: String + private let _ForwardedContacts_few: String + private let _ForwardedContacts_many: String + private let _ForwardedContacts_other: String + public func ForwardedContacts(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedContacts_zero, "\(value)") + case .one: + return String(format: self._ForwardedContacts_one, "\(value)") + case .two: + return String(format: self._ForwardedContacts_two, "\(value)") + case .few: + return String(format: self._ForwardedContacts_few, "\(value)") + case .many: + return String(format: self._ForwardedContacts_many, "\(value)") + case .other: + return String(format: self._ForwardedContacts_other, "\(value)") + } + } + private let _Channel_NotificationComments_zero: String + private let _Channel_NotificationComments_one: String + private let _Channel_NotificationComments_two: String + private let _Channel_NotificationComments_few: String + private let _Channel_NotificationComments_many: String + private let _Channel_NotificationComments_other: String + public func Channel_NotificationComments(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Channel_NotificationComments_zero, "\(value)") + case .one: + return String(format: self._Channel_NotificationComments_one, "\(value)") + case .two: + return String(format: self._Channel_NotificationComments_two, "\(value)") + case .few: + return String(format: self._Channel_NotificationComments_few, "\(value)") + case .many: + return String(format: self._Channel_NotificationComments_many, "\(value)") + case .other: + return String(format: self._Channel_NotificationComments_other, "\(value)") + } + } + private let _UserCount_zero: String + private let _UserCount_one: String + private let _UserCount_two: String + private let _UserCount_few: String + private let _UserCount_many: String + private let _UserCount_other: String + public func UserCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._UserCount_zero, "\(value)") + case .one: + return String(format: self._UserCount_one, "\(value)") + case .two: + return String(format: self._UserCount_two, "\(value)") + case .few: + return String(format: self._UserCount_few, "\(value)") + case .many: + return String(format: self._UserCount_many, "\(value)") + case .other: + return String(format: self._UserCount_other, "\(value)") + } + } + private let _ForwardedGifs_zero: String + private let _ForwardedGifs_one: String + private let _ForwardedGifs_two: String + private let _ForwardedGifs_few: String + private let _ForwardedGifs_many: String + private let _ForwardedGifs_other: String + public func ForwardedGifs(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedGifs_zero, "\(value)") + case .one: + return String(format: self._ForwardedGifs_one, "\(value)") + case .two: + return String(format: self._ForwardedGifs_two, "\(value)") + case .few: + return String(format: self._ForwardedGifs_few, "\(value)") + case .many: + return String(format: self._ForwardedGifs_many, "\(value)") + case .other: + return String(format: self._ForwardedGifs_other, "\(value)") + } + } + private let _MessageTimer_ShortHours_zero: String + private let _MessageTimer_ShortHours_one: String + private let _MessageTimer_ShortHours_two: String + private let _MessageTimer_ShortHours_few: String + private let _MessageTimer_ShortHours_many: String + private let _MessageTimer_ShortHours_other: String + public func MessageTimer_ShortHours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortHours_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortHours_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortHours_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortHours_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortHours_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortHours_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreExtended_zero: String + private let _ServiceMessage_GameScoreExtended_one: String + private let _ServiceMessage_GameScoreExtended_two: String + private let _ServiceMessage_GameScoreExtended_few: String + private let _ServiceMessage_GameScoreExtended_many: String + private let _ServiceMessage_GameScoreExtended_other: String + public func ServiceMessage_GameScoreExtended(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreExtended_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreExtended_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreExtended_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreExtended_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreExtended_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreExtended_other, "\(value)") + } + } + private let _StickerPack_AddStickerCount_zero: String + private let _StickerPack_AddStickerCount_one: String + private let _StickerPack_AddStickerCount_two: String + private let _StickerPack_AddStickerCount_few: String + private let _StickerPack_AddStickerCount_many: String + private let _StickerPack_AddStickerCount_other: String + public func StickerPack_AddStickerCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_AddStickerCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_AddStickerCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_AddStickerCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_AddStickerCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_AddStickerCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_AddStickerCount_other, "\(value)") + } + } + private let _AttachmentMenu_SendPhoto_zero: String + private let _AttachmentMenu_SendPhoto_one: String + private let _AttachmentMenu_SendPhoto_two: String + private let _AttachmentMenu_SendPhoto_few: String + private let _AttachmentMenu_SendPhoto_many: String + private let _AttachmentMenu_SendPhoto_other: String + public func AttachmentMenu_SendPhoto(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._AttachmentMenu_SendPhoto_zero, "\(value)") + case .one: + return String(format: self._AttachmentMenu_SendPhoto_one, "\(value)") + case .two: + return String(format: self._AttachmentMenu_SendPhoto_two, "\(value)") + case .few: + return String(format: self._AttachmentMenu_SendPhoto_few, "\(value)") + case .many: + return String(format: self._AttachmentMenu_SendPhoto_many, "\(value)") + case .other: + return String(format: self._AttachmentMenu_SendPhoto_other, "\(value)") + } + } + private let _Conversation_StatusRecipients_zero: String + private let _Conversation_StatusRecipients_one: String + private let _Conversation_StatusRecipients_two: String + private let _Conversation_StatusRecipients_few: String + private let _Conversation_StatusRecipients_many: String + private let _Conversation_StatusRecipients_other: String + public func Conversation_StatusRecipients(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_StatusRecipients_zero, "\(value)") + case .one: + return String(format: self._Conversation_StatusRecipients_one, "\(value)") + case .two: + return String(format: self._Conversation_StatusRecipients_two, "\(value)") + case .few: + return String(format: self._Conversation_StatusRecipients_few, "\(value)") + case .many: + return String(format: self._Conversation_StatusRecipients_many, "\(value)") + case .other: + return String(format: self._Conversation_StatusRecipients_other, "\(value)") + } + } + private let _Channel_Management_LabelRights_zero: String + private let _Channel_Management_LabelRights_one: String + private let _Channel_Management_LabelRights_two: String + private let _Channel_Management_LabelRights_few: String + private let _Channel_Management_LabelRights_many: String + private let _Channel_Management_LabelRights_other: String + public func Channel_Management_LabelRights(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Channel_Management_LabelRights_zero, "\(value)") + case .one: + return String(format: self._Channel_Management_LabelRights_one, "\(value)") + case .two: + return String(format: self._Channel_Management_LabelRights_two, "\(value)") + case .few: + return String(format: self._Channel_Management_LabelRights_few, "\(value)") + case .many: + return String(format: self._Channel_Management_LabelRights_many, "\(value)") + case .other: + return String(format: self._Channel_Management_LabelRights_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreSelfSimple_zero: String + private let _ServiceMessage_GameScoreSelfSimple_one: String + private let _ServiceMessage_GameScoreSelfSimple_two: String + private let _ServiceMessage_GameScoreSelfSimple_few: String + private let _ServiceMessage_GameScoreSelfSimple_many: String + private let _ServiceMessage_GameScoreSelfSimple_other: String + public func ServiceMessage_GameScoreSelfSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreSelfSimple_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreSelfSimple_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreSelfSimple_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreSelfSimple_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreSelfSimple_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreSelfSimple_other, "\(value)") + } + } + private let _SharedMedia_Photo_zero: String + private let _SharedMedia_Photo_one: String + private let _SharedMedia_Photo_two: String + private let _SharedMedia_Photo_few: String + private let _SharedMedia_Photo_many: String + private let _SharedMedia_Photo_other: String + public func SharedMedia_Photo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_Photo_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_Photo_one, "\(value)") + case .two: + return String(format: self._SharedMedia_Photo_two, "\(value)") + case .few: + return String(format: self._SharedMedia_Photo_few, "\(value)") + case .many: + return String(format: self._SharedMedia_Photo_many, "\(value)") + case .other: + return String(format: self._SharedMedia_Photo_other, "\(value)") + } + } + private let _MessageTimer_Weeks_zero: String + private let _MessageTimer_Weeks_one: String + private let _MessageTimer_Weeks_two: String + private let _MessageTimer_Weeks_few: String + private let _MessageTimer_Weeks_many: String + private let _MessageTimer_Weeks_other: String + public func MessageTimer_Weeks(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Weeks_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Weeks_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Weeks_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Weeks_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Weeks_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Weeks_other, "\(value)") + } + } + private let _StickerPack_AddMaskCount_zero: String + private let _StickerPack_AddMaskCount_one: String + private let _StickerPack_AddMaskCount_two: String + private let _StickerPack_AddMaskCount_few: String + private let _StickerPack_AddMaskCount_many: String + private let _StickerPack_AddMaskCount_other: String + public func StickerPack_AddMaskCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_AddMaskCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_AddMaskCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_AddMaskCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_AddMaskCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_AddMaskCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_AddMaskCount_other, "\(value)") + } + } + private let _LastSeen_MinutesAgo_zero: String + private let _LastSeen_MinutesAgo_one: String + private let _LastSeen_MinutesAgo_two: String + private let _LastSeen_MinutesAgo_few: String + private let _LastSeen_MinutesAgo_many: String + private let _LastSeen_MinutesAgo_other: String + public func LastSeen_MinutesAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._LastSeen_MinutesAgo_zero, "\(value)") + case .one: + return String(format: self._LastSeen_MinutesAgo_one, "\(value)") + case .two: + return String(format: self._LastSeen_MinutesAgo_two, "\(value)") + case .few: + return String(format: self._LastSeen_MinutesAgo_few, "\(value)") + case .many: + return String(format: self._LastSeen_MinutesAgo_many, "\(value)") + case .other: + return String(format: self._LastSeen_MinutesAgo_other, "\(value)") + } + } + private let _LastSeen_HoursAgo_zero: String + private let _LastSeen_HoursAgo_one: String + private let _LastSeen_HoursAgo_two: String + private let _LastSeen_HoursAgo_few: String + private let _LastSeen_HoursAgo_many: String + private let _LastSeen_HoursAgo_other: String + public func LastSeen_HoursAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._LastSeen_HoursAgo_zero, "\(value)") + case .one: + return String(format: self._LastSeen_HoursAgo_one, "\(value)") + case .two: + return String(format: self._LastSeen_HoursAgo_two, "\(value)") + case .few: + return String(format: self._LastSeen_HoursAgo_few, "\(value)") + case .many: + return String(format: self._LastSeen_HoursAgo_many, "\(value)") + case .other: + return String(format: self._LastSeen_HoursAgo_other, "\(value)") + } + } + private let _MuteExpires_Days_zero: String + private let _MuteExpires_Days_one: String + private let _MuteExpires_Days_two: String + private let _MuteExpires_Days_few: String + private let _MuteExpires_Days_many: String + private let _MuteExpires_Days_other: String + public func MuteExpires_Days(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteExpires_Days_zero, "\(value)") + case .one: + return String(format: self._MuteExpires_Days_one, "\(value)") + case .two: + return String(format: self._MuteExpires_Days_two, "\(value)") + case .few: + return String(format: self._MuteExpires_Days_few, "\(value)") + case .many: + return String(format: self._MuteExpires_Days_many, "\(value)") + case .other: + return String(format: self._MuteExpires_Days_other, "\(value)") + } + } + private let _MuteExpires_Hours_zero: String + private let _MuteExpires_Hours_one: String + private let _MuteExpires_Hours_two: String + private let _MuteExpires_Hours_few: String + private let _MuteExpires_Hours_many: String + private let _MuteExpires_Hours_other: String + public func MuteExpires_Hours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteExpires_Hours_zero, "\(value)") + case .one: + return String(format: self._MuteExpires_Hours_one, "\(value)") + case .two: + return String(format: self._MuteExpires_Hours_two, "\(value)") + case .few: + return String(format: self._MuteExpires_Hours_few, "\(value)") + case .many: + return String(format: self._MuteExpires_Hours_many, "\(value)") + case .other: + return String(format: self._MuteExpires_Hours_other, "\(value)") + } + } + private let _Watch_LastSeen_HoursAgo_zero: String + private let _Watch_LastSeen_HoursAgo_one: String + private let _Watch_LastSeen_HoursAgo_two: String + private let _Watch_LastSeen_HoursAgo_few: String + private let _Watch_LastSeen_HoursAgo_many: String + private let _Watch_LastSeen_HoursAgo_other: String + public func Watch_LastSeen_HoursAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Watch_LastSeen_HoursAgo_zero, "\(value)") + case .one: + return String(format: self._Watch_LastSeen_HoursAgo_one, "\(value)") + case .two: + return String(format: self._Watch_LastSeen_HoursAgo_two, "\(value)") + case .few: + return String(format: self._Watch_LastSeen_HoursAgo_few, "\(value)") + case .many: + return String(format: self._Watch_LastSeen_HoursAgo_many, "\(value)") + case .other: + return String(format: self._Watch_LastSeen_HoursAgo_other, "\(value)") + } + } + private let _Forward_ConfirmMultipleFiles_zero: String + private let _Forward_ConfirmMultipleFiles_one: String + private let _Forward_ConfirmMultipleFiles_two: String + private let _Forward_ConfirmMultipleFiles_few: String + private let _Forward_ConfirmMultipleFiles_many: String + private let _Forward_ConfirmMultipleFiles_other: String + public func Forward_ConfirmMultipleFiles(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Forward_ConfirmMultipleFiles_zero, "\(value)") + case .one: + return String(format: self._Forward_ConfirmMultipleFiles_one, "\(value)") + case .two: + return String(format: self._Forward_ConfirmMultipleFiles_two, "\(value)") + case .few: + return String(format: self._Forward_ConfirmMultipleFiles_few, "\(value)") + case .many: + return String(format: self._Forward_ConfirmMultipleFiles_many, "\(value)") + case .other: + return String(format: self._Forward_ConfirmMultipleFiles_other, "\(value)") + } + } + private let _AttachmentMenu_SendGif_zero: String + private let _AttachmentMenu_SendGif_one: String + private let _AttachmentMenu_SendGif_two: String + private let _AttachmentMenu_SendGif_few: String + private let _AttachmentMenu_SendGif_many: String + private let _AttachmentMenu_SendGif_other: String + public func AttachmentMenu_SendGif(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._AttachmentMenu_SendGif_zero, "\(value)") + case .one: + return String(format: self._AttachmentMenu_SendGif_one, "\(value)") + case .two: + return String(format: self._AttachmentMenu_SendGif_two, "\(value)") + case .few: + return String(format: self._AttachmentMenu_SendGif_few, "\(value)") + case .many: + return String(format: self._AttachmentMenu_SendGif_many, "\(value)") + case .other: + return String(format: self._AttachmentMenu_SendGif_other, "\(value)") + } + } + private let _StickerPack_RemoveStickerCount_zero: String + private let _StickerPack_RemoveStickerCount_one: String + private let _StickerPack_RemoveStickerCount_two: String + private let _StickerPack_RemoveStickerCount_few: String + private let _StickerPack_RemoveStickerCount_many: String + private let _StickerPack_RemoveStickerCount_other: String + public func StickerPack_RemoveStickerCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_RemoveStickerCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_RemoveStickerCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_RemoveStickerCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_RemoveStickerCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_RemoveStickerCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_RemoveStickerCount_other, "\(value)") + } + } + private let _SharedMedia_Link_zero: String + private let _SharedMedia_Link_one: String + private let _SharedMedia_Link_two: String + private let _SharedMedia_Link_few: String + private let _SharedMedia_Link_many: String + private let _SharedMedia_Link_other: String + public func SharedMedia_Link(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_Link_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_Link_one, "\(value)") + case .two: + return String(format: self._SharedMedia_Link_two, "\(value)") + case .few: + return String(format: self._SharedMedia_Link_few, "\(value)") + case .many: + return String(format: self._SharedMedia_Link_many, "\(value)") + case .other: + return String(format: self._SharedMedia_Link_other, "\(value)") + } + } + private let _Map_ETAHours_zero: String + private let _Map_ETAHours_one: String + private let _Map_ETAHours_two: String + private let _Map_ETAHours_few: String + private let _Map_ETAHours_many: String + private let _Map_ETAHours_other: String + public func Map_ETAHours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Map_ETAHours_zero, "\(value)") + case .one: + return String(format: self._Map_ETAHours_one, "\(value)") + case .two: + return String(format: self._Map_ETAHours_two, "\(value)") + case .few: + return String(format: self._Map_ETAHours_few, "\(value)") + case .many: + return String(format: self._Map_ETAHours_many, "\(value)") + case .other: + return String(format: self._Map_ETAHours_other, "\(value)") + } + } + private let _SharedMedia_DeleteItemsConfirmation_zero: String + private let _SharedMedia_DeleteItemsConfirmation_one: String + private let _SharedMedia_DeleteItemsConfirmation_two: String + private let _SharedMedia_DeleteItemsConfirmation_few: String + private let _SharedMedia_DeleteItemsConfirmation_many: String + private let _SharedMedia_DeleteItemsConfirmation_other: String + public func SharedMedia_DeleteItemsConfirmation(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_DeleteItemsConfirmation_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_DeleteItemsConfirmation_one, "\(value)") + case .two: + return String(format: self._SharedMedia_DeleteItemsConfirmation_two, "\(value)") + case .few: + return String(format: self._SharedMedia_DeleteItemsConfirmation_few, "\(value)") + case .many: + return String(format: self._SharedMedia_DeleteItemsConfirmation_many, "\(value)") + case .other: + return String(format: self._SharedMedia_DeleteItemsConfirmation_other, "\(value)") + } + } + private let _Watch_LastSeen_MinutesAgo_zero: String + private let _Watch_LastSeen_MinutesAgo_one: String + private let _Watch_LastSeen_MinutesAgo_two: String + private let _Watch_LastSeen_MinutesAgo_few: String + private let _Watch_LastSeen_MinutesAgo_many: String + private let _Watch_LastSeen_MinutesAgo_other: String + public func Watch_LastSeen_MinutesAgo(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Watch_LastSeen_MinutesAgo_zero, "\(value)") + case .one: + return String(format: self._Watch_LastSeen_MinutesAgo_one, "\(value)") + case .two: + return String(format: self._Watch_LastSeen_MinutesAgo_two, "\(value)") + case .few: + return String(format: self._Watch_LastSeen_MinutesAgo_few, "\(value)") + case .many: + return String(format: self._Watch_LastSeen_MinutesAgo_many, "\(value)") + case .other: + return String(format: self._Watch_LastSeen_MinutesAgo_other, "\(value)") + } + } + private let _ForwardedMessages_zero: String + private let _ForwardedMessages_one: String + private let _ForwardedMessages_two: String + private let _ForwardedMessages_few: String + private let _ForwardedMessages_many: String + private let _ForwardedMessages_other: String + public func ForwardedMessages(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedMessages_zero, "\(value)") + case .one: + return String(format: self._ForwardedMessages_one, "\(value)") + case .two: + return String(format: self._ForwardedMessages_two, "\(value)") + case .few: + return String(format: self._ForwardedMessages_few, "\(value)") + case .many: + return String(format: self._ForwardedMessages_many, "\(value)") + case .other: + return String(format: self._ForwardedMessages_other, "\(value)") + } + } + private let _SharedMedia_ItemsSelected_zero: String + private let _SharedMedia_ItemsSelected_one: String + private let _SharedMedia_ItemsSelected_two: String + private let _SharedMedia_ItemsSelected_few: String + private let _SharedMedia_ItemsSelected_many: String + private let _SharedMedia_ItemsSelected_other: String + public func SharedMedia_ItemsSelected(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._SharedMedia_ItemsSelected_zero, "\(value)") + case .one: + return String(format: self._SharedMedia_ItemsSelected_one, "\(value)") + case .two: + return String(format: self._SharedMedia_ItemsSelected_two, "\(value)") + case .few: + return String(format: self._SharedMedia_ItemsSelected_few, "\(value)") + case .many: + return String(format: self._SharedMedia_ItemsSelected_many, "\(value)") + case .other: + return String(format: self._SharedMedia_ItemsSelected_other, "\(value)") + } + } + private let _MessageTimer_Hours_zero: String + private let _MessageTimer_Hours_one: String + private let _MessageTimer_Hours_two: String + private let _MessageTimer_Hours_few: String + private let _MessageTimer_Hours_many: String + private let _MessageTimer_Hours_other: String + public func MessageTimer_Hours(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Hours_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Hours_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Hours_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Hours_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Hours_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Hours_other, "\(value)") + } + } + private let _MessageTimer_Years_zero: String + private let _MessageTimer_Years_one: String + private let _MessageTimer_Years_two: String + private let _MessageTimer_Years_few: String + private let _MessageTimer_Years_many: String + private let _MessageTimer_Years_other: String + public func MessageTimer_Years(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Years_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Years_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Years_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Years_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Years_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Years_other, "\(value)") + } + } + private let _Map_ETAMinutes_zero: String + private let _Map_ETAMinutes_one: String + private let _Map_ETAMinutes_two: String + private let _Map_ETAMinutes_few: String + private let _Map_ETAMinutes_many: String + private let _Map_ETAMinutes_other: String + public func Map_ETAMinutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Map_ETAMinutes_zero, "\(value)") + case .one: + return String(format: self._Map_ETAMinutes_one, "\(value)") + case .two: + return String(format: self._Map_ETAMinutes_two, "\(value)") + case .few: + return String(format: self._Map_ETAMinutes_few, "\(value)") + case .many: + return String(format: self._Map_ETAMinutes_many, "\(value)") + case .other: + return String(format: self._Map_ETAMinutes_other, "\(value)") + } + } + private let _ForwardedVideos_zero: String + private let _ForwardedVideos_one: String + private let _ForwardedVideos_two: String + private let _ForwardedVideos_few: String + private let _ForwardedVideos_many: String + private let _ForwardedVideos_other: String + public func ForwardedVideos(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedVideos_zero, "\(value)") + case .one: + return String(format: self._ForwardedVideos_one, "\(value)") + case .two: + return String(format: self._ForwardedVideos_two, "\(value)") + case .few: + return String(format: self._ForwardedVideos_few, "\(value)") + case .many: + return String(format: self._ForwardedVideos_many, "\(value)") + case .other: + return String(format: self._ForwardedVideos_other, "\(value)") + } + } + private let _Notification_GameScoreSelfSimple_zero: String + private let _Notification_GameScoreSelfSimple_one: String + private let _Notification_GameScoreSelfSimple_two: String + private let _Notification_GameScoreSelfSimple_few: String + private let _Notification_GameScoreSelfSimple_many: String + private let _Notification_GameScoreSelfSimple_other: String + public func Notification_GameScoreSelfSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Notification_GameScoreSelfSimple_zero, "\(value)") + case .one: + return String(format: self._Notification_GameScoreSelfSimple_one, "\(value)") + case .two: + return String(format: self._Notification_GameScoreSelfSimple_two, "\(value)") + case .few: + return String(format: self._Notification_GameScoreSelfSimple_few, "\(value)") + case .many: + return String(format: self._Notification_GameScoreSelfSimple_many, "\(value)") + case .other: + return String(format: self._Notification_GameScoreSelfSimple_other, "\(value)") + } + } + private let _ServiceMessage_GameScoreSimple_zero: String + private let _ServiceMessage_GameScoreSimple_one: String + private let _ServiceMessage_GameScoreSimple_two: String + private let _ServiceMessage_GameScoreSimple_few: String + private let _ServiceMessage_GameScoreSimple_many: String + private let _ServiceMessage_GameScoreSimple_other: String + public func ServiceMessage_GameScoreSimple(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ServiceMessage_GameScoreSimple_zero, "\(value)") + case .one: + return String(format: self._ServiceMessage_GameScoreSimple_one, "\(value)") + case .two: + return String(format: self._ServiceMessage_GameScoreSimple_two, "\(value)") + case .few: + return String(format: self._ServiceMessage_GameScoreSimple_few, "\(value)") + case .many: + return String(format: self._ServiceMessage_GameScoreSimple_many, "\(value)") + case .other: + return String(format: self._ServiceMessage_GameScoreSimple_other, "\(value)") + } + } + private let _QuickSend_Photos_zero: String + private let _QuickSend_Photos_one: String + private let _QuickSend_Photos_two: String + private let _QuickSend_Photos_few: String + private let _QuickSend_Photos_many: String + private let _QuickSend_Photos_other: String + public func QuickSend_Photos(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._QuickSend_Photos_zero, "\(value)") + case .one: + return String(format: self._QuickSend_Photos_one, "\(value)") + case .two: + return String(format: self._QuickSend_Photos_two, "\(value)") + case .few: + return String(format: self._QuickSend_Photos_few, "\(value)") + case .many: + return String(format: self._QuickSend_Photos_many, "\(value)") + case .other: + return String(format: self._QuickSend_Photos_other, "\(value)") + } + } + private let _MuteFor_Days_zero: String + private let _MuteFor_Days_one: String + private let _MuteFor_Days_two: String + private let _MuteFor_Days_few: String + private let _MuteFor_Days_many: String + private let _MuteFor_Days_other: String + public func MuteFor_Days(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteFor_Days_zero, "\(value)") + case .one: + return String(format: self._MuteFor_Days_one, "\(value)") + case .two: + return String(format: self._MuteFor_Days_two, "\(value)") + case .few: + return String(format: self._MuteFor_Days_few, "\(value)") + case .many: + return String(format: self._MuteFor_Days_many, "\(value)") + case .other: + return String(format: self._MuteFor_Days_other, "\(value)") + } + } + private let _Conversation_StatusOnline_zero: String + private let _Conversation_StatusOnline_one: String + private let _Conversation_StatusOnline_two: String + private let _Conversation_StatusOnline_few: String + private let _Conversation_StatusOnline_many: String + private let _Conversation_StatusOnline_other: String + public func Conversation_StatusOnline(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Conversation_StatusOnline_zero, "\(value)") + case .one: + return String(format: self._Conversation_StatusOnline_one, "\(value)") + case .two: + return String(format: self._Conversation_StatusOnline_two, "\(value)") + case .few: + return String(format: self._Conversation_StatusOnline_few, "\(value)") + case .many: + return String(format: self._Conversation_StatusOnline_many, "\(value)") + case .other: + return String(format: self._Conversation_StatusOnline_other, "\(value)") + } + } + private let _AttachmentMenu_SendItem_zero: String + private let _AttachmentMenu_SendItem_one: String + private let _AttachmentMenu_SendItem_two: String + private let _AttachmentMenu_SendItem_few: String + private let _AttachmentMenu_SendItem_many: String + private let _AttachmentMenu_SendItem_other: String + public func AttachmentMenu_SendItem(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._AttachmentMenu_SendItem_zero, "\(value)") + case .one: + return String(format: self._AttachmentMenu_SendItem_one, "\(value)") + case .two: + return String(format: self._AttachmentMenu_SendItem_two, "\(value)") + case .few: + return String(format: self._AttachmentMenu_SendItem_few, "\(value)") + case .many: + return String(format: self._AttachmentMenu_SendItem_many, "\(value)") + case .other: + return String(format: self._AttachmentMenu_SendItem_other, "\(value)") + } + } + private let _Time_MonthOfYear_zero: String + private let _Time_MonthOfYear_one: String + private let _Time_MonthOfYear_two: String + private let _Time_MonthOfYear_few: String + private let _Time_MonthOfYear_many: String + private let _Time_MonthOfYear_other: String + public func Time_MonthOfYear(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Time_MonthOfYear_zero, "\(value)") + case .one: + return String(format: self._Time_MonthOfYear_one, "\(value)") + case .two: + return String(format: self._Time_MonthOfYear_two, "\(value)") + case .few: + return String(format: self._Time_MonthOfYear_few, "\(value)") + case .many: + return String(format: self._Time_MonthOfYear_many, "\(value)") + case .other: + return String(format: self._Time_MonthOfYear_other, "\(value)") + } + } + private let _Watch_UserInfo_Mute_zero: String + private let _Watch_UserInfo_Mute_one: String + private let _Watch_UserInfo_Mute_two: String + private let _Watch_UserInfo_Mute_few: String + private let _Watch_UserInfo_Mute_many: String + private let _Watch_UserInfo_Mute_other: String + public func Watch_UserInfo_Mute(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Watch_UserInfo_Mute_zero, "\(value)") + case .one: + return String(format: self._Watch_UserInfo_Mute_one, "\(value)") + case .two: + return String(format: self._Watch_UserInfo_Mute_two, "\(value)") + case .few: + return String(format: self._Watch_UserInfo_Mute_few, "\(value)") + case .many: + return String(format: self._Watch_UserInfo_Mute_many, "\(value)") + case .other: + return String(format: self._Watch_UserInfo_Mute_other, "\(value)") + } + } + private let _StickerPack_MaskCount_zero: String + private let _StickerPack_MaskCount_one: String + private let _StickerPack_MaskCount_two: String + private let _StickerPack_MaskCount_few: String + private let _StickerPack_MaskCount_many: String + private let _StickerPack_MaskCount_other: String + public func StickerPack_MaskCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_MaskCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_MaskCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_MaskCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_MaskCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_MaskCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_MaskCount_other, "\(value)") + } + } + private let _Call_ShortMinutes_zero: String + private let _Call_ShortMinutes_one: String + private let _Call_ShortMinutes_two: String + private let _Call_ShortMinutes_few: String + private let _Call_ShortMinutes_many: String + private let _Call_ShortMinutes_other: String + public func Call_ShortMinutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._Call_ShortMinutes_zero, "\(value)") + case .one: + return String(format: self._Call_ShortMinutes_one, "\(value)") + case .two: + return String(format: self._Call_ShortMinutes_two, "\(value)") + case .few: + return String(format: self._Call_ShortMinutes_few, "\(value)") + case .many: + return String(format: self._Call_ShortMinutes_many, "\(value)") + case .other: + return String(format: self._Call_ShortMinutes_other, "\(value)") + } + } + private let _StickerPack_RemoveMaskCount_zero: String + private let _StickerPack_RemoveMaskCount_one: String + private let _StickerPack_RemoveMaskCount_two: String + private let _StickerPack_RemoveMaskCount_few: String + private let _StickerPack_RemoveMaskCount_many: String + private let _StickerPack_RemoveMaskCount_other: String + public func StickerPack_RemoveMaskCount(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._StickerPack_RemoveMaskCount_zero, "\(value)") + case .one: + return String(format: self._StickerPack_RemoveMaskCount_one, "\(value)") + case .two: + return String(format: self._StickerPack_RemoveMaskCount_two, "\(value)") + case .few: + return String(format: self._StickerPack_RemoveMaskCount_few, "\(value)") + case .many: + return String(format: self._StickerPack_RemoveMaskCount_many, "\(value)") + case .other: + return String(format: self._StickerPack_RemoveMaskCount_other, "\(value)") + } + } + private let _ForwardedLocations_zero: String + private let _ForwardedLocations_one: String + private let _ForwardedLocations_two: String + private let _ForwardedLocations_few: String + private let _ForwardedLocations_many: String + private let _ForwardedLocations_other: String + public func ForwardedLocations(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._ForwardedLocations_zero, "\(value)") + case .one: + return String(format: self._ForwardedLocations_one, "\(value)") + case .two: + return String(format: self._ForwardedLocations_two, "\(value)") + case .few: + return String(format: self._ForwardedLocations_few, "\(value)") + case .many: + return String(format: self._ForwardedLocations_many, "\(value)") + case .other: + return String(format: self._ForwardedLocations_other, "\(value)") + } + } + private let _MessageTimer_ShortWeeks_zero: String + private let _MessageTimer_ShortWeeks_one: String + private let _MessageTimer_ShortWeeks_two: String + private let _MessageTimer_ShortWeeks_few: String + private let _MessageTimer_ShortWeeks_many: String + private let _MessageTimer_ShortWeeks_other: String + public func MessageTimer_ShortWeeks(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_ShortWeeks_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_ShortWeeks_one, "\(value)") + case .two: + return String(format: self._MessageTimer_ShortWeeks_two, "\(value)") + case .few: + return String(format: self._MessageTimer_ShortWeeks_few, "\(value)") + case .many: + return String(format: self._MessageTimer_ShortWeeks_many, "\(value)") + case .other: + return String(format: self._MessageTimer_ShortWeeks_other, "\(value)") + } + } + private let _MessageTimer_Minutes_zero: String + private let _MessageTimer_Minutes_one: String + private let _MessageTimer_Minutes_two: String + private let _MessageTimer_Minutes_few: String + private let _MessageTimer_Minutes_many: String + private let _MessageTimer_Minutes_other: String + public func MessageTimer_Minutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Minutes_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Minutes_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Minutes_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Minutes_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Minutes_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Minutes_other, "\(value)") + } + } + private let _MessageTimer_Months_zero: String + private let _MessageTimer_Months_one: String + private let _MessageTimer_Months_two: String + private let _MessageTimer_Months_few: String + private let _MessageTimer_Months_many: String + private let _MessageTimer_Months_other: String + public func MessageTimer_Months(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Months_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Months_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Months_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Months_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Months_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Months_other, "\(value)") + } + } + private let _MessageTimer_Days_zero: String + private let _MessageTimer_Days_one: String + private let _MessageTimer_Days_two: String + private let _MessageTimer_Days_few: String + private let _MessageTimer_Days_many: String + private let _MessageTimer_Days_other: String + public func MessageTimer_Days(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MessageTimer_Days_zero, "\(value)") + case .one: + return String(format: self._MessageTimer_Days_one, "\(value)") + case .two: + return String(format: self._MessageTimer_Days_two, "\(value)") + case .few: + return String(format: self._MessageTimer_Days_few, "\(value)") + case .many: + return String(format: self._MessageTimer_Days_many, "\(value)") + case .other: + return String(format: self._MessageTimer_Days_other, "\(value)") + } + } + private let _MuteExpires_Minutes_zero: String + private let _MuteExpires_Minutes_one: String + private let _MuteExpires_Minutes_two: String + private let _MuteExpires_Minutes_few: String + private let _MuteExpires_Minutes_many: String + private let _MuteExpires_Minutes_other: String + public func MuteExpires_Minutes(_ value: Int32) -> String { + switch presentationStringsPluralizationForm(self.lc, value) { + case .zero: + return String(format: self._MuteExpires_Minutes_zero, "\(value)") + case .one: + return String(format: self._MuteExpires_Minutes_one, "\(value)") + case .two: + return String(format: self._MuteExpires_Minutes_two, "\(value)") + case .few: + return String(format: self._MuteExpires_Minutes_few, "\(value)") + case .many: + return String(format: self._MuteExpires_Minutes_many, "\(value)") + case .other: + return String(format: self._MuteExpires_Minutes_other, "\(value)") + } + } + + + init(languageCode: String, dict: [String: String]) { + self.languageCode = languageCode + var rawCode = languageCode as NSString + let range = rawCode.range(of: "_") + if range.location != NSNotFound { + rawCode = rawCode.substring(to: range.location) as NSString + } + rawCode = rawCode.lowercased as NSString + var lc: UInt32 = 0 + for i in 0 ..< rawCode.length { + lc = (lc << 8) + UInt32(rawCode.character(at: i)) + } + self.lc = lc + self.Channel_BanUser_Title = getValue(dict, "Channel.BanUser.Title") + self.Preview_SaveGif = getValue(dict, "Preview.SaveGif") + self.EnterPasscode_EnterNewPasscodeNew = getValue(dict, "EnterPasscode.EnterNewPasscodeNew") + self.Privacy_Calls_WhoCanCallMe = getValue(dict, "Privacy.Calls.WhoCanCallMe") + self.Watch_NoConnection = getValue(dict, "Watch.NoConnection") + self._Group_Username_LinkHint = getValue(dict, "Group.Username.LinkHint") + self._Group_Username_LinkHint_r = extractArgumentRanges(self._Group_Username_LinkHint) + self.Activity_UploadingPhoto = getValue(dict, "Activity.UploadingPhoto") + self.PrivacySettings_PrivacyTitle = getValue(dict, "PrivacySettings.PrivacyTitle") + self._DialogList_PinLimitError = getValue(dict, "DialogList.PinLimitError") + self._DialogList_PinLimitError_r = extractArgumentRanges(self._DialogList_PinLimitError) + self.Settings_LogoutError = getValue(dict, "Settings.LogoutError") + self.Cache_ClearCache = getValue(dict, "Cache.ClearCache") + self.Common_Close = getValue(dict, "Common.Close") + self.ChangePhoneNumberCode_Called = getValue(dict, "ChangePhoneNumberCode.Called") + self.Login_PhoneTitle = getValue(dict, "Login.PhoneTitle") + self._Cache_Clear = getValue(dict, "Cache.Clear") + self._Cache_Clear_r = extractArgumentRanges(self._Cache_Clear) + self.EnterPasscode_EnterNewPasscodeChange = getValue(dict, "EnterPasscode.EnterNewPasscodeChange") + self.Watch_ChatList_Compose = getValue(dict, "Watch.ChatList.Compose") + self.DialogList_SearchSectionDialogs = getValue(dict, "DialogList.SearchSectionDialogs") + self.Contacts_TabTitle = getValue(dict, "Contacts.TabTitle") + self.TwoStepAuth_SetupPasswordConfirmPassword = getValue(dict, "TwoStepAuth.SetupPasswordConfirmPassword") + self.ChannelIntro_Text = getValue(dict, "ChannelIntro.Text") + self.PrivacySettings_SecurityTitle = getValue(dict, "PrivacySettings.SecurityTitle") + self._Login_SmsRequestState1 = getValue(dict, "Login.SmsRequestState1") + self._Login_SmsRequestState1_r = extractArgumentRanges(self._Login_SmsRequestState1) + self.Conversation_Download = getValue(dict, "Conversation.Download") + self._Call_StatusOngoing = getValue(dict, "Call.StatusOngoing") + self._Call_StatusOngoing_r = extractArgumentRanges(self._Call_StatusOngoing) + self.Settings_LogoutConfirmationText = getValue(dict, "Settings.LogoutConfirmationText") + self.BlockedUsers_Info = getValue(dict, "BlockedUsers.Info") + self.ChatSettings_AutomaticAudioDownload = getValue(dict, "ChatSettings.AutomaticAudioDownload") + self.Map_OpenInFoursquare = getValue(dict, "Map.OpenInFoursquare") + self.Privacy_Calls_CustomShareHelp = getValue(dict, "Privacy.Calls.CustomShareHelp") + self.Group_MessagePhotoUpdated = getValue(dict, "Group.MessagePhotoUpdated") + self.Message_PinnedInvoice = getValue(dict, "Message.PinnedInvoice") + self.Login_InfoAvatarAdd = getValue(dict, "Login.InfoAvatarAdd") + self.WebSearch_RecentSectionTitle = getValue(dict, "WebSearch.RecentSectionTitle") + self._CHAT_MESSAGE_TEXT = getValue(dict, "CHAT_MESSAGE_TEXT") + self._CHAT_MESSAGE_TEXT_r = extractArgumentRanges(self._CHAT_MESSAGE_TEXT) + self.Message_Sticker = getValue(dict, "Message.Sticker") + self.Channel_Management_Remove = getValue(dict, "Channel.Management.Remove") + self.Paint_Regular = getValue(dict, "Paint.Regular") + self.Channel_Username_Help = getValue(dict, "Channel.Username.Help") + self._Profile_CreateEncryptedChatOutdatedError = getValue(dict, "Profile.CreateEncryptedChatOutdatedError") + self._Profile_CreateEncryptedChatOutdatedError_r = extractArgumentRanges(self._Profile_CreateEncryptedChatOutdatedError) + self.Login_InactiveHelp = getValue(dict, "Login.InactiveHelp") + self.ChatSettings_Security = getValue(dict, "ChatSettings.Security") + self._Time_PreciseDate_9 = getValue(dict, "Time.PreciseDate_9") + self._Time_PreciseDate_9_r = extractArgumentRanges(self._Time_PreciseDate_9) + self._PINNED_STICKER = getValue(dict, "PINNED_STICKER") + self._PINNED_STICKER_r = extractArgumentRanges(self._PINNED_STICKER) + self.Conversation_ShareInlineBotLocationConfirmation = getValue(dict, "Conversation.ShareInlineBotLocationConfirmation") + self._Channel_AdminLog_MessageEdited = getValue(dict, "Channel.AdminLog.MessageEdited") + self._Channel_AdminLog_MessageEdited_r = extractArgumentRanges(self._Channel_AdminLog_MessageEdited) + self._PHONE_CALL_REQUEST = getValue(dict, "PHONE_CALL_REQUEST") + self._PHONE_CALL_REQUEST_r = extractArgumentRanges(self._PHONE_CALL_REQUEST) + self.AccessDenied_MicrophoneRestricted = getValue(dict, "AccessDenied.MicrophoneRestricted") + self.Your_cards_expiration_year_is_invalid = getValue(dict, "Your_cards_expiration_year_is_invalid") + self.GroupInfo_InviteByLink = getValue(dict, "GroupInfo.InviteByLink") + self._Notification_LeftChat = getValue(dict, "Notification.LeftChat") + self._Notification_LeftChat_r = extractArgumentRanges(self._Notification_LeftChat) + self._Channel_AdminLog_MessageAdmin = getValue(dict, "Channel.AdminLog.MessageAdmin") + self._Channel_AdminLog_MessageAdmin_r = extractArgumentRanges(self._Channel_AdminLog_MessageAdmin) + self.PrivacyLastSeenSettings_NeverShareWith_Placeholder = getValue(dict, "PrivacyLastSeenSettings.NeverShareWith.Placeholder") + self.TwoStepAuth_SetupEmail = getValue(dict, "TwoStepAuth.SetupEmail") + self.Login_ResetAccountProtected_Reset = getValue(dict, "Login.ResetAccountProtected.Reset") + self._MESSAGE_CONTACT = getValue(dict, "MESSAGE_CONTACT") + self._MESSAGE_CONTACT_r = extractArgumentRanges(self._MESSAGE_CONTACT) + self._Group_Management_ErrorNotMember = getValue(dict, "Group.Management.ErrorNotMember") + self._Group_Management_ErrorNotMember_r = extractArgumentRanges(self._Group_Management_ErrorNotMember) + self.MediaPicker_MomentsDateRangeSameMonthYearFormat = getValue(dict, "MediaPicker.MomentsDateRangeSameMonthYearFormat") + self.Notification_MessageLifetime1w = getValue(dict, "Notification.MessageLifetime1w") + self.PasscodeSettings_AutoLock_IfAwayFor_5minutes = getValue(dict, "PasscodeSettings.AutoLock.IfAwayFor_5minutes") + self.ChatSettings_Groups = getValue(dict, "ChatSettings.Groups") + self.State_Connecting = getValue(dict, "State.Connecting") + self._Message_ForwardedMessageShort = getValue(dict, "Message.ForwardedMessageShort") + self._Message_ForwardedMessageShort_r = extractArgumentRanges(self._Message_ForwardedMessageShort) + self.Watch_ConnectionDescription = getValue(dict, "Watch.ConnectionDescription") + self._Notification_CallTimeFormat = getValue(dict, "Notification.CallTimeFormat") + self._Notification_CallTimeFormat_r = extractArgumentRanges(self._Notification_CallTimeFormat) + self.Paint_Delete = getValue(dict, "Paint.Delete") + self.Channel_MessagePhotoUpdated = getValue(dict, "Channel.MessagePhotoUpdated") + self.SharedMedia_All = getValue(dict, "SharedMedia.All") + self.Cache_Help = getValue(dict, "Cache.Help") + self._Login_EmailPhoneBody = getValue(dict, "Login.EmailPhoneBody") + self._Login_EmailPhoneBody_r = extractArgumentRanges(self._Login_EmailPhoneBody) + self.Checkout_ShippingAddress = getValue(dict, "Checkout.ShippingAddress") + self.Channel_BanList_RestrictedTitle = getValue(dict, "Channel.BanList.RestrictedTitle") + self.Checkout_TotalAmount = getValue(dict, "Checkout.TotalAmount") + self.Conversation_MessageEditedLabel = getValue(dict, "Conversation.MessageEditedLabel") + self.SharedMedia_EmptyLinksText = getValue(dict, "SharedMedia.EmptyLinksText") + self.Channel_Members_Kick = getValue(dict, "Channel.Members.Kick") + self.GoogleDrive_FolderIsEmpty = getValue(dict, "GoogleDrive.FolderIsEmpty") + self.Calls_NoCallsPlaceholder = getValue(dict, "Calls.NoCallsPlaceholder") + self.Message_PinnedDeletedMessage = getValue(dict, "Message.PinnedDeletedMessage") + self.Conversation_PinMessageAlert_OnlyPin = getValue(dict, "Conversation.PinMessageAlert.OnlyPin") + self.ReportPeer_ReasonOther_Send = getValue(dict, "ReportPeer.ReasonOther.Send") + self.Conversation_InstantPagePreview = getValue(dict, "Conversation.InstantPagePreview") + self.PasscodeSettings_SimplePasscodeHelp = getValue(dict, "PasscodeSettings.SimplePasscodeHelp") + self.Group_ErrorAddTooMuch = getValue(dict, "Group.ErrorAddTooMuch") + self.GroupInfo_Title = getValue(dict, "GroupInfo.Title") + self.State_Updating = getValue(dict, "State.Updating") + self.StickerSettings_ContextShow = getValue(dict, "StickerSettings.ContextShow") + self.Map_GetDirections = getValue(dict, "Map.GetDirections") + self._TwoStepAuth_PendingEmailHelp = getValue(dict, "TwoStepAuth.PendingEmailHelp") + self._TwoStepAuth_PendingEmailHelp_r = extractArgumentRanges(self._TwoStepAuth_PendingEmailHelp) + self.UserInfo_PhoneCall = getValue(dict, "UserInfo.PhoneCall") + self.MusicPlayer_VoiceNote = getValue(dict, "MusicPlayer.VoiceNote") + self.Paint_Duplicate = getValue(dict, "Paint.Duplicate") + self.Channel_Username_InvalidTaken = getValue(dict, "Channel.Username.InvalidTaken") + self._Profile_ShareContactGroupFormat = getValue(dict, "Profile.ShareContactGroupFormat") + self._Profile_ShareContactGroupFormat_r = extractArgumentRanges(self._Profile_ShareContactGroupFormat) + self.SecretChat_Title = getValue(dict, "SecretChat.Title") + self.Group_UpgradeConfirmation = getValue(dict, "Group.UpgradeConfirmation") + self.Checkout_LiabilityAlertTitle = getValue(dict, "Checkout.LiabilityAlertTitle") + self.GroupInfo_GroupNamePlaceholder = getValue(dict, "GroupInfo.GroupNamePlaceholder") + self.Conversation_InfoBroadcastList = getValue(dict, "Conversation.InfoBroadcastList") + self._Notification_JoinedGroupByLink = getValue(dict, "Notification.JoinedGroupByLink") + self._Notification_JoinedGroupByLink_r = extractArgumentRanges(self._Notification_JoinedGroupByLink) + self._Time_PreciseDate_8 = getValue(dict, "Time.PreciseDate_8") + self._Time_PreciseDate_8_r = extractArgumentRanges(self._Time_PreciseDate_8) + self.Login_HaveNotReceivedCodeInternal = getValue(dict, "Login.HaveNotReceivedCodeInternal") + self.LoginPassword_Title = getValue(dict, "LoginPassword.Title") + self.Conversation_PlayVideo = getValue(dict, "Conversation.PlayVideo") + self.PasscodeSettings_SimplePasscode = getValue(dict, "PasscodeSettings.SimplePasscode") + self.Conversation_MicrophoneAccessDisabled = getValue(dict, "Conversation.MicrophoneAccessDisabled") + self.NewContact_Title = getValue(dict, "NewContact.Title") + self.Username_CheckingUsername = getValue(dict, "Username.CheckingUsername") + self.Login_ResetAccountProtected_TimerTitle = getValue(dict, "Login.ResetAccountProtected.TimerTitle") + self.UserInfo_InviteBotToGroup = getValue(dict, "UserInfo.InviteBotToGroup") + self.Checkout_Email = getValue(dict, "Checkout.Email") + self.CheckoutInfo_SaveInfo = getValue(dict, "CheckoutInfo.SaveInfo") + self._ChangePhoneNumberCode_CallTimer = getValue(dict, "ChangePhoneNumberCode.CallTimer") + self._ChangePhoneNumberCode_CallTimer_r = extractArgumentRanges(self._ChangePhoneNumberCode_CallTimer) + self.TwoStepAuth_SetupPasswordEnterPasswordNew = getValue(dict, "TwoStepAuth.SetupPasswordEnterPasswordNew") + self.Weekday_Wednesday = getValue(dict, "Weekday.Wednesday") + self._Channel_AdminLog_MessageToggleSignaturesOff = getValue(dict, "Channel.AdminLog.MessageToggleSignaturesOff") + self._Channel_AdminLog_MessageToggleSignaturesOff_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleSignaturesOff) + self.Month_ShortDecember = getValue(dict, "Month.ShortDecember") + self.Channel_SignMessages = getValue(dict, "Channel.SignMessages") + self.Conversation_Moderate_Delete = getValue(dict, "Conversation.Moderate.Delete") + self.Conversation_CloudStorage_ChatStatus = getValue(dict, "Conversation.CloudStorage.ChatStatus") + self.Login_InfoTitle = getValue(dict, "Login.InfoTitle") + self.Privacy_GroupsAndChannels_NeverAllow_Placeholder = getValue(dict, "Privacy.GroupsAndChannels.NeverAllow.Placeholder") + self.Message_Video = getValue(dict, "Message.Video") + self.Notification_ChannelInviterSelf = getValue(dict, "Notification.ChannelInviterSelf") + self._VideoPreview_OptionSD = getValue(dict, "VideoPreview.OptionSD") + self._VideoPreview_OptionSD_r = extractArgumentRanges(self._VideoPreview_OptionSD) + self.Conversation_SecretLinkPreviewAlert = getValue(dict, "Conversation.SecretLinkPreviewAlert") + self.Channel_AdminLog_BanEmbedLinks = getValue(dict, "Channel.AdminLog.BanEmbedLinks") + self.Cache_Videos = getValue(dict, "Cache.Videos") + self.NetworkUsageSettings_MediaImageDataSection = getValue(dict, "NetworkUsageSettings.MediaImageDataSection") + self.TwoStepAuth_GenericHelp = getValue(dict, "TwoStepAuth.GenericHelp") + self._DialogList_SingleRecordingAudioSuffix = getValue(dict, "DialogList.SingleRecordingAudioSuffix") + self._DialogList_SingleRecordingAudioSuffix_r = extractArgumentRanges(self._DialogList_SingleRecordingAudioSuffix) + self.Checkout_NewCard_CardholderNameTitle = getValue(dict, "Checkout.NewCard.CardholderNameTitle") + self.Settings_FAQ_Button = getValue(dict, "Settings.FAQ_Button") + self._GroupInfo_AddParticipantConfirmation = getValue(dict, "GroupInfo.AddParticipantConfirmation") + self._GroupInfo_AddParticipantConfirmation_r = extractArgumentRanges(self._GroupInfo_AddParticipantConfirmation) + self.AccessDenied_PhotosRestricted = getValue(dict, "AccessDenied.PhotosRestricted") + self.Map_Locating = getValue(dict, "Map.Locating") + self._SearchImages_Downloading_Kb = getValue(dict, "SearchImages.Downloading#Kb") + self._SearchImages_Downloading_Kb_r = extractArgumentRanges(self._SearchImages_Downloading_Kb) + self._Profile_ShareBotPersonFormat = getValue(dict, "Profile.ShareBotPersonFormat") + self._Profile_ShareBotPersonFormat_r = extractArgumentRanges(self._Profile_ShareBotPersonFormat) + self.SearchImages_SearchImages = getValue(dict, "SearchImages.SearchImages") + self.SharedMedia_EmptyMusicText = getValue(dict, "SharedMedia.EmptyMusicText") + self.Cache_ByPeerHeader = getValue(dict, "Cache.ByPeerHeader") + self.Bot_GroupStatusReadsHistory = getValue(dict, "Bot.GroupStatusReadsHistory") + self.TwoStepAuth_ResetAccountConfirmation = getValue(dict, "TwoStepAuth.ResetAccountConfirmation") + self.CallSettings_Always = getValue(dict, "CallSettings.Always") + self.SearchImages_DownloadCancelled = getValue(dict, "SearchImages.DownloadCancelled") + self.Settings_LogoutConfirmationTitle = getValue(dict, "Settings.LogoutConfirmationTitle") + self.UserInfo_FirstNamePlaceholder = getValue(dict, "UserInfo.FirstNamePlaceholder") + self.ChatSettings_AutoPlayAudio = getValue(dict, "ChatSettings.AutoPlayAudio") + self.LoginPassword_ResetAccount = getValue(dict, "LoginPassword.ResetAccount") + self.Privacy_GroupsAndChannels_AlwaysAllow = getValue(dict, "Privacy.GroupsAndChannels.AlwaysAllow") + self._Notification_JoinedChat = getValue(dict, "Notification.JoinedChat") + self._Notification_JoinedChat_r = extractArgumentRanges(self._Notification_JoinedChat) + self.ChannelInfo_DeleteChannel = getValue(dict, "ChannelInfo.DeleteChannel") + self.NetworkUsageSettings_BytesReceived = getValue(dict, "NetworkUsageSettings.BytesReceived") + self.BlockedUsers_BlockTitle = getValue(dict, "BlockedUsers.BlockTitle") + self.AccessDenied_PhotosAndVideos = getValue(dict, "AccessDenied.PhotosAndVideos") + self.Channel_Username_Title = getValue(dict, "Channel.Username.Title") + self._Channel_AdminLog_MessageToggleSignaturesOn = getValue(dict, "Channel.AdminLog.MessageToggleSignaturesOn") + self._Channel_AdminLog_MessageToggleSignaturesOn_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleSignaturesOn) + self._Conversation_EncryptionWaiting = getValue(dict, "Conversation.EncryptionWaiting") + self._Conversation_EncryptionWaiting_r = extractArgumentRanges(self._Conversation_EncryptionWaiting) + self.Calls_NotNow = getValue(dict, "Calls.NotNow") + self.Conversation_Report = getValue(dict, "Conversation.Report") + self._CHANNEL_MESSAGE_DOC = getValue(dict, "CHANNEL_MESSAGE_DOC") + self._CHANNEL_MESSAGE_DOC_r = extractArgumentRanges(self._CHANNEL_MESSAGE_DOC) + self.Channel_AdminLogFilter_EventsAll = getValue(dict, "Channel.AdminLogFilter.EventsAll") + self.Call_ConnectionErrorTitle = getValue(dict, "Call.ConnectionErrorTitle") + self.Settings_ChatSettings = getValue(dict, "Settings.ChatSettings") + self.Group_About_Help = getValue(dict, "Group.About.Help") + self._CHANNEL_MESSAGE_NOTEXT = getValue(dict, "CHANNEL_MESSAGE_NOTEXT") + self._CHANNEL_MESSAGE_NOTEXT_r = extractArgumentRanges(self._CHANNEL_MESSAGE_NOTEXT) + self.Month_GenSeptember = getValue(dict, "Month.GenSeptember") + self.PrivacySettings_LastSeenEverybody = getValue(dict, "PrivacySettings.LastSeenEverybody") + self.PhotoEditor_BlurToolRadial = getValue(dict, "PhotoEditor.BlurToolRadial") + self.TwoStepAuth_PasswordRemoveConfirmation = getValue(dict, "TwoStepAuth.PasswordRemoveConfirmation") + self.Channel_EditAdmin_PermissionEditMessages = getValue(dict, "Channel.EditAdmin.PermissionEditMessages") + self.TwoStepAuth_ChangePassword = getValue(dict, "TwoStepAuth.ChangePassword") + self.Watch_MessageView_Title = getValue(dict, "Watch.MessageView.Title") + self._Notification_PinnedRoundMessage = getValue(dict, "Notification.PinnedRoundMessage") + self._Notification_PinnedRoundMessage_r = extractArgumentRanges(self._Notification_PinnedRoundMessage) + self.Conversation_DeleteGroup = getValue(dict, "Conversation.DeleteGroup") + self._Time_PreciseDate_7 = getValue(dict, "Time.PreciseDate_7") + self._Time_PreciseDate_7_r = extractArgumentRanges(self._Time_PreciseDate_7) + self.Channel_Management_LabelCreator = getValue(dict, "Channel.Management.LabelCreator") + self._Notification_PinnedStickerMessage = getValue(dict, "Notification.PinnedStickerMessage") + self._Notification_PinnedStickerMessage_r = extractArgumentRanges(self._Notification_PinnedStickerMessage) + self.Settings_SaveEditedPhotos = getValue(dict, "Settings.SaveEditedPhotos") + self.PhotoEditor_QualityTool = getValue(dict, "PhotoEditor.QualityTool") + self.Login_NetworkError = getValue(dict, "Login.NetworkError") + self.TwoStepAuth_EnterPasswordForgot = getValue(dict, "TwoStepAuth.EnterPasswordForgot") + self.Compose_ChannelMembers = getValue(dict, "Compose.ChannelMembers") + self.Common_Yes = getValue(dict, "Common.Yes") + self.KeyCommand_JumpToPreviousUnreadChat = getValue(dict, "KeyCommand.JumpToPreviousUnreadChat") + self.CheckoutInfo_ReceiverInfoPhone = getValue(dict, "CheckoutInfo.ReceiverInfoPhone") + self.GroupInfo_AddParticipantTitle = getValue(dict, "GroupInfo.AddParticipantTitle") + self._CHANNEL_MESSAGE_TEXT = getValue(dict, "CHANNEL_MESSAGE_TEXT") + self._CHANNEL_MESSAGE_TEXT_r = extractArgumentRanges(self._CHANNEL_MESSAGE_TEXT) + self.Checkout_PayNone = getValue(dict, "Checkout.PayNone") + self.CheckoutInfo_ErrorNameInvalid = getValue(dict, "CheckoutInfo.ErrorNameInvalid") + self.Channel_Share = getValue(dict, "Channel.Share") + self.Notification_PaymentSent = getValue(dict, "Notification.PaymentSent") + self.Settings_Username = getValue(dict, "Settings.Username") + self.Notification_CallMissedShort = getValue(dict, "Notification.CallMissedShort") + self.Call_CallInProgressTitle = getValue(dict, "Call.CallInProgressTitle") + self.PhotoEditor_Skip = getValue(dict, "PhotoEditor.Skip") + self.AuthSessions_TerminateOtherSessionsHelp = getValue(dict, "AuthSessions.TerminateOtherSessionsHelp") + self.Call_AudioRouteHeadphones = getValue(dict, "Call.AudioRouteHeadphones") + self.Contacts_InviteFriends = getValue(dict, "Contacts.InviteFriends") + self.Channel_BanUser_PermissionSendMessages = getValue(dict, "Channel.BanUser.PermissionSendMessages") + self.Notifications_InAppNotificationsVibrate = getValue(dict, "Notifications.InAppNotificationsVibrate") + self.StickerPack_Share = getValue(dict, "StickerPack.Share") + self.Watch_MessageView_Reply = getValue(dict, "Watch.MessageView.Reply") + self.Call_AudioRouteSpeaker = getValue(dict, "Call.AudioRouteSpeaker") + self.PrivacySettings_DeleteAccountNever = getValue(dict, "PrivacySettings.DeleteAccountNever") + self._WelcomeScreen_ContactsAccessHelp = getValue(dict, "WelcomeScreen.ContactsAccessHelp") + self._WelcomeScreen_ContactsAccessHelp_r = extractArgumentRanges(self._WelcomeScreen_ContactsAccessHelp) + self._MESSAGE_GEO = getValue(dict, "MESSAGE_GEO") + self._MESSAGE_GEO_r = extractArgumentRanges(self._MESSAGE_GEO) + self.Checkout_Title = getValue(dict, "Checkout.Title") + self.Privacy_Calls = getValue(dict, "Privacy.Calls") + self.Channel_AdminLogFilter_EventsInfo = getValue(dict, "Channel.AdminLogFilter.EventsInfo") + self._Channel_AdminLog_MessagePinned = getValue(dict, "Channel.AdminLog.MessagePinned") + self._Channel_AdminLog_MessagePinned_r = extractArgumentRanges(self._Channel_AdminLog_MessagePinned) + self._Channel_AdminLog_MessageToggleInvitesOn = getValue(dict, "Channel.AdminLog.MessageToggleInvitesOn") + self._Channel_AdminLog_MessageToggleInvitesOn_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleInvitesOn) + self.Conversation_SearchWebImages = getValue(dict, "Conversation.SearchWebImages") + self.KeyCommand_ScrollDown = getValue(dict, "KeyCommand.ScrollDown") + self.Conversation_LinkDialogSave = getValue(dict, "Conversation.LinkDialogSave") + self.Presence_offline = getValue(dict, "Presence.offline") + self.Conversation_SendMessageErrorFlood = getValue(dict, "Conversation.SendMessageErrorFlood") + self._Conversation_ForwardToPersonFormat = getValue(dict, "Conversation.ForwardToPersonFormat") + self._Conversation_ForwardToPersonFormat_r = extractArgumentRanges(self._Conversation_ForwardToPersonFormat) + self.CheckoutInfo_ErrorShippingNotAvailable = getValue(dict, "CheckoutInfo.ErrorShippingNotAvailable") + self.SharedMedia_Incoming = getValue(dict, "SharedMedia.Incoming") + self._Checkout_SavePasswordTimeoutAndTouchId = getValue(dict, "Checkout.SavePasswordTimeoutAndTouchId") + self._Checkout_SavePasswordTimeoutAndTouchId_r = extractArgumentRanges(self._Checkout_SavePasswordTimeoutAndTouchId) + self.CheckoutInfo_ShippingInfoCountry = getValue(dict, "CheckoutInfo.ShippingInfoCountry") + self.Map_ShowPlaces = getValue(dict, "Map.ShowPlaces") + self.Camera_VideoMode = getValue(dict, "Camera.VideoMode") + self._Watch_Time_ShortFullAt = getValue(dict, "Watch.Time.ShortFullAt") + self._Watch_Time_ShortFullAt_r = extractArgumentRanges(self._Watch_Time_ShortFullAt) + self.UserInfo_TelegramCall = getValue(dict, "UserInfo.TelegramCall") + self.PrivacyLastSeenSettings_CustomShareSettingsHelp = getValue(dict, "PrivacyLastSeenSettings.CustomShareSettingsHelp") + self.Channel_AdminLog_InfoPanelAlertText = getValue(dict, "Channel.AdminLog.InfoPanelAlertText") + self.Watch_State_WaitingForNetwork = getValue(dict, "Watch.State.WaitingForNetwork") + self.Cache_Photos = getValue(dict, "Cache.Photos") + self.Message_PinnedStickerMessage = getValue(dict, "Message.PinnedStickerMessage") + self.PhotoEditor_QualityMedium = getValue(dict, "PhotoEditor.QualityMedium") + self.Privacy_PaymentsClearInfo = getValue(dict, "Privacy.PaymentsClearInfo") + self.PhotoEditor_CurvesRed = getValue(dict, "PhotoEditor.CurvesRed") + self.Privacy_PaymentsTitle = getValue(dict, "Privacy.PaymentsTitle") + self.Login_PhoneNumberHelp = getValue(dict, "Login.PhoneNumberHelp") + self.User_DeletedAccount = getValue(dict, "User.DeletedAccount") + self.Call_StatusFailed = getValue(dict, "Call.StatusFailed") + self._Notification_GroupInviter = getValue(dict, "Notification.GroupInviter") + self._Notification_GroupInviter_r = extractArgumentRanges(self._Notification_GroupInviter) + self.Localization_ChooseLanguage = getValue(dict, "Localization.ChooseLanguage") + self.CheckoutInfo_ShippingInfoAddress2Placeholder = getValue(dict, "CheckoutInfo.ShippingInfoAddress2Placeholder") + self._Notification_SecretChatMessageScreenshot = getValue(dict, "Notification.SecretChatMessageScreenshot") + self._Notification_SecretChatMessageScreenshot_r = extractArgumentRanges(self._Notification_SecretChatMessageScreenshot) + self._DialogList_SingleUploadingPhotoSuffix = getValue(dict, "DialogList.SingleUploadingPhotoSuffix") + self._DialogList_SingleUploadingPhotoSuffix_r = extractArgumentRanges(self._DialogList_SingleUploadingPhotoSuffix) + self.Channel_LeaveChannel = getValue(dict, "Channel.LeaveChannel") + self.Compose_NewGroup = getValue(dict, "Compose.NewGroup") + self.TwoStepAuth_EmailPlaceholder = getValue(dict, "TwoStepAuth.EmailPlaceholder") + self.PhotoEditor_ExposureTool = getValue(dict, "PhotoEditor.ExposureTool") + self.ChatAdmins_AdminLabel = getValue(dict, "ChatAdmins.AdminLabel") + self.Contacts_FailedToSendInvitesMessage = getValue(dict, "Contacts.FailedToSendInvitesMessage") + self.Login_Code = getValue(dict, "Login.Code") + self.Channel_Username_InvalidCharacters = getValue(dict, "Channel.Username.InvalidCharacters") + self.Calls_CallTabTitle = getValue(dict, "Calls.CallTabTitle") + self.FeatureDisabled_Oops = getValue(dict, "FeatureDisabled.Oops") + self.Login_InviteButton = getValue(dict, "Login.InviteButton") + self.ShareMenu_Send = getValue(dict, "ShareMenu.Send") + self.Conversation_InfoGroup = getValue(dict, "Conversation.InfoGroup") + self.WatchRemote_AlertTitle = getValue(dict, "WatchRemote.AlertTitle") + self.Preview_ProfilePhotoTitle = getValue(dict, "Preview.ProfilePhotoTitle") + self.Checkout_Phone = getValue(dict, "Checkout.Phone") + self.Channel_SignMessages_Help = getValue(dict, "Channel.SignMessages.Help") + self.Calls_SubmitRating = getValue(dict, "Calls.SubmitRating") + self.Camera_FlashOn = getValue(dict, "Camera.FlashOn") + self.Watch_MessageView_Forward = getValue(dict, "Watch.MessageView.Forward") + self._Time_PreciseDate_6 = getValue(dict, "Time.PreciseDate_6") + self._Time_PreciseDate_6_r = extractArgumentRanges(self._Time_PreciseDate_6) + self.DialogList_You = getValue(dict, "DialogList.You") + self.Weekday_Monday = getValue(dict, "Weekday.Monday") + self.Watch_Suggestion_Yes = getValue(dict, "Watch.Suggestion.Yes") + self.AccessDenied_Camera = getValue(dict, "AccessDenied.Camera") + self.WatchRemote_NotificationText = getValue(dict, "WatchRemote.NotificationText") + self.Activity_Location = getValue(dict, "Activity.Location") + self.SharedMedia_ViewInChat = getValue(dict, "SharedMedia.ViewInChat") + self.Activity_RecordingAudio = getValue(dict, "Activity.RecordingAudio") + self.Watch_Stickers_StickerPacks = getValue(dict, "Watch.Stickers.StickerPacks") + self._Target_ShareGameConfirmationPrivate = getValue(dict, "Target.ShareGameConfirmationPrivate") + self._Target_ShareGameConfirmationPrivate_r = extractArgumentRanges(self._Target_ShareGameConfirmationPrivate) + self.Checkout_NewCard_PostcodePlaceholder = getValue(dict, "Checkout.NewCard.PostcodePlaceholder") + self.Conversation_SearchImages = getValue(dict, "Conversation.SearchImages") + self.DialogList_DeleteConversationConfirmation = getValue(dict, "DialogList.DeleteConversationConfirmation") + self.AttachmentMenu_SendAsFile = getValue(dict, "AttachmentMenu.SendAsFile") + self.Message_GamePreviewLabel = getValue(dict, "Message.GamePreviewLabel") + self.Checkout_ShippingOption_Header = getValue(dict, "Checkout.ShippingOption.Header") + self.Watch_Conversation_Unblock = getValue(dict, "Watch.Conversation.Unblock") + self.Channel_AdminLog_MessagePreviousLink = getValue(dict, "Channel.AdminLog.MessagePreviousLink") + self.CallSettings_PrivacyDescription = getValue(dict, "CallSettings.PrivacyDescription") + self.Conversation_ContextMenuCopy = getValue(dict, "Conversation.ContextMenuCopy") + self.GroupInfo_UpgradeButton = getValue(dict, "GroupInfo.UpgradeButton") + self.PrivacyLastSeenSettings_NeverShareWith = getValue(dict, "PrivacyLastSeenSettings.NeverShareWith") + self.ConvertToSupergroup_HelpText = getValue(dict, "ConvertToSupergroup.HelpText") + self.MediaPicker_VideoMuteDescription = getValue(dict, "MediaPicker.VideoMuteDescription") + self._SearchImages_Downloading_Mb = getValue(dict, "SearchImages.Downloading#Mb") + self._SearchImages_Downloading_Mb_r = extractArgumentRanges(self._SearchImages_Downloading_Mb) + self.UserInfo_ShareMyContactInfo = getValue(dict, "UserInfo.ShareMyContactInfo") + self._FileSize_GB = getValue(dict, "FileSize.GB") + self._FileSize_GB_r = extractArgumentRanges(self._FileSize_GB) + self.Month_ShortJanuary = getValue(dict, "Month.ShortJanuary") + self.Channel_BanUser_PermissionsHeader = getValue(dict, "Channel.BanUser.PermissionsHeader") + self.PhotoEditor_QualityVeryHigh = getValue(dict, "PhotoEditor.QualityVeryHigh") + self.Login_TermsOfServiceLabel = getValue(dict, "Login.TermsOfServiceLabel") + self._MESSAGE_TEXT = getValue(dict, "MESSAGE_TEXT") + self._MESSAGE_TEXT_r = extractArgumentRanges(self._MESSAGE_TEXT) + self.DialogList_NoMessagesTitle = getValue(dict, "DialogList.NoMessagesTitle") + self.AccessDenied_Contacts = getValue(dict, "AccessDenied.Contacts") + self.Your_cards_security_code_is_invalid = getValue(dict, "Your_cards_security_code_is_invalid") + self.Tour_StartButton = getValue(dict, "Tour.StartButton") + self.CheckoutInfo_Title = getValue(dict, "CheckoutInfo.Title") + self.ChangePhoneNumberCode_Help = getValue(dict, "ChangePhoneNumberCode.Help") + self.Web_Error = getValue(dict, "Web.Error") + self.ShareFileTip_Title = getValue(dict, "ShareFileTip.Title") + self.Username_InvalidStartsWithNumber = getValue(dict, "Username.InvalidStartsWithNumber") + self.ChatSettings_RevertLanguage = getValue(dict, "ChatSettings.RevertLanguage") + self.Conversation_ReportSpamAndLeave = getValue(dict, "Conversation.ReportSpamAndLeave") + self._DialogList_EncryptedChatStartedIncoming = getValue(dict, "DialogList.EncryptedChatStartedIncoming") + self._DialogList_EncryptedChatStartedIncoming_r = extractArgumentRanges(self._DialogList_EncryptedChatStartedIncoming) + self.Calls_AddTab = getValue(dict, "Calls.AddTab") + self.ChannelMembers_WhoCanAddMembers_Admins = getValue(dict, "ChannelMembers.WhoCanAddMembers.Admins") + self.Tour_Text5 = getValue(dict, "Tour.Text5") + self.Watch_Stickers_RecentPlaceholder = getValue(dict, "Watch.Stickers.RecentPlaceholder") + self.Common_Select = getValue(dict, "Common.Select") + self._Notification_MessageLifetimeRemoved = getValue(dict, "Notification.MessageLifetimeRemoved") + self._Notification_MessageLifetimeRemoved_r = extractArgumentRanges(self._Notification_MessageLifetimeRemoved) + self._PINNED_INVOICE = getValue(dict, "PINNED_INVOICE") + self._PINNED_INVOICE_r = extractArgumentRanges(self._PINNED_INVOICE) + self.Month_GenFebruary = getValue(dict, "Month.GenFebruary") + self.Contacts_SelectAll = getValue(dict, "Contacts.SelectAll") + self.Month_GenOctober = getValue(dict, "Month.GenOctober") + self.CheckoutInfo_ErrorPhoneInvalid = getValue(dict, "CheckoutInfo.ErrorPhoneInvalid") + self.SharedMedia_TitleVideo = getValue(dict, "SharedMedia.TitleVideo") + self.Checkout_PaymentMethod_New = getValue(dict, "Checkout.PaymentMethod.New") + self.ShareMenu_Comment = getValue(dict, "ShareMenu.Comment") + self.Channel_Management_LabelEditor = getValue(dict, "Channel.Management.LabelEditor") + self.TwoStepAuth_SetPasswordHelp = getValue(dict, "TwoStepAuth.SetPasswordHelp") + self.Channel_AdminLogFilter_EventsTitle = getValue(dict, "Channel.AdminLogFilter.EventsTitle") + self.Username_LinkCopied = getValue(dict, "Username.LinkCopied") + self.DialogList_Conversations = getValue(dict, "DialogList.Conversations") + self.Channel_EditAdmin_PermissionAddAdmins = getValue(dict, "Channel.EditAdmin.PermissionAddAdmins") + self.Conversation_SendMessage = getValue(dict, "Conversation.SendMessage") + self.Notification_CallIncoming = getValue(dict, "Notification.CallIncoming") + self._MESSAGE_FWDS = getValue(dict, "MESSAGE_FWDS") + self._MESSAGE_FWDS_r = extractArgumentRanges(self._MESSAGE_FWDS) + self._Time_PreciseDate_5 = getValue(dict, "Time.PreciseDate_5") + self._Time_PreciseDate_5_r = extractArgumentRanges(self._Time_PreciseDate_5) + self.Conversation_InputTextCommentPlaceholder = getValue(dict, "Conversation.InputTextCommentPlaceholder") + self.Map_OpenInYandexMaps = getValue(dict, "Map.OpenInYandexMaps") + self.Month_ShortNovember = getValue(dict, "Month.ShortNovember") + self.AccessDenied_Settings = getValue(dict, "AccessDenied.Settings") + self.EncryptionKey_Title = getValue(dict, "EncryptionKey.Title") + self.Profile_MessageLifetime1h = getValue(dict, "Profile.MessageLifetime1h") + self._Map_DistanceAway = getValue(dict, "Map.DistanceAway") + self._Map_DistanceAway_r = extractArgumentRanges(self._Map_DistanceAway) + self.Compose_NewMessage = getValue(dict, "Compose.NewMessage") + self.Checkout_ErrorPaymentFailed = getValue(dict, "Checkout.ErrorPaymentFailed") + self.Map_OpenInWaze = getValue(dict, "Map.OpenInWaze") + self.Common_ChooseVideo = getValue(dict, "Common.ChooseVideo") + self.Checkout_ShippingMethod = getValue(dict, "Checkout.ShippingMethod") + self.Login_InfoFirstNamePlaceholder = getValue(dict, "Login.InfoFirstNamePlaceholder") + self.DialogList_Broadcast = getValue(dict, "DialogList.Broadcast") + self.Checkout_ErrorProviderAccountInvalid = getValue(dict, "Checkout.ErrorProviderAccountInvalid") + self.CallSettings_TabIconDescription = getValue(dict, "CallSettings.TabIconDescription") + self.Checkout_WebConfirmation_Title = getValue(dict, "Checkout.WebConfirmation.Title") + self.PasscodeSettings_AutoLock = getValue(dict, "PasscodeSettings.AutoLock") + self.Notifications_MessageNotificationsPreview = getValue(dict, "Notifications.MessageNotificationsPreview") + self.Conversation_BlockUser = getValue(dict, "Conversation.BlockUser") + self.MessageTimer_Custom = getValue(dict, "MessageTimer.Custom") + self.Conversation_SilentBroadcastTooltipOff = getValue(dict, "Conversation.SilentBroadcastTooltipOff") + self.Conversation_Mute = getValue(dict, "Conversation.Mute") + self.Call_CallBack = getValue(dict, "Call.CallBack") + self.CreateGroup_SoftUserLimitAlert = getValue(dict, "CreateGroup.SoftUserLimitAlert") + self.AccessDenied_LocationDenied = getValue(dict, "AccessDenied.LocationDenied") + self.Tour_Title6 = getValue(dict, "Tour.Title6") + self.Settings_UsernameEmpty = getValue(dict, "Settings.UsernameEmpty") + self.PrivacySettings_TwoStepAuth = getValue(dict, "PrivacySettings.TwoStepAuth") + self.Conversation_FileICloudDrive = getValue(dict, "Conversation.FileICloudDrive") + self.KeyCommand_SendMessage = getValue(dict, "KeyCommand.SendMessage") + self._Channel_AdminLog_MessageDeleted = getValue(dict, "Channel.AdminLog.MessageDeleted") + self._Channel_AdminLog_MessageDeleted_r = extractArgumentRanges(self._Channel_AdminLog_MessageDeleted) + self.DialogList_DeleteBotConfirmation = getValue(dict, "DialogList.DeleteBotConfirmation") + self.Common_TakePhotoOrVideo = getValue(dict, "Common.TakePhotoOrVideo") + self.Notification_MessageLifetime2s = getValue(dict, "Notification.MessageLifetime2s") + self.Checkout_ErrorGeneric = getValue(dict, "Checkout.ErrorGeneric") + self.Conversation_FileGoogleDrive = getValue(dict, "Conversation.FileGoogleDrive") + self._MediaPicker_Processing = getValue(dict, "MediaPicker.Processing") + self._MediaPicker_Processing_r = extractArgumentRanges(self._MediaPicker_Processing) + self.Channel_AdminLog_CanBanUsers = getValue(dict, "Channel.AdminLog.CanBanUsers") + self.Cache_Indexing = getValue(dict, "Cache.Indexing") + self._ENCRYPTION_REQUEST = getValue(dict, "ENCRYPTION_REQUEST") + self._ENCRYPTION_REQUEST_r = extractArgumentRanges(self._ENCRYPTION_REQUEST) + self.StickerSettings_ContextInfo = getValue(dict, "StickerSettings.ContextInfo") + self.Message_SharedContact = getValue(dict, "Message.SharedContact") + self.Channel_BanUser_PermissionEmbedLinks = getValue(dict, "Channel.BanUser.PermissionEmbedLinks") + self.Channel_Username_CreateCommentsEnabled = getValue(dict, "Channel.Username.CreateCommentsEnabled") + self.GroupInfo_InviteLink_LinkSection = getValue(dict, "GroupInfo.InviteLink.LinkSection") + self.Privacy_Calls_AlwaysAllow_Placeholder = getValue(dict, "Privacy.Calls.AlwaysAllow.Placeholder") + self.CheckoutInfo_ShippingInfoPostcode = getValue(dict, "CheckoutInfo.ShippingInfoPostcode") + self.PasscodeSettings_EncryptDataHelp = getValue(dict, "PasscodeSettings.EncryptDataHelp") + self.KeyCommand_FocusOnInputField = getValue(dict, "KeyCommand.FocusOnInputField") + self.Cache_KeepMedia = getValue(dict, "Cache.KeepMedia") + self.WebPreview_GettingLinkInfo = getValue(dict, "WebPreview.GettingLinkInfo") + self.Group_Setup_TypePublicHelp = getValue(dict, "Group.Setup.TypePublicHelp") + self.Channel_Moderator_AccessLevelModeratorHelp = getValue(dict, "Channel.Moderator.AccessLevelModeratorHelp") + self.Map_Satellite = getValue(dict, "Map.Satellite") + self.Username_InvalidTaken = getValue(dict, "Username.InvalidTaken") + self._Notification_PinnedAudioMessage = getValue(dict, "Notification.PinnedAudioMessage") + self._Notification_PinnedAudioMessage_r = extractArgumentRanges(self._Notification_PinnedAudioMessage) + self.Notification_MessageLifetime1d = getValue(dict, "Notification.MessageLifetime1d") + self.Profile_MessageLifetime2s = getValue(dict, "Profile.MessageLifetime2s") + self._TwoStepAuth_RecoveryEmailUnavailable = getValue(dict, "TwoStepAuth.RecoveryEmailUnavailable") + self._TwoStepAuth_RecoveryEmailUnavailable_r = extractArgumentRanges(self._TwoStepAuth_RecoveryEmailUnavailable) + self.Calls_RatingFeedback = getValue(dict, "Calls.RatingFeedback") + self.Profile_EncryptionKey = getValue(dict, "Profile.EncryptionKey") + self.Watch_Suggestion_WhatsUp = getValue(dict, "Watch.Suggestion.WhatsUp") + self.LoginPassword_PasswordPlaceholder = getValue(dict, "LoginPassword.PasswordPlaceholder") + self.TwoStepAuth_EnterPasswordPassword = getValue(dict, "TwoStepAuth.EnterPasswordPassword") + self._CHANNEL_MESSAGE_CONTACT = getValue(dict, "CHANNEL_MESSAGE_CONTACT") + self._CHANNEL_MESSAGE_CONTACT_r = extractArgumentRanges(self._CHANNEL_MESSAGE_CONTACT) + self.PrivacySettings_DeleteAccountHelp = getValue(dict, "PrivacySettings.DeleteAccountHelp") + self._Time_PreciseDate_4 = getValue(dict, "Time.PreciseDate_4") + self._Time_PreciseDate_4_r = extractArgumentRanges(self._Time_PreciseDate_4) + self.Channel_Info_Banned = getValue(dict, "Channel.Info.Banned") + self.Conversation_ShareBotContactConfirmationTitle = getValue(dict, "Conversation.ShareBotContactConfirmationTitle") + self.ConversationProfile_UsersTooMuchError = getValue(dict, "ConversationProfile.UsersTooMuchError") + self.ChatAdmins_AllMembersAreAdminsOffHelp = getValue(dict, "ChatAdmins.AllMembersAreAdminsOffHelp") + self.Privacy_GroupsAndChannels_WhoCanAddMe = getValue(dict, "Privacy.GroupsAndChannels.WhoCanAddMe") + self.Settings_PhoneNumber = getValue(dict, "Settings.PhoneNumber") + self.Login_CodeExpiredError = getValue(dict, "Login.CodeExpiredError") + self._DialogList_MultipleTypingSuffix = getValue(dict, "DialogList.MultipleTypingSuffix") + self._DialogList_MultipleTypingSuffix_r = extractArgumentRanges(self._DialogList_MultipleTypingSuffix) + self.ChannelMembers_Blacklist_EmptyText = getValue(dict, "ChannelMembers.Blacklist.EmptyText") + self.Bot_GenericBotStatus = getValue(dict, "Bot.GenericBotStatus") + self.Common_edit = getValue(dict, "Common.edit") + self.Settings_AppLanguage = getValue(dict, "Settings.AppLanguage") + self.PrivacyLastSeenSettings_WhoCanSeeMyTimestamp = getValue(dict, "PrivacyLastSeenSettings.WhoCanSeeMyTimestamp") + self._Notification_Kicked = getValue(dict, "Notification.Kicked") + self._Notification_Kicked_r = extractArgumentRanges(self._Notification_Kicked) + self.Conversation_Send = getValue(dict, "Conversation.Send") + self.ChannelInfo_DeleteChannelConfirmation = getValue(dict, "ChannelInfo.DeleteChannelConfirmation") + self.Weekday_ShortSaturday = getValue(dict, "Weekday.ShortSaturday") + self.Map_SendThisLocation = getValue(dict, "Map.SendThisLocation") + self.DialogList_RecentTitleBots = getValue(dict, "DialogList.RecentTitleBots") + self._Notification_PinnedDocumentMessage = getValue(dict, "Notification.PinnedDocumentMessage") + self._Notification_PinnedDocumentMessage_r = extractArgumentRanges(self._Notification_PinnedDocumentMessage) + self.Conversation_ContextMenuReply = getValue(dict, "Conversation.ContextMenuReply") + self.Channel_BanUser_PermissionSendMedia = getValue(dict, "Channel.BanUser.PermissionSendMedia") + self.NetworkUsageSettings_Wifi = getValue(dict, "NetworkUsageSettings.Wifi") + self.Call_Accept = getValue(dict, "Call.Accept") + self.GroupInfo_SetGroupPhotoDelete = getValue(dict, "GroupInfo.SetGroupPhotoDelete") + self.PhotoEditor_CropAuto = getValue(dict, "PhotoEditor.CropAuto") + self.PhotoEditor_ContrastTool = getValue(dict, "PhotoEditor.ContrastTool") + self.MediaPicker_MomentsDateYearFormat = getValue(dict, "MediaPicker.MomentsDateYearFormat") + self.CheckoutInfo_ReceiverInfoNamePlaceholder = getValue(dict, "CheckoutInfo.ReceiverInfoNamePlaceholder") + self.Privacy_PaymentsClear_ShippingInfo = getValue(dict, "Privacy.PaymentsClear.ShippingInfo") + self.TwoStepAuth_GenericError = getValue(dict, "TwoStepAuth.GenericError") + self.Channel_Moderator_AccessLevelEditorHelp = getValue(dict, "Channel.Moderator.AccessLevelEditorHelp") + self.Compose_NewChannelButton = getValue(dict, "Compose.NewChannelButton") + self.ConversationMedia_EmptyTitle = getValue(dict, "ConversationMedia.EmptyTitle") + self.Date_DialogDateFormat = getValue(dict, "Date.DialogDateFormat") + self.ReportPeer_ReasonSpam = getValue(dict, "ReportPeer.ReasonSpam") + self.Compose_TokenListPlaceholder = getValue(dict, "Compose.TokenListPlaceholder") + self._PINNED_VIDEO = getValue(dict, "PINNED_VIDEO") + self._PINNED_VIDEO_r = extractArgumentRanges(self._PINNED_VIDEO) + self.StickerPacksSettings_Title = getValue(dict, "StickerPacksSettings.Title") + self.Privacy_Calls_NeverAllow_Placeholder = getValue(dict, "Privacy.Calls.NeverAllow.Placeholder") + self.Settings_Support = getValue(dict, "Settings.Support") + self.Notification_GroupInviterSelf = getValue(dict, "Notification.GroupInviterSelf") + self.MaskStickerSettings_Title = getValue(dict, "MaskStickerSettings.Title") + self.Watch_Suggestion_ThankYou = getValue(dict, "Watch.Suggestion.ThankYou") + self.TwoStepAuth_SetPassword = getValue(dict, "TwoStepAuth.SetPassword") + self.GoogleDrive_LoadErrorMessage = getValue(dict, "GoogleDrive.LoadErrorMessage") + self.GroupInfo_InviteLink_ShareLink = getValue(dict, "GroupInfo.InviteLink.ShareLink") + self.ChannelMembers_AllMembersMayInviteOnHelp = getValue(dict, "ChannelMembers.AllMembersMayInviteOnHelp") + self.Common_Cancel = getValue(dict, "Common.Cancel") + self.Preview_LoadingImages = getValue(dict, "Preview.LoadingImages") + self.ChangePhoneNumberCode_RequestingACall = getValue(dict, "ChangePhoneNumberCode.RequestingACall") + self.PrivacyLastSeenSettings_NeverShareWith_Title = getValue(dict, "PrivacyLastSeenSettings.NeverShareWith.Title") + self.KeyCommand_JumpToNextChat = getValue(dict, "KeyCommand.JumpToNextChat") + self.Tour_Text1 = getValue(dict, "Tour.Text1") + self.StickerPack_Remove = getValue(dict, "StickerPack.Remove") + self.Conversation_HoldForVideo = getValue(dict, "Conversation.HoldForVideo") + self.Checkout_NewCard_Title = getValue(dict, "Checkout.NewCard.Title") + self.Channel_TitleInfo = getValue(dict, "Channel.TitleInfo") + self.Settings_About_Help = getValue(dict, "Settings.About.Help") + self._Time_PreciseDate_3 = getValue(dict, "Time.PreciseDate_3") + self._Time_PreciseDate_3_r = extractArgumentRanges(self._Time_PreciseDate_3) + self.Watch_Conversation_Reply = getValue(dict, "Watch.Conversation.Reply") + self.ShareMenu_CopyShareLink = getValue(dict, "ShareMenu.CopyShareLink") + self.Channel_Setup_TypePrivateHelp = getValue(dict, "Channel.Setup.TypePrivateHelp") + self.PhotoEditor_GrainTool = getValue(dict, "PhotoEditor.GrainTool") + self.Watch_Suggestion_TalkLater = getValue(dict, "Watch.Suggestion.TalkLater") + self.TwoStepAuth_ChangeEmail = getValue(dict, "TwoStepAuth.ChangeEmail") + self._ENCRYPTION_ACCEPT = getValue(dict, "ENCRYPTION_ACCEPT") + self._ENCRYPTION_ACCEPT_r = extractArgumentRanges(self._ENCRYPTION_ACCEPT) + self.Conversation_ShareBotLocationConfirmationTitle = getValue(dict, "Conversation.ShareBotLocationConfirmationTitle") + self.NetworkUsageSettings_BytesSent = getValue(dict, "NetworkUsageSettings.BytesSent") + self.Conversation_ForwardContacts = getValue(dict, "Conversation.ForwardContacts") + self._Notification_ChangedGroupName = getValue(dict, "Notification.ChangedGroupName") + self._Notification_ChangedGroupName_r = extractArgumentRanges(self._Notification_ChangedGroupName) + self._MESSAGE_VIDEO = getValue(dict, "MESSAGE_VIDEO") + self._MESSAGE_VIDEO_r = extractArgumentRanges(self._MESSAGE_VIDEO) + self._Checkout_PayPrice = getValue(dict, "Checkout.PayPrice") + self._Checkout_PayPrice_r = extractArgumentRanges(self._Checkout_PayPrice) + self._Notification_PinnedTextMessage = getValue(dict, "Notification.PinnedTextMessage") + self._Notification_PinnedTextMessage_r = extractArgumentRanges(self._Notification_PinnedTextMessage) + self.GroupInfo_InvitationLinkDoesNotExist = getValue(dict, "GroupInfo.InvitationLinkDoesNotExist") + self.ReportPeer_ReasonOther_Placeholder = getValue(dict, "ReportPeer.ReasonOther.Placeholder") + self.PasscodeSettings_AutoLock_Disabled = getValue(dict, "PasscodeSettings.AutoLock.Disabled") + self.Wallpaper_Title = getValue(dict, "Wallpaper.Title") + self.Watch_Compose_CreateMessage = getValue(dict, "Watch.Compose.CreateMessage") + self.Message_Audio = getValue(dict, "Message.Audio") + self.Notification_CreatedGroup = getValue(dict, "Notification.CreatedGroup") + self.Conversation_SearchNoResults = getValue(dict, "Conversation.SearchNoResults") + self.ChannelMembers_BanList_EmptyText = getValue(dict, "ChannelMembers.BanList.EmptyText") + self.ReportPeer_ReasonViolence = getValue(dict, "ReportPeer.ReasonViolence") + self.Group_Username_RemoveExistingUsernamesInfo = getValue(dict, "Group.Username.RemoveExistingUsernamesInfo") + self.Message_InvoiceLabel = getValue(dict, "Message.InvoiceLabel") + self._LastSeen_AtWeekday = getValue(dict, "LastSeen.AtWeekday") + self._LastSeen_AtWeekday_r = extractArgumentRanges(self._LastSeen_AtWeekday) + self.Contacts_SearchLabel = getValue(dict, "Contacts.SearchLabel") + self.Group_Username_InvalidStartsWithNumber = getValue(dict, "Group.Username.InvalidStartsWithNumber") + self.Channel_AdminLogFilter_Title = getValue(dict, "Channel.AdminLogFilter.Title") + self.ChatAdmins_AllMembersAreAdminsOnHelp = getValue(dict, "ChatAdmins.AllMembersAreAdminsOnHelp") + self.Month_ShortSeptember = getValue(dict, "Month.ShortSeptember") + self.Group_Username_CreatePublicLinkHelp = getValue(dict, "Group.Username.CreatePublicLinkHelp") + self.Login_CallRequestState2 = getValue(dict, "Login.CallRequestState2") + self.TwoStepAuth_RecoveryUnavailable = getValue(dict, "TwoStepAuth.RecoveryUnavailable") + self.Bot_Unblock = getValue(dict, "Bot.Unblock") + self.SharedMedia_CategoryMedia = getValue(dict, "SharedMedia.CategoryMedia") + self.Conversation_HoldForAudio = getValue(dict, "Conversation.HoldForAudio") + self.Conversation_ClousStorageInfo_Description1 = getValue(dict, "Conversation.ClousStorageInfo.Description1") + self.Channel_Members_InviteLink = getValue(dict, "Channel.Members.InviteLink") + self.WebSearch_RecentClearConfirmation = getValue(dict, "WebSearch.RecentClearConfirmation") + self.Core_ServiceUserStatus = getValue(dict, "Core.ServiceUserStatus") + self.Notification_ChannelMigratedFrom = getValue(dict, "Notification.ChannelMigratedFrom") + self.Settings_Title = getValue(dict, "Settings.Title") + self.Call_StatusBusy = getValue(dict, "Call.StatusBusy") + self.ArchivedPacksAlert_Title = getValue(dict, "ArchivedPacksAlert.Title") + self.ConversationMedia_Title = getValue(dict, "ConversationMedia.Title") + self._Conversation_MessageViaUser = getValue(dict, "Conversation.MessageViaUser") + self._Conversation_MessageViaUser_r = extractArgumentRanges(self._Conversation_MessageViaUser) + self.Presence_invisible = getValue(dict, "Presence.invisible") + self.DialogList_Create = getValue(dict, "DialogList.Create") + self.Tour_Title4 = getValue(dict, "Tour.Title4") + self.Call_StatusEnded = getValue(dict, "Call.StatusEnded") + self.Conversation_UnpinMessageAlert = getValue(dict, "Conversation.UnpinMessageAlert") + self._Conversation_MessageDialogRetryAll = getValue(dict, "Conversation.MessageDialogRetryAll") + self._Conversation_MessageDialogRetryAll_r = extractArgumentRanges(self._Conversation_MessageDialogRetryAll) + self._Checkout_PasswordEntry_Text = getValue(dict, "Checkout.PasswordEntry.Text") + self._Checkout_PasswordEntry_Text_r = extractArgumentRanges(self._Checkout_PasswordEntry_Text) + self.Call_Message = getValue(dict, "Call.Message") + self.Contacts_MemberSearchSectionTitleGroup = getValue(dict, "Contacts.MemberSearchSectionTitleGroup") + self._Conversation_BotInteractiveUrlAlert = getValue(dict, "Conversation.BotInteractiveUrlAlert") + self._Conversation_BotInteractiveUrlAlert_r = extractArgumentRanges(self._Conversation_BotInteractiveUrlAlert) + self.GroupInfo_SharedMedia = getValue(dict, "GroupInfo.SharedMedia") + self.Channel_Username_InvalidStartsWithNumber = getValue(dict, "Channel.Username.InvalidStartsWithNumber") + self.KeyCommand_JumpToPreviousChat = getValue(dict, "KeyCommand.JumpToPreviousChat") + self.Conversation_Call = getValue(dict, "Conversation.Call") + self.KeyCommand_ScrollUp = getValue(dict, "KeyCommand.ScrollUp") + self._Privacy_GroupsAndChannels_InviteToChannelError = getValue(dict, "Privacy.GroupsAndChannels.InviteToChannelError") + self._Privacy_GroupsAndChannels_InviteToChannelError_r = extractArgumentRanges(self._Privacy_GroupsAndChannels_InviteToChannelError) + self.Document_TargetConfirmationFormat = getValue(dict, "Document.TargetConfirmationFormat") + self.Group_Setup_TypeHeader = getValue(dict, "Group.Setup.TypeHeader") + self._DialogList_SinglePlayingGameSuffix = getValue(dict, "DialogList.SinglePlayingGameSuffix") + self._DialogList_SinglePlayingGameSuffix_r = extractArgumentRanges(self._DialogList_SinglePlayingGameSuffix) + self.AttachmentMenu_SendAsFiles = getValue(dict, "AttachmentMenu.SendAsFiles") + self.Profile_MessageLifetime1m = getValue(dict, "Profile.MessageLifetime1m") + self.DialogList_SelectContact = getValue(dict, "DialogList.SelectContact") + self.Settings_AppleWatch = getValue(dict, "Settings.AppleWatch") + self.Conversation_View = getValue(dict, "Conversation.View") + self.Contacts_Invite = getValue(dict, "Contacts.Invite") + self.Channel_AdminLog_MessagePreviousDescription = getValue(dict, "Channel.AdminLog.MessagePreviousDescription") + self.Your_card_was_declined = getValue(dict, "Your_card_was_declined") + self.PhoneNumberHelp_ChangeNumber = getValue(dict, "PhoneNumberHelp.ChangeNumber") + self.ReportPeer_ReasonPornography = getValue(dict, "ReportPeer.ReasonPornography") + self.Notification_CreatedChannel = getValue(dict, "Notification.CreatedChannel") + self.PhotoEditor_Original = getValue(dict, "PhotoEditor.Original") + self.Target_SelectGroup = getValue(dict, "Target.SelectGroup") + self.Channel_AdminLog_InfoPanelAlertTitle = getValue(dict, "Channel.AdminLog.InfoPanelAlertTitle") + self.Notifications_GroupNotificationsPreview = getValue(dict, "Notifications.GroupNotificationsPreview") + self.Message_PinnedLocationMessage = getValue(dict, "Message.PinnedLocationMessage") + self.Settings_Logout = getValue(dict, "Settings.Logout") + self.Profile_Username = getValue(dict, "Profile.Username") + self.Group_Username_InvalidTooShort = getValue(dict, "Group.Username.InvalidTooShort") + self.AuthSessions_TerminateOtherSessions = getValue(dict, "AuthSessions.TerminateOtherSessions") + self.PasscodeSettings_TryAgainIn1Minute = getValue(dict, "PasscodeSettings.TryAgainIn1Minute") + self.Notifications_InAppNotifications = getValue(dict, "Notifications.InAppNotifications") + self.Channels_Title = getValue(dict, "Channels.Title") + self.StickerPack_ViewPack = getValue(dict, "StickerPack.ViewPack") + self.EnterPasscode_ChangeTitle = getValue(dict, "EnterPasscode.ChangeTitle") + self.Call_Decline = getValue(dict, "Call.Decline") + self.UserInfo_AddPhone = getValue(dict, "UserInfo.AddPhone") + self.Web_CopyLink = getValue(dict, "Web.CopyLink") + self.Activity_PlayingGame = getValue(dict, "Activity.PlayingGame") + self.CheckoutInfo_ShippingInfoStatePlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoStatePlaceholder") + self.Notifications_MessageNotificationsSound = getValue(dict, "Notifications.MessageNotificationsSound") + self.Call_StatusWaiting = getValue(dict, "Call.StatusWaiting") + self.Weekday_ShortWednesday = getValue(dict, "Weekday.ShortWednesday") + self.DC_UPDATE = getValue(dict, "DC_UPDATE") + self.PasscodeSettings_AutoLock_IfAwayFor_5hours = getValue(dict, "PasscodeSettings.AutoLock.IfAwayFor_5hours") + self.Notifications_Title = getValue(dict, "Notifications.Title") + self.Conversation_PinnedMessage = getValue(dict, "Conversation.PinnedMessage") + self.Channel_AdminLog_MessagePreviousMessage = getValue(dict, "Channel.AdminLog.MessagePreviousMessage") + self.ConversationProfile_LeaveDeleteAndExit = getValue(dict, "ConversationProfile.LeaveDeleteAndExit") + self.State_connecting = getValue(dict, "State.connecting") + self.WebPreview_LinkPreview = getValue(dict, "WebPreview.LinkPreview") + self.Map_OpenInHereMaps = getValue(dict, "Map.OpenInHereMaps") + self.CheckoutInfo_Pay = getValue(dict, "CheckoutInfo.Pay") + self.DialogList_Messages = getValue(dict, "DialogList.Messages") + self.Login_CountryCode = getValue(dict, "Login.CountryCode") + self.CheckoutInfo_ShippingInfoState = getValue(dict, "CheckoutInfo.ShippingInfoState") + self.Map_OpenInGooglePlus = getValue(dict, "Map.OpenInGooglePlus") + self._CHAT_MESSAGE_AUDIO = getValue(dict, "CHAT_MESSAGE_AUDIO") + self._CHAT_MESSAGE_AUDIO_r = extractArgumentRanges(self._CHAT_MESSAGE_AUDIO) + self.Login_SmsRequestState2 = getValue(dict, "Login.SmsRequestState2") + self.Preview_SaveToCameraRoll = getValue(dict, "Preview.SaveToCameraRoll") + self.PasscodeSettings_ChangePasscode = getValue(dict, "PasscodeSettings.ChangePasscode") + self.TwoStepAuth_RecoveryCodeInvalid = getValue(dict, "TwoStepAuth.RecoveryCodeInvalid") + self._Message_PaymentSent = getValue(dict, "Message.PaymentSent") + self._Message_PaymentSent_r = extractArgumentRanges(self._Message_PaymentSent) + self.Message_PinnedAudioMessage = getValue(dict, "Message.PinnedAudioMessage") + self.Login_InfoDeletePhoto = getValue(dict, "Login.InfoDeletePhoto") + self.Settings_SaveIncomingPhotosHelp = getValue(dict, "Settings.SaveIncomingPhotosHelp") + self.TwoStepAuth_RecoveryCodeExpired = getValue(dict, "TwoStepAuth.RecoveryCodeExpired") + self.TwoStepAuth_EmailTitle = getValue(dict, "TwoStepAuth.EmailTitle") + self.Privacy_GroupsAndChannels_NeverAllow = getValue(dict, "Privacy.GroupsAndChannels.NeverAllow") + self.Conversation_AddContact = getValue(dict, "Conversation.AddContact") + self.PhotoEditor_QualityLow = getValue(dict, "PhotoEditor.QualityLow") + self.Paint_Outlined = getValue(dict, "Paint.Outlined") + self.Checkout_PasswordEntry_Title = getValue(dict, "Checkout.PasswordEntry.Title") + self.Common_Done = getValue(dict, "Common.Done") + self.PrivacySettings_LastSeenContacts = getValue(dict, "PrivacySettings.LastSeenContacts") + self.CheckoutInfo_ShippingInfoAddress1 = getValue(dict, "CheckoutInfo.ShippingInfoAddress1") + self.UserInfo_LastNamePlaceholder = getValue(dict, "UserInfo.LastNamePlaceholder") + self.Conversation_StatusKickedFromChannel = getValue(dict, "Conversation.StatusKickedFromChannel") + self.GroupInfo_InviteLink_RevokeAlert_Text = getValue(dict, "GroupInfo.InviteLink.RevokeAlert.Text") + self._DialogList_SingleTypingSuffix = getValue(dict, "DialogList.SingleTypingSuffix") + self._DialogList_SingleTypingSuffix_r = extractArgumentRanges(self._DialogList_SingleTypingSuffix) + self.LastSeen_JustNow = getValue(dict, "LastSeen.JustNow") + self.CheckoutInfo_ShippingInfoAddress2 = getValue(dict, "CheckoutInfo.ShippingInfoAddress2") + self.Watch_Suggestion_No = getValue(dict, "Watch.Suggestion.No") + self.BroadcastListInfo_AddRecipient = getValue(dict, "BroadcastListInfo.AddRecipient") + self._Channel_Management_ErrorNotMember = getValue(dict, "Channel.Management.ErrorNotMember") + self._Channel_Management_ErrorNotMember_r = extractArgumentRanges(self._Channel_Management_ErrorNotMember) + self.Privacy_Calls_NeverAllow = getValue(dict, "Privacy.Calls.NeverAllow") + self.Settings_About_Title = getValue(dict, "Settings.About.Title") + self.PhoneNumberHelp_Help = getValue(dict, "PhoneNumberHelp.Help") + self.Service_NetworkConfigurationUpdatedMessage = getValue(dict, "Service.NetworkConfigurationUpdatedMessage") + self._Time_MonthOfYear_9 = getValue(dict, "Time.MonthOfYear_9") + self._Time_MonthOfYear_9_r = extractArgumentRanges(self._Time_MonthOfYear_9) + self.Channel_LinkItem = getValue(dict, "Channel.LinkItem") + self.Camera_Retake = getValue(dict, "Camera.Retake") + self.StickerPack_ShowStickers = getValue(dict, "StickerPack.ShowStickers") + self._CHAT_CREATED = getValue(dict, "CHAT_CREATED") + self._CHAT_CREATED_r = extractArgumentRanges(self._CHAT_CREATED) + self.LastSeen_WithinAMonth = getValue(dict, "LastSeen.WithinAMonth") + self._PrivacySettings_LastSeenContactsPlus = getValue(dict, "PrivacySettings.LastSeenContactsPlus") + self._PrivacySettings_LastSeenContactsPlus_r = extractArgumentRanges(self._PrivacySettings_LastSeenContactsPlus) + self.Conversation_FileHowTo = getValue(dict, "Conversation.FileHowTo") + self.ChangePhoneNumberNumber_NewNumber = getValue(dict, "ChangePhoneNumberNumber.NewNumber") + self.Compose_NewChannel = getValue(dict, "Compose.NewChannel") + self.Channel_AdminLog_CanChangeInviteLink = getValue(dict, "Channel.AdminLog.CanChangeInviteLink") + self._Call_CallInProgressMessage = getValue(dict, "Call.CallInProgressMessage") + self._Call_CallInProgressMessage_r = extractArgumentRanges(self._Call_CallInProgressMessage) + self.Conversation_InputTextBroadcastPlaceholder = getValue(dict, "Conversation.InputTextBroadcastPlaceholder") + self._ShareFileTip_Text = getValue(dict, "ShareFileTip.Text") + self._ShareFileTip_Text_r = extractArgumentRanges(self._ShareFileTip_Text) + self._CancelResetAccount_TextSMS = getValue(dict, "CancelResetAccount.TextSMS") + self._CancelResetAccount_TextSMS_r = extractArgumentRanges(self._CancelResetAccount_TextSMS) + self.Channel_EditAdmin_PermissionInviteUsers = getValue(dict, "Channel.EditAdmin.PermissionInviteUsers") + self.Conversation_Document = getValue(dict, "Conversation.Document") + self.SearchImages_RetryDownload = getValue(dict, "SearchImages.RetryDownload") + self.GroupInfo_DeleteAndExit = getValue(dict, "GroupInfo.DeleteAndExit") + self.GroupInfo_InviteLink_CopyLink = getValue(dict, "GroupInfo.InviteLink.CopyLink") + self.Weekday_Friday = getValue(dict, "Weekday.Friday") + self.Login_ResetAccountProtected_Title = getValue(dict, "Login.ResetAccountProtected.Title") + self.Settings_SetProfilePhoto = getValue(dict, "Settings.SetProfilePhoto") + self.Compose_ChannelTokenListPlaceholder = getValue(dict, "Compose.ChannelTokenListPlaceholder") + self.Channel_EditAdmin_PermissionPinMessages = getValue(dict, "Channel.EditAdmin.PermissionPinMessages") + self.Your_card_has_expired = getValue(dict, "Your_card_has_expired") + self._CHAT_MESSAGE_INVOICE = getValue(dict, "CHAT_MESSAGE_INVOICE") + self._CHAT_MESSAGE_INVOICE_r = extractArgumentRanges(self._CHAT_MESSAGE_INVOICE) + self.ChannelInfo_ConfirmLeave = getValue(dict, "ChannelInfo.ConfirmLeave") + self.ShareMenu_CopyShareLinkGame = getValue(dict, "ShareMenu.CopyShareLinkGame") + self.ReportPeer_ReasonOther = getValue(dict, "ReportPeer.ReasonOther") + self._Username_UsernameIsAvailable = getValue(dict, "Username.UsernameIsAvailable") + self._Username_UsernameIsAvailable_r = extractArgumentRanges(self._Username_UsernameIsAvailable) + self.KeyCommand_JumpToNextUnreadChat = getValue(dict, "KeyCommand.JumpToNextUnreadChat") + self.Conversation_EncryptedDescriptionTitle = getValue(dict, "Conversation.EncryptedDescriptionTitle") + self.DialogList_Pin = getValue(dict, "DialogList.Pin") + self._Notification_RemovedGroupPhoto = getValue(dict, "Notification.RemovedGroupPhoto") + self._Notification_RemovedGroupPhoto_r = extractArgumentRanges(self._Notification_RemovedGroupPhoto) + self.Channel_ErrorAddTooMuch = getValue(dict, "Channel.ErrorAddTooMuch") + self.GroupInfo_SharedMediaNone = getValue(dict, "GroupInfo.SharedMediaNone") + self.ChatSettings_TextSizeUnits = getValue(dict, "ChatSettings.TextSizeUnits") + self.ChatSettings_AutoPlayAnimations = getValue(dict, "ChatSettings.AutoPlayAnimations") + self.Conversation_FileOpenIn = getValue(dict, "Conversation.FileOpenIn") + self.Channel_Setup_TypePublic = getValue(dict, "Channel.Setup.TypePublic") + self._ChangePhone_ErrorOccupied = getValue(dict, "ChangePhone.ErrorOccupied") + self._ChangePhone_ErrorOccupied_r = extractArgumentRanges(self._ChangePhone_ErrorOccupied) + self.DialogList_RecentTitleGroups = getValue(dict, "DialogList.RecentTitleGroups") + self.Privacy_GroupsAndChannels_CustomShareHelp = getValue(dict, "Privacy.GroupsAndChannels.CustomShareHelp") + self.KeyCommand_ChatInfo = getValue(dict, "KeyCommand.ChatInfo") + self.Notification_CreatedBroadcastList = getValue(dict, "Notification.CreatedBroadcastList") + self.PhotoEditor_HighlightsTint = getValue(dict, "PhotoEditor.HighlightsTint") + self.Watch_Compose_AddContact = getValue(dict, "Watch.Compose.AddContact") + self.Coub_TapForSound = getValue(dict, "Coub.TapForSound") + self.Compose_NewEncryptedChat = getValue(dict, "Compose.NewEncryptedChat") + self.PhotoEditor_CropReset = getValue(dict, "PhotoEditor.CropReset") + self.Login_InvalidLastNameError = getValue(dict, "Login.InvalidLastNameError") + self.Channel_Members_AddMembers = getValue(dict, "Channel.Members.AddMembers") + self.Tour_Title2 = getValue(dict, "Tour.Title2") + self.Login_TermsOfServiceHeader = getValue(dict, "Login.TermsOfServiceHeader") + self.AuthSessions_OtherSessions = getValue(dict, "AuthSessions.OtherSessions") + self.Watch_UserInfo_Title = getValue(dict, "Watch.UserInfo.Title") + self.InstantPage_FeedbackButton = getValue(dict, "InstantPage.FeedbackButton") + self._Generic_OpenHiddenLinkAlert = getValue(dict, "Generic.OpenHiddenLinkAlert") + self._Generic_OpenHiddenLinkAlert_r = extractArgumentRanges(self._Generic_OpenHiddenLinkAlert) + self.Conversation_Contact = getValue(dict, "Conversation.Contact") + self.NetworkUsageSettings_GeneralDataSection = getValue(dict, "NetworkUsageSettings.GeneralDataSection") + self.Service_ApplyLocalization = getValue(dict, "Service.ApplyLocalization") + self._StickerPack_RemovePrompt = getValue(dict, "StickerPack.RemovePrompt") + self._StickerPack_RemovePrompt_r = extractArgumentRanges(self._StickerPack_RemovePrompt) + self.Channel_NotificationCommentsDisabled = getValue(dict, "Channel.NotificationCommentsDisabled") + self.EnterPasscode_RepeatNewPasscode = getValue(dict, "EnterPasscode.RepeatNewPasscode") + self.InstantPage_AutoNightTheme = getValue(dict, "InstantPage.AutoNightTheme") + self.CloudStorage_Title = getValue(dict, "CloudStorage.Title") + self.Month_ShortOctober = getValue(dict, "Month.ShortOctober") + self.Settings_FAQ = getValue(dict, "Settings.FAQ") + self.PrivacySettings_LastSeen = getValue(dict, "PrivacySettings.LastSeen") + self.DialogList_SearchSectionRecent = getValue(dict, "DialogList.SearchSectionRecent") + self.ChatSettings_AutomaticVideoMessageDownload = getValue(dict, "ChatSettings.AutomaticVideoMessageDownload") + self.Conversation_ContextMenuDelete = getValue(dict, "Conversation.ContextMenuDelete") + self.Tour_Text6 = getValue(dict, "Tour.Text6") + self.PhotoEditor_WarmthTool = getValue(dict, "PhotoEditor.WarmthTool") + self._Time_MonthOfYear_8 = getValue(dict, "Time.MonthOfYear_8") + self._Time_MonthOfYear_8_r = extractArgumentRanges(self._Time_MonthOfYear_8) + self.Common_TakePhoto = getValue(dict, "Common.TakePhoto") + self.PhotoEditor_Current = getValue(dict, "PhotoEditor.Current") + self.UserInfo_CreateNewContact = getValue(dict, "UserInfo.CreateNewContact") + self.NetworkUsageSettings_MediaDocumentDataSection = getValue(dict, "NetworkUsageSettings.MediaDocumentDataSection") + self.Login_CodeSentCall = getValue(dict, "Login.CodeSentCall") + self.Watch_PhotoView_Title = getValue(dict, "Watch.PhotoView.Title") + self._PrivacySettings_LastSeenContactsMinus = getValue(dict, "PrivacySettings.LastSeenContactsMinus") + self._PrivacySettings_LastSeenContactsMinus_r = extractArgumentRanges(self._PrivacySettings_LastSeenContactsMinus) + self.Login_InfoUpdatePhoto = getValue(dict, "Login.InfoUpdatePhoto") + self.ShareMenu_SelectChats = getValue(dict, "ShareMenu.SelectChats") + self.Group_ErrorSendRestrictedMedia = getValue(dict, "Group.ErrorSendRestrictedMedia") + self.Channel_EditAdmin_PermissinAddAdminOff = getValue(dict, "Channel.EditAdmin.PermissinAddAdminOff") + self.Cache_Files = getValue(dict, "Cache.Files") + self.PhotoEditor_EnhanceTool = getValue(dict, "PhotoEditor.EnhanceTool") + self.Conversation_SearchPlaceholder = getValue(dict, "Conversation.SearchPlaceholder") + self.Calls_Search = getValue(dict, "Calls.Search") + self.BroadcastListInfo_Title = getValue(dict, "BroadcastListInfo.Title") + self.WatchRemote_AlertText = getValue(dict, "WatchRemote.AlertText") + self.Channel_AdminLog_CanInviteUsers = getValue(dict, "Channel.AdminLog.CanInviteUsers") + self.Conversation_Block = getValue(dict, "Conversation.Block") + self.AttachmentMenu_PhotoOrVideo = getValue(dict, "AttachmentMenu.PhotoOrVideo") + self.Channel_BanUser_PermissionReadMessages = getValue(dict, "Channel.BanUser.PermissionReadMessages") + self.Month_ShortMarch = getValue(dict, "Month.ShortMarch") + self.GroupInfo_InviteLink_Title = getValue(dict, "GroupInfo.InviteLink.Title") + self.Watch_LastSeen_JustNow = getValue(dict, "Watch.LastSeen.JustNow") + self.BroadcastLists_Title = getValue(dict, "BroadcastLists.Title") + self.PhoneLabel_Title = getValue(dict, "PhoneLabel.Title") + self.PrivacySettings_Passcode = getValue(dict, "PrivacySettings.Passcode") + self.Paint_ClearConfirm = getValue(dict, "Paint.ClearConfirm") + self._Checkout_SavePasswordTimeout = getValue(dict, "Checkout.SavePasswordTimeout") + self._Checkout_SavePasswordTimeout_r = extractArgumentRanges(self._Checkout_SavePasswordTimeout) + self.PhotoEditor_BlurToolOff = getValue(dict, "PhotoEditor.BlurToolOff") + self.AccessDenied_VideoMicrophone = getValue(dict, "AccessDenied.VideoMicrophone") + self.Weekday_ShortThursday = getValue(dict, "Weekday.ShortThursday") + self.UserInfo_ShareContact = getValue(dict, "UserInfo.ShareContact") + self.LoginPassword_InvalidPasswordError = getValue(dict, "LoginPassword.InvalidPasswordError") + self.Login_PhoneAndCountryHelp = getValue(dict, "Login.PhoneAndCountryHelp") + self.CheckoutInfo_ReceiverInfoName = getValue(dict, "CheckoutInfo.ReceiverInfoName") + self._LastSeen_TodayAt = getValue(dict, "LastSeen.TodayAt") + self._LastSeen_TodayAt_r = extractArgumentRanges(self._LastSeen_TodayAt) + self._Time_YesterdayAt = getValue(dict, "Time.YesterdayAt") + self._Time_YesterdayAt_r = extractArgumentRanges(self._Time_YesterdayAt) + self.Weekday_Yesterday = getValue(dict, "Weekday.Yesterday") + self.Conversation_InputTextSilentBroadcastPlaceholder = getValue(dict, "Conversation.InputTextSilentBroadcastPlaceholder") + self.Embed_PlayingInPIP = getValue(dict, "Embed.PlayingInPIP") + self.Call_StatusIncoming = getValue(dict, "Call.StatusIncoming") + self.Conversation_Play = getValue(dict, "Conversation.Play") + self.Settings_PrivacySettings = getValue(dict, "Settings.PrivacySettings") + self.Conversation_SilentBroadcastTooltipOn = getValue(dict, "Conversation.SilentBroadcastTooltipOn") + self._CHAT_MESSAGE_GEO = getValue(dict, "CHAT_MESSAGE_GEO") + self._CHAT_MESSAGE_GEO_r = extractArgumentRanges(self._CHAT_MESSAGE_GEO) + self.DialogList_SearchLabel = getValue(dict, "DialogList.SearchLabel") + self.Login_CodeSentInternal = getValue(dict, "Login.CodeSentInternal") + self.Channel_AdminLog_BanSendMessages = getValue(dict, "Channel.AdminLog.BanSendMessages") + self.Channel_MessagePhotoRemoved = getValue(dict, "Channel.MessagePhotoRemoved") + self.Conversation_StatusKickedFromGroup = getValue(dict, "Conversation.StatusKickedFromGroup") + self.Compose_NewChannel_AddMemberHelp = getValue(dict, "Compose.NewChannel.AddMemberHelp") + self.GroupInfo_ChatAdmins = getValue(dict, "GroupInfo.ChatAdmins") + self.PhotoEditor_CurvesAll = getValue(dict, "PhotoEditor.CurvesAll") + self.Compose_Create = getValue(dict, "Compose.Create") + self._LOCKED_MESSAGE = getValue(dict, "LOCKED_MESSAGE") + self._LOCKED_MESSAGE_r = extractArgumentRanges(self._LOCKED_MESSAGE) + self.Conversation_ContextMenuShare = getValue(dict, "Conversation.ContextMenuShare") + self._Call_GroupFormat = getValue(dict, "Call.GroupFormat") + self._Call_GroupFormat_r = extractArgumentRanges(self._Call_GroupFormat) + self.Forward_ChannelReadOnly = getValue(dict, "Forward.ChannelReadOnly") + self.Privacy_GroupsAndChannels_NeverAllow_Title = getValue(dict, "Privacy.GroupsAndChannels.NeverAllow.Title") + self.Conversation_StatusGroupDeactivated = getValue(dict, "Conversation.StatusGroupDeactivated") + self._CHAT_JOINED = getValue(dict, "CHAT_JOINED") + self._CHAT_JOINED_r = extractArgumentRanges(self._CHAT_JOINED) + self.Conversation_Moderate_Ban = getValue(dict, "Conversation.Moderate.Ban") + self.Group_Status = getValue(dict, "Group.Status") + self.Watch_Suggestion_Absolutely = getValue(dict, "Watch.Suggestion.Absolutely") + self.Conversation_InputTextPlaceholder = getValue(dict, "Conversation.InputTextPlaceholder") + self._Time_MonthOfYear_7 = getValue(dict, "Time.MonthOfYear_7") + self._Time_MonthOfYear_7_r = extractArgumentRanges(self._Time_MonthOfYear_7) + self.SharedMedia_TitleAudio = getValue(dict, "SharedMedia.TitleAudio") + self.TwoStepAuth_RecoveryCode = getValue(dict, "TwoStepAuth.RecoveryCode") + self.SharedMedia_CategoryDocs = getValue(dict, "SharedMedia.CategoryDocs") + self.Channel_AdminLog_CanChangeInfo = getValue(dict, "Channel.AdminLog.CanChangeInfo") + self.Channel_AdminLogFilter_EventsAdmins = getValue(dict, "Channel.AdminLogFilter.EventsAdmins") + self._AuthSessions_AppUnofficial = getValue(dict, "AuthSessions.AppUnofficial") + self._AuthSessions_AppUnofficial_r = extractArgumentRanges(self._AuthSessions_AppUnofficial) + self.Channel_EditAdmin_PermissionsHeader = getValue(dict, "Channel.EditAdmin.PermissionsHeader") + self._DialogList_SingleUploadingVideoSuffix = getValue(dict, "DialogList.SingleUploadingVideoSuffix") + self._DialogList_SingleUploadingVideoSuffix_r = extractArgumentRanges(self._DialogList_SingleUploadingVideoSuffix) + self.Group_UpgradeNoticeHeader = getValue(dict, "Group.UpgradeNoticeHeader") + self._CHAT_DELETE_YOU = getValue(dict, "CHAT_DELETE_YOU") + self._CHAT_DELETE_YOU_r = extractArgumentRanges(self._CHAT_DELETE_YOU) + self._MESSAGE_NOTEXT = getValue(dict, "MESSAGE_NOTEXT") + self._MESSAGE_NOTEXT_r = extractArgumentRanges(self._MESSAGE_NOTEXT) + self._CHAT_MESSAGE_GIF = getValue(dict, "CHAT_MESSAGE_GIF") + self._CHAT_MESSAGE_GIF_r = extractArgumentRanges(self._CHAT_MESSAGE_GIF) + self.GroupInfo_InviteLink_CopyAlert_Success = getValue(dict, "GroupInfo.InviteLink.CopyAlert.Success") + self.Channel_Info_Members = getValue(dict, "Channel.Info.Members") + self.ShareFileTip_CloseTip = getValue(dict, "ShareFileTip.CloseTip") + self.KeyCommand_Find = getValue(dict, "KeyCommand.Find") + self.Preview_VideoNotYetDownloaded = getValue(dict, "Preview.VideoNotYetDownloaded") + self.Checkout_NewCard_PostcodeTitle = getValue(dict, "Checkout.NewCard.PostcodeTitle") + self._Channel_AdminLog_MessageRestricted = getValue(dict, "Channel.AdminLog.MessageRestricted") + self._Channel_AdminLog_MessageRestricted_r = extractArgumentRanges(self._Channel_AdminLog_MessageRestricted) + self.Channel_EditAdmin_PermissinAddAdminOn = getValue(dict, "Channel.EditAdmin.PermissinAddAdminOn") + self.WebSearch_GIFs = getValue(dict, "WebSearch.GIFs") + self.TwoStepAuth_EnterPasswordTitle = getValue(dict, "TwoStepAuth.EnterPasswordTitle") + self._CHANNEL_MESSAGE_GAME = getValue(dict, "CHANNEL_MESSAGE_GAME") + self._CHANNEL_MESSAGE_GAME_r = extractArgumentRanges(self._CHANNEL_MESSAGE_GAME) + self.AccessDenied_CallMicrophone = getValue(dict, "AccessDenied.CallMicrophone") + self.Conversation_DeleteMessagesForEveryone = getValue(dict, "Conversation.DeleteMessagesForEveryone") + self.UserInfo_TapToCall = getValue(dict, "UserInfo.TapToCall") + self.Common_Edit = getValue(dict, "Common.Edit") + self.Conversation_OpenFile = getValue(dict, "Conversation.OpenFile") + self.Message_PinnedDocumentMessage = getValue(dict, "Message.PinnedDocumentMessage") + self.Channel_ShareChannel = getValue(dict, "Channel.ShareChannel") + self.PrivacySettings_DeleteAccountNowConfirmation = getValue(dict, "PrivacySettings.DeleteAccountNowConfirmation") + self.Checkout_TotalPaidAmount = getValue(dict, "Checkout.TotalPaidAmount") + self.Conversation_UnsupportedMedia = getValue(dict, "Conversation.UnsupportedMedia") + self._Message_ForwardedMessage = getValue(dict, "Message.ForwardedMessage") + self._Message_ForwardedMessage_r = extractArgumentRanges(self._Message_ForwardedMessage) + self.Checkout_NewCard_SaveInfoEnableHelp = getValue(dict, "Checkout.NewCard.SaveInfoEnableHelp") + self.Call_AudioRouteHide = getValue(dict, "Call.AudioRouteHide") + self.CallSettings_OnMobile = getValue(dict, "CallSettings.OnMobile") + self.Conversation_GifTooltip = getValue(dict, "Conversation.GifTooltip") + self.CheckoutInfo_ErrorCityInvalid = getValue(dict, "CheckoutInfo.ErrorCityInvalid") + self.Profile_CreateEncryptedChatError = getValue(dict, "Profile.CreateEncryptedChatError") + self.Map_LocationTitle = getValue(dict, "Map.LocationTitle") + self.Compose_Recipients = getValue(dict, "Compose.Recipients") + self.Message_ReplyActionButtonShowReceipt = getValue(dict, "Message.ReplyActionButtonShowReceipt") + self.PhotoEditor_ShadowsTool = getValue(dict, "PhotoEditor.ShadowsTool") + self.Checkout_NewCard_CardholderNamePlaceholder = getValue(dict, "Checkout.NewCard.CardholderNamePlaceholder") + self.Cache_Title = getValue(dict, "Cache.Title") + self.Month_GenMay = getValue(dict, "Month.GenMay") + self._Notification_CreatedChat = getValue(dict, "Notification.CreatedChat") + self._Notification_CreatedChat_r = extractArgumentRanges(self._Notification_CreatedChat) + self.Calls_NoMissedCallsPlacehoder = getValue(dict, "Calls.NoMissedCallsPlacehoder") + self.Watch_UserInfo_Block = getValue(dict, "Watch.UserInfo.Block") + self.Watch_LastSeen_ALongTimeAgo = getValue(dict, "Watch.LastSeen.ALongTimeAgo") + self.StickerPacksSettings_ManagingHelp = getValue(dict, "StickerPacksSettings.ManagingHelp") + self.Privacy_GroupsAndChannels_InviteToChannelMultipleError = getValue(dict, "Privacy.GroupsAndChannels.InviteToChannelMultipleError") + self.PrivacySettings_TouchIdEnable = getValue(dict, "PrivacySettings.TouchIdEnable") + self.SearchImages_Title = getValue(dict, "SearchImages.Title") + self.Channel_BlackList_Title = getValue(dict, "Channel.BlackList.Title") + self.Checkout_NewCard_SaveInfo = getValue(dict, "Checkout.NewCard.SaveInfo") + self.Notification_CallMissed = getValue(dict, "Notification.CallMissed") + self.Profile_ShareContactButton = getValue(dict, "Profile.ShareContactButton") + self.Group_ErrorSendRestrictedStickers = getValue(dict, "Group.ErrorSendRestrictedStickers") + self.Bot_GroupStatusDoesNotReadHistory = getValue(dict, "Bot.GroupStatusDoesNotReadHistory") + self.Notification_Mute1h = getValue(dict, "Notification.Mute1h") + self.Cache_ClearCacheAlert = getValue(dict, "Cache.ClearCacheAlert") + self.BroadcastLists_NoListsYet = getValue(dict, "BroadcastLists.NoListsYet") + self.Settings_TabTitle = getValue(dict, "Settings.TabTitle") + self._Time_MonthOfYear_6 = getValue(dict, "Time.MonthOfYear_6") + self._Time_MonthOfYear_6_r = extractArgumentRanges(self._Time_MonthOfYear_6) + self.NetworkUsageSettings_MediaAudioDataSection = getValue(dict, "NetworkUsageSettings.MediaAudioDataSection") + self.GroupInfo_DeactivatedStatus = getValue(dict, "GroupInfo.DeactivatedStatus") + self._CHAT_PHOTO_EDITED = getValue(dict, "CHAT_PHOTO_EDITED") + self._CHAT_PHOTO_EDITED_r = extractArgumentRanges(self._CHAT_PHOTO_EDITED) + self.Conversation_ContextMenuMore = getValue(dict, "Conversation.ContextMenuMore") + self._PrivacySettings_LastSeenEverybodyMinus = getValue(dict, "PrivacySettings.LastSeenEverybodyMinus") + self._PrivacySettings_LastSeenEverybodyMinus_r = extractArgumentRanges(self._PrivacySettings_LastSeenEverybodyMinus) + self.Weekday_Today = getValue(dict, "Weekday.Today") + self.Login_InvalidFirstNameError = getValue(dict, "Login.InvalidFirstNameError") + self._Notification_Joined = getValue(dict, "Notification.Joined") + self._Notification_Joined_r = extractArgumentRanges(self._Notification_Joined) + self._VideoPreview_OptionHD = getValue(dict, "VideoPreview.OptionHD") + self._VideoPreview_OptionHD_r = extractArgumentRanges(self._VideoPreview_OptionHD) + self.Paint_Clear = getValue(dict, "Paint.Clear") + self.TwoStepAuth_RecoveryFailed = getValue(dict, "TwoStepAuth.RecoveryFailed") + self._MESSAGE_AUDIO = getValue(dict, "MESSAGE_AUDIO") + self._MESSAGE_AUDIO_r = extractArgumentRanges(self._MESSAGE_AUDIO) + self.Checkout_PasswordEntry_Pay = getValue(dict, "Checkout.PasswordEntry.Pay") + self.Notifications_MessageNotificationsHelp = getValue(dict, "Notifications.MessageNotificationsHelp") + self.Notification_EncryptedChatRequested = getValue(dict, "Notification.EncryptedChatRequested") + self.EnterPasscode_EnterCurrentPasscode = getValue(dict, "EnterPasscode.EnterCurrentPasscode") + self.Channel_Management_LabelModerator = getValue(dict, "Channel.Management.LabelModerator") + self._MESSAGE_GAME = getValue(dict, "MESSAGE_GAME") + self._MESSAGE_GAME_r = extractArgumentRanges(self._MESSAGE_GAME) + self.Conversation_Moderate_Report = getValue(dict, "Conversation.Moderate.Report") + self.MessageTimer_Forever = getValue(dict, "MessageTimer.Forever") + self._Conversation_EncryptedPlaceholderTitleIncoming = getValue(dict, "Conversation.EncryptedPlaceholderTitleIncoming") + self._Conversation_EncryptedPlaceholderTitleIncoming_r = extractArgumentRanges(self._Conversation_EncryptedPlaceholderTitleIncoming) + self._Map_AccurateTo = getValue(dict, "Map.AccurateTo") + self._Map_AccurateTo_r = extractArgumentRanges(self._Map_AccurateTo) + self._Call_ParticipantVersionOutdatedError = getValue(dict, "Call.ParticipantVersionOutdatedError") + self._Call_ParticipantVersionOutdatedError_r = extractArgumentRanges(self._Call_ParticipantVersionOutdatedError) + self.Tour_Text2 = getValue(dict, "Tour.Text2") + self.Preview_ViewStickerPack = getValue(dict, "Preview.ViewStickerPack") + self.Conversation_MessageDialogDelete = getValue(dict, "Conversation.MessageDialogDelete") + self.Calls_Clear = getValue(dict, "Calls.Clear") + self.Username_Placeholder = getValue(dict, "Username.Placeholder") + self._Notification_PinnedDeletedMessage = getValue(dict, "Notification.PinnedDeletedMessage") + self._Notification_PinnedDeletedMessage_r = extractArgumentRanges(self._Notification_PinnedDeletedMessage) + self.UserInfo_BotHelp = getValue(dict, "UserInfo.BotHelp") + self.Contacts_contact = getValue(dict, "Contacts.contact") + self.TwoStepAuth_PasswordSet = getValue(dict, "TwoStepAuth.PasswordSet") + self.Channel_Moderator_AccessLevelEditor = getValue(dict, "Channel.Moderator.AccessLevelEditor") + self.EnterPasscode_TouchId = getValue(dict, "EnterPasscode.TouchId") + self._CHANNEL_MESSAGE_VIDEO = getValue(dict, "CHANNEL_MESSAGE_VIDEO") + self._CHANNEL_MESSAGE_VIDEO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_VIDEO) + self.Checkout_ErrorInvoiceAlreadyPaid = getValue(dict, "Checkout.ErrorInvoiceAlreadyPaid") + self.ChatAdmins_Title = getValue(dict, "ChatAdmins.Title") + self.BroadcastLists_NoListsText = getValue(dict, "BroadcastLists.NoListsText") + self.ChannelMembers_WhoCanAddMembers = getValue(dict, "ChannelMembers.WhoCanAddMembers") + self.ChannelMembers_AllMembersMayInviteOffHelp = getValue(dict, "ChannelMembers.AllMembersMayInviteOffHelp") + self.Conversation_InfoPrivate = getValue(dict, "Conversation.InfoPrivate") + self.PasscodeSettings_Help = getValue(dict, "PasscodeSettings.Help") + self.Conversation_EditingMessagePanelTitle = getValue(dict, "Conversation.EditingMessagePanelTitle") + self._NetworkUsageSettings_CellularUsageSince = getValue(dict, "NetworkUsageSettings.CellularUsageSince") + self._NetworkUsageSettings_CellularUsageSince_r = extractArgumentRanges(self._NetworkUsageSettings_CellularUsageSince) + self.GroupInfo_ConvertToSupergroup = getValue(dict, "GroupInfo.ConvertToSupergroup") + self._Notification_PinnedContactMessage = getValue(dict, "Notification.PinnedContactMessage") + self._Notification_PinnedContactMessage_r = extractArgumentRanges(self._Notification_PinnedContactMessage) + self.CallSettings_UseLessDataLongDescription = getValue(dict, "CallSettings.UseLessDataLongDescription") + self.Conversation_SecretChatContextBotAlert = getValue(dict, "Conversation.SecretChatContextBotAlert") + self.Channel_Moderator_AccessLevelRevoke = getValue(dict, "Channel.Moderator.AccessLevelRevoke") + self.CheckoutInfo_ReceiverInfoTitle = getValue(dict, "CheckoutInfo.ReceiverInfoTitle") + self.Channel_AdminLogFilter_EventsRestrictions = getValue(dict, "Channel.AdminLogFilter.EventsRestrictions") + self.GroupInfo_InviteLink_RevokeLink = getValue(dict, "GroupInfo.InviteLink.RevokeLink") + self.Conversation_Unmute = getValue(dict, "Conversation.Unmute") + self.Checkout_PaymentMethod_Title = getValue(dict, "Checkout.PaymentMethod.Title") + self._AppLanguage_LanguageSuggested = getValue(dict, "AppLanguage.LanguageSuggested") + self._AppLanguage_LanguageSuggested_r = extractArgumentRanges(self._AppLanguage_LanguageSuggested) + self.Notifications_MessageNotifications = getValue(dict, "Notifications.MessageNotifications") + self.ChannelMembers_WhoCanAddMembersAdminsHelp = getValue(dict, "ChannelMembers.WhoCanAddMembersAdminsHelp") + self.DialogList_DeleteBotConversationConfirmation = getValue(dict, "DialogList.DeleteBotConversationConfirmation") + self._MediaPicker_AccessDeniedHelp = getValue(dict, "MediaPicker.AccessDeniedHelp") + self._MediaPicker_AccessDeniedHelp_r = extractArgumentRanges(self._MediaPicker_AccessDeniedHelp) + self._GroupInfo_InvitationLinkAccept = getValue(dict, "GroupInfo.InvitationLinkAccept") + self._GroupInfo_InvitationLinkAccept_r = extractArgumentRanges(self._GroupInfo_InvitationLinkAccept) + self.Conversation_ClousStorageInfo_Description2 = getValue(dict, "Conversation.ClousStorageInfo.Description2") + self.Map_Hybrid = getValue(dict, "Map.Hybrid") + self.Channel_Setup_Title = getValue(dict, "Channel.Setup.Title") + self.Activity_UploadingVideo = getValue(dict, "Activity.UploadingVideo") + self.Channel_Info_Management = getValue(dict, "Channel.Info.Management") + self._Notification_MessageLifetimeChangedOutgoing = getValue(dict, "Notification.MessageLifetimeChangedOutgoing") + self._Notification_MessageLifetimeChangedOutgoing_r = extractArgumentRanges(self._Notification_MessageLifetimeChangedOutgoing) + self.Conversation_DeleteOneMessage = getValue(dict, "Conversation.DeleteOneMessage") + self.PhotoEditor_QualityVeryLow = getValue(dict, "PhotoEditor.QualityVeryLow") + self.Month_ShortFebruary = getValue(dict, "Month.ShortFebruary") + self.Compose_NewBroadcast = getValue(dict, "Compose.NewBroadcast") + self.Conversation_ForwardTitle = getValue(dict, "Conversation.ForwardTitle") + self.Settings_FAQ_URL = getValue(dict, "Settings.FAQ_URL") + self.TwoStepAuth_ConfirmationChangeEmail = getValue(dict, "TwoStepAuth.ConfirmationChangeEmail") + self.Activity_RecordingVideoMessage = getValue(dict, "Activity.RecordingVideoMessage") + self.WelcomeScreen_ContactsAccessSettings = getValue(dict, "WelcomeScreen.ContactsAccessSettings") + self.SharedMedia_EmptyFilesText = getValue(dict, "SharedMedia.EmptyFilesText") + self._Contacts_AccessDeniedHelpLandscape = getValue(dict, "Contacts.AccessDeniedHelpLandscape") + self._Contacts_AccessDeniedHelpLandscape_r = extractArgumentRanges(self._Contacts_AccessDeniedHelpLandscape) + self.Channel_NotificationCommentsEnabled = getValue(dict, "Channel.NotificationCommentsEnabled") + self.PasscodeSettings_UnlockWithTouchId = getValue(dict, "PasscodeSettings.UnlockWithTouchId") + self.Contacts_AccessDeniedHelpON = getValue(dict, "Contacts.AccessDeniedHelpON") + self._Time_MonthOfYear_5 = getValue(dict, "Time.MonthOfYear_5") + self._Time_MonthOfYear_5_r = extractArgumentRanges(self._Time_MonthOfYear_5) + self._PrivacySettings_LastSeenContactsMinusPlus = getValue(dict, "PrivacySettings.LastSeenContactsMinusPlus") + self._PrivacySettings_LastSeenContactsMinusPlus_r = extractArgumentRanges(self._PrivacySettings_LastSeenContactsMinusPlus) + self.NetworkUsageSettings_ResetStats = getValue(dict, "NetworkUsageSettings.ResetStats") + self._Notification_ChannelInviter = getValue(dict, "Notification.ChannelInviter") + self._Notification_ChannelInviter_r = extractArgumentRanges(self._Notification_ChannelInviter) + self.Profile_MessageLifetimeForever = getValue(dict, "Profile.MessageLifetimeForever") + self.Conversation_Edit = getValue(dict, "Conversation.Edit") + self.TwoStepAuth_ResetAccountHelp = getValue(dict, "TwoStepAuth.ResetAccountHelp") + self.Month_GenDecember = getValue(dict, "Month.GenDecember") + self._Watch_LastSeen_YesterdayAt = getValue(dict, "Watch.LastSeen.YesterdayAt") + self._Watch_LastSeen_YesterdayAt_r = extractArgumentRanges(self._Watch_LastSeen_YesterdayAt) + self.Channel_ErrorAddBlocked = getValue(dict, "Channel.ErrorAddBlocked") + self.Conversation_Unpin = getValue(dict, "Conversation.Unpin") + self.Call_RecordingDisabledMessage = getValue(dict, "Call.RecordingDisabledMessage") + self.Conversation_Stop = getValue(dict, "Conversation.Stop") + self.Conversation_UnblockUser = getValue(dict, "Conversation.UnblockUser") + self.Conversation_Unblock = getValue(dict, "Conversation.Unblock") + self._CHANNEL_MESSAGE_GIF = getValue(dict, "CHANNEL_MESSAGE_GIF") + self._CHANNEL_MESSAGE_GIF_r = extractArgumentRanges(self._CHANNEL_MESSAGE_GIF) + self.Channel_AdminLogFilter_EventsEditedMessages = getValue(dict, "Channel.AdminLogFilter.EventsEditedMessages") + self.Channel_Username_InvalidTooShort = getValue(dict, "Channel.Username.InvalidTooShort") + self.Watch_LastSeen_WithinAWeek = getValue(dict, "Watch.LastSeen.WithinAWeek") + self.BlockedUsers_SelectUserTitle = getValue(dict, "BlockedUsers.SelectUserTitle") + self.Profile_MessageLifetime1w = getValue(dict, "Profile.MessageLifetime1w") + self.DialogList_TabTitle = getValue(dict, "DialogList.TabTitle") + self.UserInfo_GenericPhoneLabel = getValue(dict, "UserInfo.GenericPhoneLabel") + self.MediaPicker_MomentsDateFormat = getValue(dict, "MediaPicker.MomentsDateFormat") + self._Conversation_DownloadKilobytes = getValue(dict, "Conversation.DownloadKilobytes") + self._Conversation_DownloadKilobytes_r = extractArgumentRanges(self._Conversation_DownloadKilobytes) + self._Username_LinkHint = getValue(dict, "Username.LinkHint") + self._Username_LinkHint_r = extractArgumentRanges(self._Username_LinkHint) + self.NetworkUsageSettings_Title = getValue(dict, "NetworkUsageSettings.Title") + self.CheckoutInfo_ShippingInfoPostcodePlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoPostcodePlaceholder") + self.Wallpaper_Wallpaper = getValue(dict, "Wallpaper.Wallpaper") + self.GroupInfo_InviteLink_RevokeAlert_Revoke = getValue(dict, "GroupInfo.InviteLink.RevokeAlert.Revoke") + self.SharedMedia_TitleLink = getValue(dict, "SharedMedia.TitleLink") + self.Channel_JoinChannel = getValue(dict, "Channel.JoinChannel") + self.StickerPack_Add = getValue(dict, "StickerPack.Add") + self.Group_ErrorNotMutualContact = getValue(dict, "Group.ErrorNotMutualContact") + self.AccessDenied_LocationDisabled = getValue(dict, "AccessDenied.LocationDisabled") + self.Conversation_DownloadPhoto = getValue(dict, "Conversation.DownloadPhoto") + self.Login_UnknownError = getValue(dict, "Login.UnknownError") + self.Presence_online = getValue(dict, "Presence.online") + self.DialogList_Title = getValue(dict, "DialogList.Title") + self.Stickers_Install = getValue(dict, "Stickers.Install") + self.SearchImages_NoImagesFound = getValue(dict, "SearchImages.NoImagesFound") + self._Notification_RemovedUserPhoto = getValue(dict, "Notification.RemovedUserPhoto") + self._Notification_RemovedUserPhoto_r = extractArgumentRanges(self._Notification_RemovedUserPhoto) + self._Watch_Time_ShortTodayAt = getValue(dict, "Watch.Time.ShortTodayAt") + self._Watch_Time_ShortTodayAt_r = extractArgumentRanges(self._Watch_Time_ShortTodayAt) + self.UserInfo_GroupsInCommon = getValue(dict, "UserInfo.GroupsInCommon") + self.ChatSettings_Language = getValue(dict, "ChatSettings.Language") + self.AccessDenied_CameraDisabled = getValue(dict, "AccessDenied.CameraDisabled") + self.Message_PinnedContactMessage = getValue(dict, "Message.PinnedContactMessage") + self.UserInfo_Call = getValue(dict, "UserInfo.Call") + self.Conversation_InputTextDisabledPlaceholder = getValue(dict, "Conversation.InputTextDisabledPlaceholder") + self.Map_ForwardViaTelegram = getValue(dict, "Map.ForwardViaTelegram") + self.Month_GenMarch = getValue(dict, "Month.GenMarch") + self.Watch_UserInfo_Unmute = getValue(dict, "Watch.UserInfo.Unmute") + self.PhotoEditor_BlurTool = getValue(dict, "PhotoEditor.BlurTool") + self.Common_Delete = getValue(dict, "Common.Delete") + self.Username_Title = getValue(dict, "Username.Title") + self.Login_PhoneFloodError = getValue(dict, "Login.PhoneFloodError") + self.CheckoutInfo_ErrorPostcodeInvalid = getValue(dict, "CheckoutInfo.ErrorPostcodeInvalid") + self._CHANNEL_MESSAGE_PHOTO = getValue(dict, "CHANNEL_MESSAGE_PHOTO") + self._CHANNEL_MESSAGE_PHOTO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_PHOTO) + self.Channel_AdminLog_InfoPanelTitle = getValue(dict, "Channel.AdminLog.InfoPanelTitle") + self.Group_ErrorAddTooMuchBots = getValue(dict, "Group.ErrorAddTooMuchBots") + self._Notification_CallFormat = getValue(dict, "Notification.CallFormat") + self._Notification_CallFormat_r = extractArgumentRanges(self._Notification_CallFormat) + self._CHAT_MESSAGE_PHOTO = getValue(dict, "CHAT_MESSAGE_PHOTO") + self._CHAT_MESSAGE_PHOTO_r = extractArgumentRanges(self._CHAT_MESSAGE_PHOTO) + self._Channel_AdminLog_MessageToggleInvitesOff = getValue(dict, "Channel.AdminLog.MessageToggleInvitesOff") + self._Channel_AdminLog_MessageToggleInvitesOff_r = extractArgumentRanges(self._Channel_AdminLog_MessageToggleInvitesOff) + self.UserInfo_ShareBot = getValue(dict, "UserInfo.ShareBot") + self.TwoStepAuth_EmailSkip = getValue(dict, "TwoStepAuth.EmailSkip") + self.Conversation_JumpToDate = getValue(dict, "Conversation.JumpToDate") + self.CheckoutInfo_ReceiverInfoEmailPlaceholder = getValue(dict, "CheckoutInfo.ReceiverInfoEmailPlaceholder") + self.Message_Photo = getValue(dict, "Message.Photo") + self.Conversation_ReportSpam = getValue(dict, "Conversation.ReportSpam") + self.Camera_FlashAuto = getValue(dict, "Camera.FlashAuto") + self._Time_MonthOfYear_4 = getValue(dict, "Time.MonthOfYear_4") + self._Time_MonthOfYear_4_r = extractArgumentRanges(self._Time_MonthOfYear_4) + self.Call_ConnectionErrorMessage = getValue(dict, "Call.ConnectionErrorMessage") + self.Compose_NewChannel_AddMember = getValue(dict, "Compose.NewChannel.AddMember") + self.Watch_State_Updating = getValue(dict, "Watch.State.Updating") + self.LastSeen_ALongTimeAgo = getValue(dict, "LastSeen.ALongTimeAgo") + self.DialogList_SearchSectionGlobal = getValue(dict, "DialogList.SearchSectionGlobal") + self.ChangePhoneNumberNumber_NumberPlaceholder = getValue(dict, "ChangePhoneNumberNumber.NumberPlaceholder") + self.GroupInfo_AddUserLeftError = getValue(dict, "GroupInfo.AddUserLeftError") + self.GroupInfo_GroupType = getValue(dict, "GroupInfo.GroupType") + self.Watch_Suggestion_OnMyWay = getValue(dict, "Watch.Suggestion.OnMyWay") + self.Checkout_NewCard_PaymentCard = getValue(dict, "Checkout.NewCard.PaymentCard") + self.PhotoEditor_CropAspectRatioOriginal = getValue(dict, "PhotoEditor.CropAspectRatioOriginal") + self.MediaPicker_MomentsDateRangeFormat = getValue(dict, "MediaPicker.MomentsDateRangeFormat") + self.UserInfo_NotificationsDisabled = getValue(dict, "UserInfo.NotificationsDisabled") + self._CONTACT_JOINED = getValue(dict, "CONTACT_JOINED") + self._CONTACT_JOINED_r = extractArgumentRanges(self._CONTACT_JOINED) + self.PrivacyLastSeenSettings_AlwaysShareWith_Title = getValue(dict, "PrivacyLastSeenSettings.AlwaysShareWith.Title") + self.BlockedUsers_LeavePrefix = getValue(dict, "BlockedUsers.LeavePrefix") + self.NetworkUsageSettings_ResetStatsConfirmation = getValue(dict, "NetworkUsageSettings.ResetStatsConfirmation") + self.Channel_EditAdmin_PermissionPostMessages = getValue(dict, "Channel.EditAdmin.PermissionPostMessages") + self.DialogList_EncryptionProcessing = getValue(dict, "DialogList.EncryptionProcessing") + self.Conversation_ApplyLocalization = getValue(dict, "Conversation.ApplyLocalization") + self.Conversation_DeleteManyMessages = getValue(dict, "Conversation.DeleteManyMessages") + self.CancelResetAccount_Title = getValue(dict, "CancelResetAccount.Title") + self.Notification_CallOutgoingShort = getValue(dict, "Notification.CallOutgoingShort") + self.Channel_Moderator_AccessLevelHeader = getValue(dict, "Channel.Moderator.AccessLevelHeader") + self.SharedMedia_TitleAll = getValue(dict, "SharedMedia.TitleAll") + self.Conversation_SlideToCancel = getValue(dict, "Conversation.SlideToCancel") + self.AuthSessions_TerminateSession = getValue(dict, "AuthSessions.TerminateSession") + self.Channel_AdminLogFilter_EventsDeletedMessages = getValue(dict, "Channel.AdminLogFilter.EventsDeletedMessages") + self.PrivacyLastSeenSettings_AlwaysShareWith_Placeholder = getValue(dict, "PrivacyLastSeenSettings.AlwaysShareWith.Placeholder") + self.Channel_Members_Title = getValue(dict, "Channel.Members.Title") + self.Channel_AdminLog_CanDeleteMessages = getValue(dict, "Channel.AdminLog.CanDeleteMessages") + self.Group_Setup_TypePrivateHelp = getValue(dict, "Group.Setup.TypePrivateHelp") + self._Notification_PinnedVideoMessage = getValue(dict, "Notification.PinnedVideoMessage") + self._Notification_PinnedVideoMessage_r = extractArgumentRanges(self._Notification_PinnedVideoMessage) + self.Conversation_ContextMenuStickerPackAdd = getValue(dict, "Conversation.ContextMenuStickerPackAdd") + self.Channel_AdminLogFilter_EventsNewMembers = getValue(dict, "Channel.AdminLogFilter.EventsNewMembers") + self.Channel_AdminLogFilter_EventsPinned = getValue(dict, "Channel.AdminLogFilter.EventsPinned") + self._Conversation_Moderate_DeleteAllMessages = getValue(dict, "Conversation.Moderate.DeleteAllMessages") + self._Conversation_Moderate_DeleteAllMessages_r = extractArgumentRanges(self._Conversation_Moderate_DeleteAllMessages) + self.SharedMedia_CategoryOther = getValue(dict, "SharedMedia.CategoryOther") + self.GoogleDrive_LogoutMessage = getValue(dict, "GoogleDrive.LogoutMessage") + self.Preview_DeletePhoto = getValue(dict, "Preview.DeletePhoto") + self.PasscodeSettings_TurnPasscodeOn = getValue(dict, "PasscodeSettings.TurnPasscodeOn") + self.GroupInfo_ChannelListNamePlaceholder = getValue(dict, "GroupInfo.ChannelListNamePlaceholder") + self.DialogList_Unpin = getValue(dict, "DialogList.Unpin") + self.GroupInfo_SetGroupPhoto = getValue(dict, "GroupInfo.SetGroupPhoto") + self.StickerPacksSettings_ArchivedPacks_Info = getValue(dict, "StickerPacksSettings.ArchivedPacks.Info") + self.ConvertToSupergroup_Title = getValue(dict, "ConvertToSupergroup.Title") + self._CHAT_MESSAGE_NOTEXT = getValue(dict, "CHAT_MESSAGE_NOTEXT") + self._CHAT_MESSAGE_NOTEXT_r = extractArgumentRanges(self._CHAT_MESSAGE_NOTEXT) + self.Channel_Setup_TypeHeader = getValue(dict, "Channel.Setup.TypeHeader") + self._Notification_NewAuthDetected = getValue(dict, "Notification.NewAuthDetected") + self._Notification_NewAuthDetected_r = extractArgumentRanges(self._Notification_NewAuthDetected) + self.Notification_CallCanceledShort = getValue(dict, "Notification.CallCanceledShort") + self.PhotoEditor_RevertMessage = getValue(dict, "PhotoEditor.RevertMessage") + self.AccessDenied_VideoMessageCamera = getValue(dict, "AccessDenied.VideoMessageCamera") + self.Conversation_Search = getValue(dict, "Conversation.Search") + self._Channel_Management_PromotedBy = getValue(dict, "Channel.Management.PromotedBy") + self._Channel_Management_PromotedBy_r = extractArgumentRanges(self._Channel_Management_PromotedBy) + self._PrivacySettings_LastSeenNobodyPlus = getValue(dict, "PrivacySettings.LastSeenNobodyPlus") + self._PrivacySettings_LastSeenNobodyPlus_r = extractArgumentRanges(self._PrivacySettings_LastSeenNobodyPlus) + self.Preview_ForwardViaTelegram = getValue(dict, "Preview.ForwardViaTelegram") + self.Notifications_InAppNotificationsSounds = getValue(dict, "Notifications.InAppNotificationsSounds") + self.Call_StatusRequesting = getValue(dict, "Call.StatusRequesting") + self._CHAT_MESSAGE_CONTACT = getValue(dict, "CHAT_MESSAGE_CONTACT") + self._CHAT_MESSAGE_CONTACT_r = extractArgumentRanges(self._CHAT_MESSAGE_CONTACT) + self.Group_UpgradeNoticeText1 = getValue(dict, "Group.UpgradeNoticeText1") + self.ChatSettings_Other = getValue(dict, "ChatSettings.Other") + self._Channel_AdminLog_MessageChangedChannelAbout = getValue(dict, "Channel.AdminLog.MessageChangedChannelAbout") + self._Channel_AdminLog_MessageChangedChannelAbout_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedChannelAbout) + self._Call_EmojiDescription = getValue(dict, "Call.EmojiDescription") + self._Call_EmojiDescription_r = extractArgumentRanges(self._Call_EmojiDescription) + self.Settings_SaveIncomingPhotos = getValue(dict, "Settings.SaveIncomingPhotos") + self._Conversation_Bytes = getValue(dict, "Conversation.Bytes") + self._Conversation_Bytes_r = extractArgumentRanges(self._Conversation_Bytes) + self.GroupInfo_InviteLink_Help = getValue(dict, "GroupInfo.InviteLink.Help") + self._Time_MonthOfYear_3 = getValue(dict, "Time.MonthOfYear_3") + self._Time_MonthOfYear_3_r = extractArgumentRanges(self._Time_MonthOfYear_3) + self.Conversation_ContextMenuForward = getValue(dict, "Conversation.ContextMenuForward") + self.Calls_Missed = getValue(dict, "Calls.Missed") + self.Call_StatusRinging = getValue(dict, "Call.StatusRinging") + self.Invitation_JoinGroup = getValue(dict, "Invitation.JoinGroup") + self.Notification_PinnedMessage = getValue(dict, "Notification.PinnedMessage") + self.Message_Location = getValue(dict, "Message.Location") + self._Notification_MessageLifetimeChanged = getValue(dict, "Notification.MessageLifetimeChanged") + self._Notification_MessageLifetimeChanged_r = extractArgumentRanges(self._Notification_MessageLifetimeChanged) + self.Message_Contact = getValue(dict, "Message.Contact") + self._Watch_LastSeen_TodayAt = getValue(dict, "Watch.LastSeen.TodayAt") + self._Watch_LastSeen_TodayAt_r = extractArgumentRanges(self._Watch_LastSeen_TodayAt) + self.Channel_Moderator_AccessLevelModerator = getValue(dict, "Channel.Moderator.AccessLevelModerator") + self.GoogleDrive_Logout = getValue(dict, "GoogleDrive.Logout") + self.PhotoEditor_RevertToOriginal = getValue(dict, "PhotoEditor.RevertToOriginal") + self.Common_More = getValue(dict, "Common.More") + self.Preview_OpenInInstagram = getValue(dict, "Preview.OpenInInstagram") + self.PhotoEditor_HighlightsTool = getValue(dict, "PhotoEditor.HighlightsTool") + self._Channel_Username_UsernameIsAvailable = getValue(dict, "Channel.Username.UsernameIsAvailable") + self._Channel_Username_UsernameIsAvailable_r = extractArgumentRanges(self._Channel_Username_UsernameIsAvailable) + self._PINNED_GAME = getValue(dict, "PINNED_GAME") + self._PINNED_GAME_r = extractArgumentRanges(self._PINNED_GAME) + self.GroupInfo_BroadcastListNamePlaceholder = getValue(dict, "GroupInfo.BroadcastListNamePlaceholder") + self.Conversation_ShareBotContactConfirmation = getValue(dict, "Conversation.ShareBotContactConfirmation") + self.Login_CodeSentSms = getValue(dict, "Login.CodeSentSms") + self.Conversation_ReportSpamConfirmation = getValue(dict, "Conversation.ReportSpamConfirmation") + self.ChannelMembers_ChannelAdminsTitle = getValue(dict, "ChannelMembers.ChannelAdminsTitle") + self.CallSettings_UseLessData = getValue(dict, "CallSettings.UseLessData") + self._TwoStepAuth_EnterPasswordHint = getValue(dict, "TwoStepAuth.EnterPasswordHint") + self._TwoStepAuth_EnterPasswordHint_r = extractArgumentRanges(self._TwoStepAuth_EnterPasswordHint) + self.CallSettings_TabIcon = getValue(dict, "CallSettings.TabIcon") + self.Conversation_EditForward = getValue(dict, "Conversation.EditForward") + self.ConversationProfile_UnknownAddMemberError = getValue(dict, "ConversationProfile.UnknownAddMemberError") + self._Conversation_FileHowToText = getValue(dict, "Conversation.FileHowToText") + self._Conversation_FileHowToText_r = extractArgumentRanges(self._Conversation_FileHowToText) + self.Channel_AdminLog_BanSendMedia = getValue(dict, "Channel.AdminLog.BanSendMedia") + self.Tour_Text7 = getValue(dict, "Tour.Text7") + self.Contacts_contactsvar = getValue(dict, "Contacts.contactsvar") + self.Watch_UserInfo_Unblock = getValue(dict, "Watch.UserInfo.Unblock") + self.Conversation_EditDelete = getValue(dict, "Conversation.EditDelete") + self.Conversation_ViewPhoto = getValue(dict, "Conversation.ViewPhoto") + self.StickerPacksSettings_ArchivedMasks = getValue(dict, "StickerPacksSettings.ArchivedMasks") + self.Message_Animation = getValue(dict, "Message.Animation") + self.Checkout_PaymentMethod = getValue(dict, "Checkout.PaymentMethod") + self.Channel_AdminLog_TitleSelectedEvents = getValue(dict, "Channel.AdminLog.TitleSelectedEvents") + self.Privacy_Calls_NeverAllow_Title = getValue(dict, "Privacy.Calls.NeverAllow.Title") + self.Cache_Music = getValue(dict, "Cache.Music") + self._Login_CallRequestState1 = getValue(dict, "Login.CallRequestState1") + self._Login_CallRequestState1_r = extractArgumentRanges(self._Login_CallRequestState1) + self._SearchImages_ImageNofM = getValue(dict, "SearchImages.ImageNofM") + self._SearchImages_ImageNofM_r = extractArgumentRanges(self._SearchImages_ImageNofM) + self.Channel_Username_CreatePrivateLinkHelp = getValue(dict, "Channel.Username.CreatePrivateLinkHelp") + self._FileSize_B = getValue(dict, "FileSize.B") + self._FileSize_B_r = extractArgumentRanges(self._FileSize_B) + self._Target_ShareGameConfirmationGroup = getValue(dict, "Target.ShareGameConfirmationGroup") + self._Target_ShareGameConfirmationGroup_r = extractArgumentRanges(self._Target_ShareGameConfirmationGroup) + self.PhotoEditor_SaturationTool = getValue(dict, "PhotoEditor.SaturationTool") + self.ImagePicker_NoPhotos = getValue(dict, "ImagePicker.NoPhotos") + self.Call_StatusConnecting = getValue(dict, "Call.StatusConnecting") + self.Channel_BanUser_BlockFor = getValue(dict, "Channel.BanUser.BlockFor") + self.Preview_DeleteVideo = getValue(dict, "Preview.DeleteVideo") + self.Bot_Start = getValue(dict, "Bot.Start") + self._Channel_AdminLog_MessageChangedGroupAbout = getValue(dict, "Channel.AdminLog.MessageChangedGroupAbout") + self._Channel_AdminLog_MessageChangedGroupAbout_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedGroupAbout) + self.Notifications_TextTone = getValue(dict, "Notifications.TextTone") + self.DialogList_Draft = getValue(dict, "DialogList.Draft") + self._Watch_Time_ShortYesterdayAt = getValue(dict, "Watch.Time.ShortYesterdayAt") + self._Watch_Time_ShortYesterdayAt_r = extractArgumentRanges(self._Watch_Time_ShortYesterdayAt) + self.Contacts_InviteToTelegram = getValue(dict, "Contacts.InviteToTelegram") + self._PINNED_DOC = getValue(dict, "PINNED_DOC") + self._PINNED_DOC_r = extractArgumentRanges(self._PINNED_DOC) + self._ConversationProfile_UserLeftChatError = getValue(dict, "ConversationProfile.UserLeftChatError") + self._ConversationProfile_UserLeftChatError_r = extractArgumentRanges(self._ConversationProfile_UserLeftChatError) + self.ChatSettings_PrivateChats = getValue(dict, "ChatSettings.PrivateChats") + self.Settings_CallSettings = getValue(dict, "Settings.CallSettings") + self.Channel_EditAdmin_PermissionDeleteMessages = getValue(dict, "Channel.EditAdmin.PermissionDeleteMessages") + self.Conversation_CloudStorageInfo_Title = getValue(dict, "Conversation.CloudStorageInfo.Title") + self.Channel_BanUser_PermissionSendStickersAndGifs = getValue(dict, "Channel.BanUser.PermissionSendStickersAndGifs") + self.Channel_AdminLog_Status = getValue(dict, "Channel.AdminLog.Status") + self.Notification_RenamedChannel = getValue(dict, "Notification.RenamedChannel") + self.BlockedUsers_BlockUser = getValue(dict, "BlockedUsers.BlockUser") + self.ChatSettings_TextSize = getValue(dict, "ChatSettings.TextSize") + self.MediaPicker_AccessDeniedError = getValue(dict, "MediaPicker.AccessDeniedError") + self.ChannelInfo_DeleteGroup = getValue(dict, "ChannelInfo.DeleteGroup") + self._BlockedUsers_BlockFormat = getValue(dict, "BlockedUsers.BlockFormat") + self._BlockedUsers_BlockFormat_r = extractArgumentRanges(self._BlockedUsers_BlockFormat) + self.PhoneNumberHelp_Alert = getValue(dict, "PhoneNumberHelp.Alert") + self._PINNED_TEXT = getValue(dict, "PINNED_TEXT") + self._PINNED_TEXT_r = extractArgumentRanges(self._PINNED_TEXT) + self.Watch_ChannelInfo_Title = getValue(dict, "Watch.ChannelInfo.Title") + self.WebSearch_RecentSectionClear = getValue(dict, "WebSearch.RecentSectionClear") + self.Channel_AdminLogFilter_AdminsAll = getValue(dict, "Channel.AdminLogFilter.AdminsAll") + self.StickerPack_AddStickers = getValue(dict, "StickerPack.AddStickers") + self.Channel_Setup_TypePrivate = getValue(dict, "Channel.Setup.TypePrivate") + self.PhotoEditor_TintTool = getValue(dict, "PhotoEditor.TintTool") + self.Watch_Suggestion_CantTalk = getValue(dict, "Watch.Suggestion.CantTalk") + self.PhotoEditor_QualityHigh = getValue(dict, "PhotoEditor.QualityHigh") + self._CHAT_MESSAGE_STICKER = getValue(dict, "CHAT_MESSAGE_STICKER") + self._CHAT_MESSAGE_STICKER_r = extractArgumentRanges(self._CHAT_MESSAGE_STICKER) + self.Map_ChooseAPlace = getValue(dict, "Map.ChooseAPlace") + self.Tour_Title7 = getValue(dict, "Tour.Title7") + self.Watch_Bot_Restart = getValue(dict, "Watch.Bot.Restart") + self.StickerPack_ShareStickers = getValue(dict, "StickerPack.ShareStickers") + self.ChannelMembers_AllMembersMayInvite = getValue(dict, "ChannelMembers.AllMembersMayInvite") + self.Channel_About_Help = getValue(dict, "Channel.About.Help") + self.Web_OpenExternal = getValue(dict, "Web.OpenExternal") + self.UserInfo_AddContact = getValue(dict, "UserInfo.AddContact") + self.Call_EncryptionKey_Title = getValue(dict, "Call.EncryptionKey.Title") + self.PhotoEditor_BlurToolLinear = getValue(dict, "PhotoEditor.BlurToolLinear") + self.AuthSessions_EmptyText = getValue(dict, "AuthSessions.EmptyText") + self.Notification_MessageLifetime1m = getValue(dict, "Notification.MessageLifetime1m") + self._Call_StatusBar = getValue(dict, "Call.StatusBar") + self._Call_StatusBar_r = extractArgumentRanges(self._Call_StatusBar) + self.Month_ShortJuly = getValue(dict, "Month.ShortJuly") + self.Watch_MessageView_ViewOnPhone = getValue(dict, "Watch.MessageView.ViewOnPhone") + self.CheckoutInfo_ShippingInfoAddress1Placeholder = getValue(dict, "CheckoutInfo.ShippingInfoAddress1Placeholder") + self.CallSettings_Never = getValue(dict, "CallSettings.Never") + self.DialogList_SelectContacts = getValue(dict, "DialogList.SelectContacts") + self.Conversation_DownloadProgressMegabytes = getValue(dict, "Conversation.DownloadProgressMegabytes") + self.TwoStepAuth_EmailSent = getValue(dict, "TwoStepAuth.EmailSent") + self._Notification_PinnedAnimationMessage = getValue(dict, "Notification.PinnedAnimationMessage") + self._Notification_PinnedAnimationMessage_r = extractArgumentRanges(self._Notification_PinnedAnimationMessage) + self.TwoStepAuth_RecoveryTitle = getValue(dict, "TwoStepAuth.RecoveryTitle") + self.WatchRemote_AlertOpen = getValue(dict, "WatchRemote.AlertOpen") + self.ExplicitContent_AlertChannel = getValue(dict, "ExplicitContent.AlertChannel") + self.TwoStepAuth_ConfirmationText = getValue(dict, "TwoStepAuth.ConfirmationText") + self.Widget_AuthRequired = getValue(dict, "Widget.AuthRequired") + self._ForwardedAuthors2 = getValue(dict, "ForwardedAuthors2") + self._ForwardedAuthors2_r = extractArgumentRanges(self._ForwardedAuthors2) + self.ChannelInfo_DeleteGroupConfirmation = getValue(dict, "ChannelInfo.DeleteGroupConfirmation") + self.Login_SmsRequestState3 = getValue(dict, "Login.SmsRequestState3") + self.Notifications_AlertTones = getValue(dict, "Notifications.AlertTones") + self.Calls_TabTitle = getValue(dict, "Calls.TabTitle") + self.Login_InfoAvatarPhoto = getValue(dict, "Login.InfoAvatarPhoto") + self.Contacts_MemberSearchSectionTitleChannel = getValue(dict, "Contacts.MemberSearchSectionTitleChannel") + self.PhotoEditor_CurvesTool = getValue(dict, "PhotoEditor.CurvesTool") + self.Preview_LoadingVideo = getValue(dict, "Preview.LoadingVideo") + self.State_updating = getValue(dict, "State.updating") + self.TwoStepAuth_ResetAccount = getValue(dict, "TwoStepAuth.ResetAccount") + self.Checkout_ShippingOption_Title = getValue(dict, "Checkout.ShippingOption.Title") + self.Weekday_Tuesday = getValue(dict, "Weekday.Tuesday") + self.Preview_Tooltip = getValue(dict, "Preview.Tooltip") + self.Conversation_EncryptionProcessing = getValue(dict, "Conversation.EncryptionProcessing") + self._CHAT_ADD_MEMBER = getValue(dict, "CHAT_ADD_MEMBER") + self._CHAT_ADD_MEMBER_r = extractArgumentRanges(self._CHAT_ADD_MEMBER) + self.Weekday_ShortSunday = getValue(dict, "Weekday.ShortSunday") + self.Month_ShortJune = getValue(dict, "Month.ShortJune") + self.Month_GenApril = getValue(dict, "Month.GenApril") + self.StickerPacksSettings_ShowStickersButton = getValue(dict, "StickerPacksSettings.ShowStickersButton") + self.MediaPicker_MomentsDateRangeSameMonthFormat = getValue(dict, "MediaPicker.MomentsDateRangeSameMonthFormat") + self.CheckoutInfo_ShippingInfoTitle = getValue(dict, "CheckoutInfo.ShippingInfoTitle") + self.StickerPacksSettings_ShowStickersButtonHelp = getValue(dict, "StickerPacksSettings.ShowStickersButtonHelp") + self._Compatibility_SecretMediaVersionTooLow = getValue(dict, "Compatibility.SecretMediaVersionTooLow") + self._Compatibility_SecretMediaVersionTooLow_r = extractArgumentRanges(self._Compatibility_SecretMediaVersionTooLow) + self.CallSettings_RecentCalls = getValue(dict, "CallSettings.RecentCalls") + self.Conversation_Megabytes = getValue(dict, "Conversation.Megabytes") + self.TwoStepAuth_FloodError = getValue(dict, "TwoStepAuth.FloodError") + self.Paint_Stickers = getValue(dict, "Paint.Stickers") + self.Login_InvalidCountryCode = getValue(dict, "Login.InvalidCountryCode") + self.Privacy_Calls_AlwaysAllow_Title = getValue(dict, "Privacy.Calls.AlwaysAllow.Title") + self.Username_InvalidTooShort = getValue(dict, "Username.InvalidTooShort") + self.Weekday_ShortFriday = getValue(dict, "Weekday.ShortFriday") + self.Conversation_ClearAll = getValue(dict, "Conversation.ClearAll") + self.MediaPicker_Moments = getValue(dict, "MediaPicker.Moments") + self.Call_PhoneCallInProgressMessage = getValue(dict, "Call.PhoneCallInProgressMessage") + self.SharedMedia_EmptyTitle = getValue(dict, "SharedMedia.EmptyTitle") + self.Checkout_Name = getValue(dict, "Checkout.Name") + self.Preview_GroupPhotoTitle = getValue(dict, "Preview.GroupPhotoTitle") + self._AUTH_REGION = getValue(dict, "AUTH_REGION") + self._AUTH_REGION_r = extractArgumentRanges(self._AUTH_REGION) + self.Settings_NotificationsAndSounds = getValue(dict, "Settings.NotificationsAndSounds") + self._GroupInfo_InvitationLinkAcceptChannel = getValue(dict, "GroupInfo.InvitationLinkAcceptChannel") + self._GroupInfo_InvitationLinkAcceptChannel_r = extractArgumentRanges(self._GroupInfo_InvitationLinkAcceptChannel) + self.Conversation_EncryptionCanceled = getValue(dict, "Conversation.EncryptionCanceled") + self.AccessDenied_SaveMedia = getValue(dict, "AccessDenied.SaveMedia") + self.Channel_Username_InvalidTooManyUsernames = getValue(dict, "Channel.Username.InvalidTooManyUsernames") + self.Compose_GroupTokenListPlaceholder = getValue(dict, "Compose.GroupTokenListPlaceholder") + self.Profile_ImageUploadError = getValue(dict, "Profile.ImageUploadError") + self.Conversation_MessageDeliveryFailed = getValue(dict, "Conversation.MessageDeliveryFailed") + self.Privacy_PaymentsClear_PaymentInfo = getValue(dict, "Privacy.PaymentsClear.PaymentInfo") + self.Notification_Mute1hMin = getValue(dict, "Notification.Mute1hMin") + self.Notifications_GroupNotifications = getValue(dict, "Notifications.GroupNotifications") + self.CheckoutInfo_SaveInfoHelp = getValue(dict, "CheckoutInfo.SaveInfoHelp") + self.StickerPacksSettings_ArchivedMasks_Info = getValue(dict, "StickerPacksSettings.ArchivedMasks.Info") + self.ChannelMembers_WhoCanAddMembers_AllMembers = getValue(dict, "ChannelMembers.WhoCanAddMembers.AllMembers") + self.Channel_Edit_PrivatePublicLinkAlert = getValue(dict, "Channel.Edit.PrivatePublicLinkAlert") + self.Watch_Conversation_UserInfo = getValue(dict, "Watch.Conversation.UserInfo") + self.Application_Name = getValue(dict, "Application.Name") + self.Conversation_AddToReadingList = getValue(dict, "Conversation.AddToReadingList") + self.Conversation_FileDropbox = getValue(dict, "Conversation.FileDropbox") + self.Login_PhonePlaceholder = getValue(dict, "Login.PhonePlaceholder") + self.ExplicitContent_AlertUser = getValue(dict, "ExplicitContent.AlertUser") + self.Profile_MessageLifetime1d = getValue(dict, "Profile.MessageLifetime1d") + self.Calls_CallTabDescription = getValue(dict, "Calls.CallTabDescription") + self.CheckoutInfo_ShippingInfoCityPlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoCityPlaceholder") + self.Resolve_ErrorNotFound = getValue(dict, "Resolve.ErrorNotFound") + self.PhotoEditor_FadeTool = getValue(dict, "PhotoEditor.FadeTool") + self.Channel_TitleShowDiscussion = getValue(dict, "Channel.TitleShowDiscussion") + self.Channel_Setup_TypePublicHelp = getValue(dict, "Channel.Setup.TypePublicHelp") + self.GroupInfo_InviteLink_RevokeAlert_Success = getValue(dict, "GroupInfo.InviteLink.RevokeAlert.Success") + self.Channel_Setup_PublicNoLink = getValue(dict, "Channel.Setup.PublicNoLink") + self.Conversation_Info = getValue(dict, "Conversation.Info") + self.ChannelInfo_InvitationLinkDoesNotExist = getValue(dict, "ChannelInfo.InvitationLinkDoesNotExist") + self._Time_TodayAt = getValue(dict, "Time.TodayAt") + self._Time_TodayAt_r = extractArgumentRanges(self._Time_TodayAt) + self.Conversation_Processing = getValue(dict, "Conversation.Processing") + self._InstantPage_AuthorAndDateTitle = getValue(dict, "InstantPage.AuthorAndDateTitle") + self._InstantPage_AuthorAndDateTitle_r = extractArgumentRanges(self._InstantPage_AuthorAndDateTitle) + self._Watch_LastSeen_AtDate = getValue(dict, "Watch.LastSeen.AtDate") + self._Watch_LastSeen_AtDate_r = extractArgumentRanges(self._Watch_LastSeen_AtDate) + self.Conversation_Location = getValue(dict, "Conversation.Location") + self.DialogList_PasscodeLockHelp = getValue(dict, "DialogList.PasscodeLockHelp") + self.Channel_Management_Title = getValue(dict, "Channel.Management.Title") + self.Notifications_InAppNotificationsPreview = getValue(dict, "Notifications.InAppNotificationsPreview") + self.PrivacySettings_FloodControlError = getValue(dict, "PrivacySettings.FloodControlError") + self.EnterPasscode_EnterTitle = getValue(dict, "EnterPasscode.EnterTitle") + self.ReportPeer_ReasonOther_Title = getValue(dict, "ReportPeer.ReasonOther.Title") + self.Month_GenJanuary = getValue(dict, "Month.GenJanuary") + self.Conversation_ForwardChats = getValue(dict, "Conversation.ForwardChats") + self.SharedMedia_TitlePhoto = getValue(dict, "SharedMedia.TitlePhoto") + self.Channel_UpdatePhotoItem = getValue(dict, "Channel.UpdatePhotoItem") + self.GroupInfo_InvitationLinkAlreadyAccepted = getValue(dict, "GroupInfo.InvitationLinkAlreadyAccepted") + self.UserInfo_StartSecretChat = getValue(dict, "UserInfo.StartSecretChat") + self.Watch_State_Connecting = getValue(dict, "Watch.State.Connecting") + self.PrivacySettings_LastSeenNobody = getValue(dict, "PrivacySettings.LastSeenNobody") + self._FileSize_MB = getValue(dict, "FileSize.MB") + self._FileSize_MB_r = extractArgumentRanges(self._FileSize_MB) + self.ChatSearch_SearchPlaceholder = getValue(dict, "ChatSearch.SearchPlaceholder") + self.TwoStepAuth_ConfirmationAbort = getValue(dict, "TwoStepAuth.ConfirmationAbort") + self.GroupInfo_KickedStatus = getValue(dict, "GroupInfo.KickedStatus") + self.TwoStepAuth_SetupPasswordConfirmFailed = getValue(dict, "TwoStepAuth.SetupPasswordConfirmFailed") + self._LastSeen_YesterdayAt = getValue(dict, "LastSeen.YesterdayAt") + self._LastSeen_YesterdayAt_r = extractArgumentRanges(self._LastSeen_YesterdayAt) + self.AppleWatch_ReplyPresetsHelp = getValue(dict, "AppleWatch.ReplyPresetsHelp") + self.Localization_LanguageName = getValue(dict, "Localization.LanguageName") + self.Map_OpenIn = getValue(dict, "Map.OpenIn") + self.Message_File = getValue(dict, "Message.File") + self._Channel_AdminLog_MessageChangedGroupUsername = getValue(dict, "Channel.AdminLog.MessageChangedGroupUsername") + self._Channel_AdminLog_MessageChangedGroupUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedGroupUsername) + self._CHAT_MESSAGE_GAME = getValue(dict, "CHAT_MESSAGE_GAME") + self._CHAT_MESSAGE_GAME_r = extractArgumentRanges(self._CHAT_MESSAGE_GAME) + self.Month_ShortMay = getValue(dict, "Month.ShortMay") + self._WelcomeScreen_Greeting = getValue(dict, "WelcomeScreen.Greeting") + self._WelcomeScreen_Greeting_r = extractArgumentRanges(self._WelcomeScreen_Greeting) + self.Tour_Text3 = getValue(dict, "Tour.Text3") + self.Contacts_GlobalSearch = getValue(dict, "Contacts.GlobalSearch") + self.Watch_Suggestion_CallSoon = getValue(dict, "Watch.Suggestion.CallSoon") + self.DialogList_LanguageTooltip = getValue(dict, "DialogList.LanguageTooltip") + self.Map_LoadError = getValue(dict, "Map.LoadError") + self.WelcomeScreen_Logout = getValue(dict, "WelcomeScreen.Logout") + self._Service_ApplyLocalizationWithWarnings = getValue(dict, "Service.ApplyLocalizationWithWarnings") + self._Service_ApplyLocalizationWithWarnings_r = extractArgumentRanges(self._Service_ApplyLocalizationWithWarnings) + self.AccessDenied_VoiceMicrophone = getValue(dict, "AccessDenied.VoiceMicrophone") + self._CHANNEL_MESSAGE_STICKER = getValue(dict, "CHANNEL_MESSAGE_STICKER") + self._CHANNEL_MESSAGE_STICKER_r = extractArgumentRanges(self._CHANNEL_MESSAGE_STICKER) + self.PrivacySettings_Title = getValue(dict, "PrivacySettings.Title") + self.PasscodeSettings_TurnPasscodeOff = getValue(dict, "PasscodeSettings.TurnPasscodeOff") + self.MediaPicker_AddCaption = getValue(dict, "MediaPicker.AddCaption") + self.Channel_AdminLog_BanReadMessages = getValue(dict, "Channel.AdminLog.BanReadMessages") + self.SharedMedia_Outgoing = getValue(dict, "SharedMedia.Outgoing") + self.Channel_About_Error = getValue(dict, "Channel.About.Error") + self.Channel_Status = getValue(dict, "Channel.Status") + self.Map_ChooseLocationTitle = getValue(dict, "Map.ChooseLocationTitle") + self.Map_OpenInYandexNavigator = getValue(dict, "Map.OpenInYandexNavigator") + self.SearchImages_SkipImage = getValue(dict, "SearchImages.SkipImage") + self.State_WaitingForNetwork = getValue(dict, "State.WaitingForNetwork") + self.TwoStepAuth_EmailHelp = getValue(dict, "TwoStepAuth.EmailHelp") + self.PhotoEditor_SharpenTool = getValue(dict, "PhotoEditor.SharpenTool") + self.Common_of = getValue(dict, "Common.of") + self.AuthSessions_Title = getValue(dict, "AuthSessions.Title") + self.PrivacyLastSeenSettings_AlwaysShareWith = getValue(dict, "PrivacyLastSeenSettings.AlwaysShareWith") + self.EnterPasscode_EnterPasscode = getValue(dict, "EnterPasscode.EnterPasscode") + self.Notifications_Reset = getValue(dict, "Notifications.Reset") + self.GroupInfo_InvitationLinkGroupFull = getValue(dict, "GroupInfo.InvitationLinkGroupFull") + self._Channel_AdminLog_MessageChangedChannelUsername = getValue(dict, "Channel.AdminLog.MessageChangedChannelUsername") + self._Channel_AdminLog_MessageChangedChannelUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageChangedChannelUsername) + self.GoogleDrive_LogoutLogout = getValue(dict, "GoogleDrive.LogoutLogout") + self._CHAT_MESSAGE_DOC = getValue(dict, "CHAT_MESSAGE_DOC") + self._CHAT_MESSAGE_DOC_r = extractArgumentRanges(self._CHAT_MESSAGE_DOC) + self.Watch_AppName = getValue(dict, "Watch.AppName") + self._Channel_NotificationSelfAdded = getValue(dict, "Channel.NotificationSelfAdded") + self._Channel_NotificationSelfAdded_r = extractArgumentRanges(self._Channel_NotificationSelfAdded) + self.ConvertToSupergroup_HelpTitle = getValue(dict, "ConvertToSupergroup.HelpTitle") + self.Conversation_TapAndHoldToRecord = getValue(dict, "Conversation.TapAndHoldToRecord") + self.Channel_ShareNoLink = getValue(dict, "Channel.ShareNoLink") + self._MESSAGE_GIF = getValue(dict, "MESSAGE_GIF") + self._MESSAGE_GIF_r = extractArgumentRanges(self._MESSAGE_GIF) + self._DialogList_EncryptedChatStartedOutgoing = getValue(dict, "DialogList.EncryptedChatStartedOutgoing") + self._DialogList_EncryptedChatStartedOutgoing_r = extractArgumentRanges(self._DialogList_EncryptedChatStartedOutgoing) + self.Checkout_PayWithTouchId = getValue(dict, "Checkout.PayWithTouchId") + self._Notification_InvitedMany = getValue(dict, "Notification.InvitedMany") + self._Notification_InvitedMany_r = extractArgumentRanges(self._Notification_InvitedMany) + self._CHAT_ADD_YOU = getValue(dict, "CHAT_ADD_YOU") + self._CHAT_ADD_YOU_r = extractArgumentRanges(self._CHAT_ADD_YOU) + self.CheckoutInfo_ShippingInfoCity = getValue(dict, "CheckoutInfo.ShippingInfoCity") + self.Conversation_DiscardVoiceMessageTitle = getValue(dict, "Conversation.DiscardVoiceMessageTitle") + self.Conversation_ClousStorageInfo_Description3 = getValue(dict, "Conversation.ClousStorageInfo.Description3") + self.Profile_MessageLifetime = getValue(dict, "Profile.MessageLifetime") + self.GoogleDrive_LogoutTitle = getValue(dict, "GoogleDrive.LogoutTitle") + self.Conversation_PinMessageAlertGroup = getValue(dict, "Conversation.PinMessageAlertGroup") + self.Settings_FAQ_Intro = getValue(dict, "Settings.FAQ_Intro") + self.PrivacySettings_AuthSessions = getValue(dict, "PrivacySettings.AuthSessions") + self.Tour_Title5 = getValue(dict, "Tour.Title5") + self.ChatAdmins_AllMembersAreAdmins = getValue(dict, "ChatAdmins.AllMembersAreAdmins") + self.Group_Management_AddModeratorHelp = getValue(dict, "Group.Management.AddModeratorHelp") + self.Channel_Username_CheckingUsername = getValue(dict, "Channel.Username.CheckingUsername") + self.Activity_UploadingAudio = getValue(dict, "Activity.UploadingAudio") + self._DialogList_SingleRecordingVideoMessageSuffix = getValue(dict, "DialogList.SingleRecordingVideoMessageSuffix") + self._DialogList_SingleRecordingVideoMessageSuffix_r = extractArgumentRanges(self._DialogList_SingleRecordingVideoMessageSuffix) + self._Contacts_AccessDeniedHelpPortrait = getValue(dict, "Contacts.AccessDeniedHelpPortrait") + self._Contacts_AccessDeniedHelpPortrait_r = extractArgumentRanges(self._Contacts_AccessDeniedHelpPortrait) + self.Channel_Info_BlackList = getValue(dict, "Channel.Info.BlackList") + self._Checkout_LiabilityAlert = getValue(dict, "Checkout.LiabilityAlert") + self._Checkout_LiabilityAlert_r = extractArgumentRanges(self._Checkout_LiabilityAlert) + self.Profile_BotInfo = getValue(dict, "Profile.BotInfo") + self.StickerPack_RemoveStickers = getValue(dict, "StickerPack.RemoveStickers") + self.Compose_NewChannel_Members = getValue(dict, "Compose.NewChannel.Members") + self.Notification_Reply = getValue(dict, "Notification.Reply") + self.Watch_Stickers_Recents = getValue(dict, "Watch.Stickers.Recents") + self.GroupInfo_SetGroupPhotoStop = getValue(dict, "GroupInfo.SetGroupPhotoStop") + self.Conversation_PinMessageAlertChannel = getValue(dict, "Conversation.PinMessageAlertChannel") + self.AttachmentMenu_File = getValue(dict, "AttachmentMenu.File") + self._MESSAGE_STICKER = getValue(dict, "MESSAGE_STICKER") + self._MESSAGE_STICKER_r = extractArgumentRanges(self._MESSAGE_STICKER) + self.Profile_MessageLifetime5s = getValue(dict, "Profile.MessageLifetime5s") + self._PINNED_PHOTO = getValue(dict, "PINNED_PHOTO") + self._PINNED_PHOTO_r = extractArgumentRanges(self._PINNED_PHOTO) + self.Channel_EditAdmin_PermissionChangeInviteLink = getValue(dict, "Channel.EditAdmin.PermissionChangeInviteLink") + self.Channel_AdminLog_CanAddAdmins = getValue(dict, "Channel.AdminLog.CanAddAdmins") + self.WelcomeScreen_Title = getValue(dict, "WelcomeScreen.Title") + self.TwoStepAuth_SetupHint = getValue(dict, "TwoStepAuth.SetupHint") + self.Conversation_StatusLeftGroup = getValue(dict, "Conversation.StatusLeftGroup") + self.Conversation_ShareBotLocationConfirmation = getValue(dict, "Conversation.ShareBotLocationConfirmation") + self.Conversation_DeleteMessagesForMe = getValue(dict, "Conversation.DeleteMessagesForMe") + self.Message_PinnedAnimationMessage = getValue(dict, "Message.PinnedAnimationMessage") + self.Checkout_ErrorPrecheckoutFailed = getValue(dict, "Checkout.ErrorPrecheckoutFailed") + self.Camera_PhotoMode = getValue(dict, "Camera.PhotoMode") + self.Channel_About_Placeholder = getValue(dict, "Channel.About.Placeholder") + self.Channel_About_Title = getValue(dict, "Channel.About.Title") + self._MESSAGE_PHOTO = getValue(dict, "MESSAGE_PHOTO") + self._MESSAGE_PHOTO_r = extractArgumentRanges(self._MESSAGE_PHOTO) + self.Calls_RatingTitle = getValue(dict, "Calls.RatingTitle") + self.SharedMedia_EmptyText = getValue(dict, "SharedMedia.EmptyText") + self.Channel_Username_CreateCommentsHelp = getValue(dict, "Channel.Username.CreateCommentsHelp") + self.Login_PadPhoneHelp = getValue(dict, "Login.PadPhoneHelp") + self.StickerPacksSettings_ArchivedPacks = getValue(dict, "StickerPacksSettings.ArchivedPacks") + self.Channel_ErrorAccessDenied = getValue(dict, "Channel.ErrorAccessDenied") + self.Generic_ErrorMoreInfo = getValue(dict, "Generic.ErrorMoreInfo") + self.Notification_GroupDeactivated = getValue(dict, "Notification.GroupDeactivated") + self.Channel_AdminLog_TitleAllEvents = getValue(dict, "Channel.AdminLog.TitleAllEvents") + self.PrivacySettings_TouchIdTitle = getValue(dict, "PrivacySettings.TouchIdTitle") + self.ChannelMembers_WhoCanAddMembersAllHelp = getValue(dict, "ChannelMembers.WhoCanAddMembersAllHelp") + self.ChangePhoneNumberCode_CodePlaceholder = getValue(dict, "ChangePhoneNumberCode.CodePlaceholder") + self.Camera_SquareMode = getValue(dict, "Camera.SquareMode") + self._Conversation_EncryptedPlaceholderTitleOutgoing = getValue(dict, "Conversation.EncryptedPlaceholderTitleOutgoing") + self._Conversation_EncryptedPlaceholderTitleOutgoing_r = extractArgumentRanges(self._Conversation_EncryptedPlaceholderTitleOutgoing) + self.NetworkUsageSettings_CallDataSection = getValue(dict, "NetworkUsageSettings.CallDataSection") + self.Login_PadPhoneHelpTitle = getValue(dict, "Login.PadPhoneHelpTitle") + self.Profile_CreateNewContact = getValue(dict, "Profile.CreateNewContact") + self.AccessDenied_VideoMessageMicrophone = getValue(dict, "AccessDenied.VideoMessageMicrophone") + self.PhotoEditor_VignetteTool = getValue(dict, "PhotoEditor.VignetteTool") + self.LastSeen_WithinAWeek = getValue(dict, "LastSeen.WithinAWeek") + self.Widget_NoUsers = getValue(dict, "Widget.NoUsers") + self.Channel_Edit_EnableComments = getValue(dict, "Channel.Edit.EnableComments") + self.DialogList_NoMessagesText = getValue(dict, "DialogList.NoMessagesText") + self._CHANNEL_MESSAGE_AUDIO = getValue(dict, "CHANNEL_MESSAGE_AUDIO") + self._CHANNEL_MESSAGE_AUDIO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_AUDIO) + self.Calls_NewCall = getValue(dict, "Calls.NewCall") + self.SharedMedia_TitleFile = getValue(dict, "SharedMedia.TitleFile") + self.MaskStickerSettings_Info = getValue(dict, "MaskStickerSettings.Info") + self.Conversation_FilePhotoOrVideo = getValue(dict, "Conversation.FilePhotoOrVideo") + self._Watch_LastSeen_AtWeekday = getValue(dict, "Watch.LastSeen.AtWeekday") + self._Watch_LastSeen_AtWeekday_r = extractArgumentRanges(self._Watch_LastSeen_AtWeekday) + self.Channel_AdminLog_BanSendStickers = getValue(dict, "Channel.AdminLog.BanSendStickers") + self.Common_Next = getValue(dict, "Common.Next") + self.Watch_Notification_Joined = getValue(dict, "Watch.Notification.Joined") + self.ImagePicker_NoVideos = getValue(dict, "ImagePicker.NoVideos") + self.GroupInfo_DeleteAndExitConfirmation = getValue(dict, "GroupInfo.DeleteAndExitConfirmation") + self.ChatSettings_Cache = getValue(dict, "ChatSettings.Cache") + self.TwoStepAuth_EmailInvalid = getValue(dict, "TwoStepAuth.EmailInvalid") + self._CHAT_MESSAGE_VIDEO = getValue(dict, "CHAT_MESSAGE_VIDEO") + self._CHAT_MESSAGE_VIDEO_r = extractArgumentRanges(self._CHAT_MESSAGE_VIDEO) + self.Month_GenJune = getValue(dict, "Month.GenJune") + self._Login_EmailCodeSubject = getValue(dict, "Login.EmailCodeSubject") + self._Login_EmailCodeSubject_r = extractArgumentRanges(self._Login_EmailCodeSubject) + self._CHAT_TITLE_EDITED = getValue(dict, "CHAT_TITLE_EDITED") + self._CHAT_TITLE_EDITED_r = extractArgumentRanges(self._CHAT_TITLE_EDITED) + self.Watch_UnlockRequired = getValue(dict, "Watch.UnlockRequired") + self._NetworkUsageSettings_WifiUsageSince = getValue(dict, "NetworkUsageSettings.WifiUsageSince") + self._NetworkUsageSettings_WifiUsageSince_r = extractArgumentRanges(self._NetworkUsageSettings_WifiUsageSince) + self.Watch_LastSeen_Lately = getValue(dict, "Watch.LastSeen.Lately") + self.Watch_Compose_CurrentLocation = getValue(dict, "Watch.Compose.CurrentLocation") + self._CHANNEL_MESSAGE_FWDS = getValue(dict, "CHANNEL_MESSAGE_FWDS") + self._CHANNEL_MESSAGE_FWDS_r = extractArgumentRanges(self._CHANNEL_MESSAGE_FWDS) + self.DialogList_RecentTitlePeople = getValue(dict, "DialogList.RecentTitlePeople") + self.Conversation_ViewLocation = getValue(dict, "Conversation.ViewLocation") + self.GroupInfo_Notifications = getValue(dict, "GroupInfo.Notifications") + self._MESSAGE_DOC = getValue(dict, "MESSAGE_DOC") + self._MESSAGE_DOC_r = extractArgumentRanges(self._MESSAGE_DOC) + self.Group_Username_CreatePrivateLinkHelp = getValue(dict, "Group.Username.CreatePrivateLinkHelp") + self.Notifications_GroupNotificationsSound = getValue(dict, "Notifications.GroupNotificationsSound") + self.AuthSessions_EmptyTitle = getValue(dict, "AuthSessions.EmptyTitle") + self.Privacy_GroupsAndChannels_AlwaysAllow_Title = getValue(dict, "Privacy.GroupsAndChannels.AlwaysAllow.Title") + self._MediaPicker_Nof = getValue(dict, "MediaPicker.Nof") + self._MediaPicker_Nof_r = extractArgumentRanges(self._MediaPicker_Nof) + self.Common_Create = getValue(dict, "Common.Create") + self.Message_InvoiceShipmentLabel = getValue(dict, "Message.InvoiceShipmentLabel") + self.Contacts_TopSection = getValue(dict, "Contacts.TopSection") + self.Your_cards_number_is_invalid = getValue(dict, "Your_cards_number_is_invalid") + self._MESSAGE_INVOICE = getValue(dict, "MESSAGE_INVOICE") + self._MESSAGE_INVOICE_r = extractArgumentRanges(self._MESSAGE_INVOICE) + self._Channel_AdminLog_MessageRemovedChannelUsername = getValue(dict, "Channel.AdminLog.MessageRemovedChannelUsername") + self._Channel_AdminLog_MessageRemovedChannelUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageRemovedChannelUsername) + self.Group_MessagePhotoRemoved = getValue(dict, "Group.MessagePhotoRemoved") + self.UserInfo_AddToExisting = getValue(dict, "UserInfo.AddToExisting") + self._LastSeen_AtDate = getValue(dict, "LastSeen.AtDate") + self._LastSeen_AtDate_r = extractArgumentRanges(self._LastSeen_AtDate) + self.Conversation_MessageDialogRetry = getValue(dict, "Conversation.MessageDialogRetry") + self.Watch_ChatList_NoConversationsTitle = getValue(dict, "Watch.ChatList.NoConversationsTitle") + self.BlockedUsers_Title = getValue(dict, "BlockedUsers.Title") + self.MediaPicker_MomentsDateRangeYearFormat = getValue(dict, "MediaPicker.MomentsDateRangeYearFormat") + self.Cache_ClearNone = getValue(dict, "Cache.ClearNone") + self.Login_InvalidCodeError = getValue(dict, "Login.InvalidCodeError") + self.Contacts_contacts = getValue(dict, "Contacts.contacts") + self.Channel_BanList_BlockedTitle = getValue(dict, "Channel.BanList.BlockedTitle") + self.NetworkUsageSettings_Cellular = getValue(dict, "NetworkUsageSettings.Cellular") + self.Watch_Location_Access = getValue(dict, "Watch.Location.Access") + self._CONTACT_ACTIVATED = getValue(dict, "CONTACT_ACTIVATED") + self._CONTACT_ACTIVATED_r = extractArgumentRanges(self._CONTACT_ACTIVATED) + self.BlockedUsers_AlreadyBlocked = getValue(dict, "BlockedUsers.AlreadyBlocked") + self.PrivacySettings_DeleteAccountIfAwayFor = getValue(dict, "PrivacySettings.DeleteAccountIfAwayFor") + self.PrivacySettings_DeleteAccountTitle = getValue(dict, "PrivacySettings.DeleteAccountTitle") + self.PrivacyLastSeenSettings_CustomShareSettings_Delete = getValue(dict, "PrivacyLastSeenSettings.CustomShareSettings.Delete") + self._ENCRYPTED_MESSAGE = getValue(dict, "ENCRYPTED_MESSAGE") + self._ENCRYPTED_MESSAGE_r = extractArgumentRanges(self._ENCRYPTED_MESSAGE) + self.Watch_LastSeen_WithinAMonth = getValue(dict, "Watch.LastSeen.WithinAMonth") + self.PrivacyLastSeenSettings_CustomHelp = getValue(dict, "PrivacyLastSeenSettings.CustomHelp") + self.TwoStepAuth_EnterPasswordHelp = getValue(dict, "TwoStepAuth.EnterPasswordHelp") + self.Bot_Stop = getValue(dict, "Bot.Stop") + self.Privacy_GroupsAndChannels_AlwaysAllow_Placeholder = getValue(dict, "Privacy.GroupsAndChannels.AlwaysAllow.Placeholder") + self._AUTH_UNKNOWN = getValue(dict, "AUTH_UNKNOWN") + self._AUTH_UNKNOWN_r = extractArgumentRanges(self._AUTH_UNKNOWN) + self.UserInfo_BotSettings = getValue(dict, "UserInfo.BotSettings") + self.Your_cards_expiration_month_is_invalid = getValue(dict, "Your_cards_expiration_month_is_invalid") + self.PrivacyLastSeenSettings_EmpryUsersPlaceholder = getValue(dict, "PrivacyLastSeenSettings.EmpryUsersPlaceholder") + self._CHANNEL_MESSAGE_ROUND = getValue(dict, "CHANNEL_MESSAGE_ROUND") + self._CHANNEL_MESSAGE_ROUND_r = extractArgumentRanges(self._CHANNEL_MESSAGE_ROUND) + self.GoogleDrive_FolderLoadError = getValue(dict, "GoogleDrive.FolderLoadError") + self.Message_VideoMessage = getValue(dict, "Message.VideoMessage") + self.Conversation_ContextMenuStickerPackInfo = getValue(dict, "Conversation.ContextMenuStickerPackInfo") + self.Login_ResetAccountProtected_LimitExceeded = getValue(dict, "Login.ResetAccountProtected.LimitExceeded") + self.Watch_Suggestion_TextInABit = getValue(dict, "Watch.Suggestion.TextInABit") + self._CHAT_DELETE_MEMBER = getValue(dict, "CHAT_DELETE_MEMBER") + self._CHAT_DELETE_MEMBER_r = extractArgumentRanges(self._CHAT_DELETE_MEMBER) + self.Conversation_EncryptedForwardingAlert = getValue(dict, "Conversation.EncryptedForwardingAlert") + self.Conversation_DiscardVoiceMessageAction = getValue(dict, "Conversation.DiscardVoiceMessageAction") + self.PhotoEditor_CurvesBlue = getValue(dict, "PhotoEditor.CurvesBlue") + self.Message_PinnedVideoMessage = getValue(dict, "Message.PinnedVideoMessage") + self._Settings_OpenSystemPrivacySettings = getValue(dict, "Settings.OpenSystemPrivacySettings") + self._Settings_OpenSystemPrivacySettings_r = extractArgumentRanges(self._Settings_OpenSystemPrivacySettings) + self._Login_EmailPhoneSubject = getValue(dict, "Login.EmailPhoneSubject") + self._Login_EmailPhoneSubject_r = extractArgumentRanges(self._Login_EmailPhoneSubject) + self.Group_EditAdmin_PermissionChangeInfo = getValue(dict, "Group.EditAdmin.PermissionChangeInfo") + self.TwoStepAuth_Email = getValue(dict, "TwoStepAuth.Email") + self.Map_SendMyCurrentLocation = getValue(dict, "Map.SendMyCurrentLocation") + self._MESSAGE_ROUND = getValue(dict, "MESSAGE_ROUND") + self._MESSAGE_ROUND_r = extractArgumentRanges(self._MESSAGE_ROUND) + self.Map_Unknown = getValue(dict, "Map.Unknown") + self.Wallpaper_Set = getValue(dict, "Wallpaper.Set") + self.SharedMedia_CategoryLinks = getValue(dict, "SharedMedia.CategoryLinks") + self.AccessDenied_Title = getValue(dict, "AccessDenied.Title") + self.Conversation_ClearAllConfirmation = getValue(dict, "Conversation.ClearAllConfirmation") + self.TwoStepAuth_EmailSkipAlert = getValue(dict, "TwoStepAuth.EmailSkipAlert") + self.ChatSettings_Stickers = getValue(dict, "ChatSettings.Stickers") + self.Camera_FlashOff = getValue(dict, "Camera.FlashOff") + self.TwoStepAuth_Title = getValue(dict, "TwoStepAuth.Title") + self.TwoStepAuth_SetupPasswordEnterPasswordChange = getValue(dict, "TwoStepAuth.SetupPasswordEnterPasswordChange") + self.WebSearch_Images = getValue(dict, "WebSearch.Images") + self.Checkout_ErrorProviderAccountTimeout = getValue(dict, "Checkout.ErrorProviderAccountTimeout") + self.Conversation_typing = getValue(dict, "Conversation.typing") + self.Common_Back = getValue(dict, "Common.Back") + self.Common_Search = getValue(dict, "Common.Search") + self._CancelResetAccount_Success = getValue(dict, "CancelResetAccount.Success") + self._CancelResetAccount_Success_r = extractArgumentRanges(self._CancelResetAccount_Success) + self.Common_No = getValue(dict, "Common.No") + self.Login_EmailNotConfiguredError = getValue(dict, "Login.EmailNotConfiguredError") + self.Watch_Suggestion_OK = getValue(dict, "Watch.Suggestion.OK") + self.Profile_AddToExisting = getValue(dict, "Profile.AddToExisting") + self._PINNED_NOTEXT = getValue(dict, "PINNED_NOTEXT") + self._PINNED_NOTEXT_r = extractArgumentRanges(self._PINNED_NOTEXT) + self._Login_EmailCodeBody = getValue(dict, "Login.EmailCodeBody") + self._Login_EmailCodeBody_r = extractArgumentRanges(self._Login_EmailCodeBody) + self.Profile_About = getValue(dict, "Profile.About") + self._EncryptionKey_Description = getValue(dict, "EncryptionKey.Description") + self._EncryptionKey_Description_r = extractArgumentRanges(self._EncryptionKey_Description) + self.Conversation_UnreadMessages = getValue(dict, "Conversation.UnreadMessages") + self.Tour_Title3 = getValue(dict, "Tour.Title3") + self.PrivacyLastSeenSettings_GroupsAndChannelsHelp = getValue(dict, "PrivacyLastSeenSettings.GroupsAndChannelsHelp") + self.Watch_Contacts_NoResults = getValue(dict, "Watch.Contacts.NoResults") + self.Watch_UserInfo_MuteTitle = getValue(dict, "Watch.UserInfo.MuteTitle") + self.MediaPicker_Choose = getValue(dict, "MediaPicker.Choose") + self.Conversation_DownloadMegabytes = getValue(dict, "Conversation.DownloadMegabytes") + self._Privacy_GroupsAndChannels_InviteToGroupError = getValue(dict, "Privacy.GroupsAndChannels.InviteToGroupError") + self._Privacy_GroupsAndChannels_InviteToGroupError_r = extractArgumentRanges(self._Privacy_GroupsAndChannels_InviteToGroupError) + self._Message_PinnedTextMessage = getValue(dict, "Message.PinnedTextMessage") + self._Message_PinnedTextMessage_r = extractArgumentRanges(self._Message_PinnedTextMessage) + self._Watch_Time_ShortWeekdayAt = getValue(dict, "Watch.Time.ShortWeekdayAt") + self._Watch_Time_ShortWeekdayAt_r = extractArgumentRanges(self._Watch_Time_ShortWeekdayAt) + self.DialogList_Typing = getValue(dict, "DialogList.Typing") + self.Notification_CallBack = getValue(dict, "Notification.CallBack") + self.Map_LocatingError = getValue(dict, "Map.LocatingError") + self.MediaPicker_Send = getValue(dict, "MediaPicker.Send") + self.ChannelIntro_Title = getValue(dict, "ChannelIntro.Title") + self.SearchImages_ErrorDownloadingImage = getValue(dict, "SearchImages.ErrorDownloadingImage") + self._PINNED_GIF = getValue(dict, "PINNED_GIF") + self._PINNED_GIF_r = extractArgumentRanges(self._PINNED_GIF) + self.Profile_PhonebookAccessDisabled = getValue(dict, "Profile.PhonebookAccessDisabled") + self.LoginPassword_PasswordHelp = getValue(dict, "LoginPassword.PasswordHelp") + self.BlockedUsers_Unblock = getValue(dict, "BlockedUsers.Unblock") + self.Conversation_ViewFile = getValue(dict, "Conversation.ViewFile") + self.Notifications_GroupNotificationsAlert = getValue(dict, "Notifications.GroupNotificationsAlert") + self.Paint_Masks = getValue(dict, "Paint.Masks") + self.StickerPack_ErrorNotFound = getValue(dict, "StickerPack.ErrorNotFound") + self._PINNED_CONTACT = getValue(dict, "PINNED_CONTACT") + self._PINNED_CONTACT_r = extractArgumentRanges(self._PINNED_CONTACT) + self._Conversation_ForwardToGroupFormat = getValue(dict, "Conversation.ForwardToGroupFormat") + self._Conversation_ForwardToGroupFormat_r = extractArgumentRanges(self._Conversation_ForwardToGroupFormat) + self._FileSize_KB = getValue(dict, "FileSize.KB") + self._FileSize_KB_r = extractArgumentRanges(self._FileSize_KB) + self.Watch_GroupInfo_Title = getValue(dict, "Watch.GroupInfo.Title") + self.PhotoEditor_Set = getValue(dict, "PhotoEditor.Set") + self._Notification_Invited = getValue(dict, "Notification.Invited") + self._Notification_Invited_r = extractArgumentRanges(self._Notification_Invited) + self.Watch_AuthRequired = getValue(dict, "Watch.AuthRequired") + self.Conversation_EncryptedDescription1 = getValue(dict, "Conversation.EncryptedDescription1") + self.AppleWatch_ReplyPresets = getValue(dict, "AppleWatch.ReplyPresets") + self.Conversation_EncryptedDescription2 = getValue(dict, "Conversation.EncryptedDescription2") + self.NetworkUsageSettings_MediaVideoDataSection = getValue(dict, "NetworkUsageSettings.MediaVideoDataSection") + self.Paint_Edit = getValue(dict, "Paint.Edit") + self.Conversation_EncryptedDescription3 = getValue(dict, "Conversation.EncryptedDescription3") + self.Login_CodeFloodError = getValue(dict, "Login.CodeFloodError") + self._Call_EncryptionKey_Description = getValue(dict, "Call.EncryptionKey.Description") + self._Call_EncryptionKey_Description_r = extractArgumentRanges(self._Call_EncryptionKey_Description) + self.Conversation_EncryptedDescription4 = getValue(dict, "Conversation.EncryptedDescription4") + self.AppleWatch_Title = getValue(dict, "AppleWatch.Title") + self.Conversation_StatusTyping = getValue(dict, "Conversation.StatusTyping") + self.Contacts_AccessDeniedError = getValue(dict, "Contacts.AccessDeniedError") + self.GoogleDrive_LoadErrorTitle = getValue(dict, "GoogleDrive.LoadErrorTitle") + self.Share_Title = getValue(dict, "Share.Title") + self.Map_Send = getValue(dict, "Map.Send") + self.TwoStepAuth_ConfirmationTitle = getValue(dict, "TwoStepAuth.ConfirmationTitle") + self.Conversation_SupportPlaceholder = getValue(dict, "Conversation.SupportPlaceholder") + self.ChatSettings_Title = getValue(dict, "ChatSettings.Title") + self.AuthSessions_CurrentSession = getValue(dict, "AuthSessions.CurrentSession") + self.Watch_Microphone_Access = getValue(dict, "Watch.Microphone.Access") + self._Notification_RenamedChat = getValue(dict, "Notification.RenamedChat") + self._Notification_RenamedChat_r = extractArgumentRanges(self._Notification_RenamedChat) + self.Watch_Conversation_GroupInfo = getValue(dict, "Watch.Conversation.GroupInfo") + self.UserInfo_Title = getValue(dict, "UserInfo.Title") + self.Service_LocalizationDownloadError = getValue(dict, "Service.LocalizationDownloadError") + self.Login_InfoHelp = getValue(dict, "Login.InfoHelp") + self.ShareMenu_ShareTo = getValue(dict, "ShareMenu.ShareTo") + self.Message_PinnedGame = getValue(dict, "Message.PinnedGame") + self.Channel_AdminLog_CanSendMessages = getValue(dict, "Channel.AdminLog.CanSendMessages") + self.Notification_RenamedGroup = getValue(dict, "Notification.RenamedGroup") + self.Weekday_Thursday = getValue(dict, "Weekday.Thursday") + self._Call_PrivacyErrorMessage = getValue(dict, "Call.PrivacyErrorMessage") + self._Call_PrivacyErrorMessage_r = extractArgumentRanges(self._Call_PrivacyErrorMessage) + self.ChangePhoneNumberNumber_Title = getValue(dict, "ChangePhoneNumberNumber.Title") + self.TwoStepAuth_EnterPasswordInvalid = getValue(dict, "TwoStepAuth.EnterPasswordInvalid") + self.DialogList_SearchSectionMessages = getValue(dict, "DialogList.SearchSectionMessages") + self._Profile_ShareBotGroupFormat = getValue(dict, "Profile.ShareBotGroupFormat") + self._Profile_ShareBotGroupFormat_r = extractArgumentRanges(self._Profile_ShareBotGroupFormat) + self.Preview_DeleteGif = getValue(dict, "Preview.DeleteGif") + self.Weekday_Saturday = getValue(dict, "Weekday.Saturday") + self.UserInfo_DeleteContact = getValue(dict, "UserInfo.DeleteContact") + self.Notifications_ResetAllNotifications = getValue(dict, "Notifications.ResetAllNotifications") + self.Notification_MessageLifetimeRemovedOutgoing = getValue(dict, "Notification.MessageLifetimeRemovedOutgoing") + self.Map_More = getValue(dict, "Map.More") + self.Login_ContinueWithLocalization = getValue(dict, "Login.ContinueWithLocalization") + self.GroupInfo_AddParticipant = getValue(dict, "GroupInfo.AddParticipant") + self.Watch_Location_Current = getValue(dict, "Watch.Location.Current") + self.Map_MapTitle = getValue(dict, "Map.MapTitle") + self.Checkout_NewCard_SaveInfoHelp = getValue(dict, "Checkout.NewCard.SaveInfoHelp") + self.MediaPicker_CameraRoll = getValue(dict, "MediaPicker.CameraRoll") + self._TwoStepAuth_RecoverySent = getValue(dict, "TwoStepAuth.RecoverySent") + self._TwoStepAuth_RecoverySent_r = extractArgumentRanges(self._TwoStepAuth_RecoverySent) + self.Channel_AdminLog_CanPinMessages = getValue(dict, "Channel.AdminLog.CanPinMessages") + self.KeyCommand_NewMessage = getValue(dict, "KeyCommand.NewMessage") + self.Compose_NewBroadcastButton = getValue(dict, "Compose.NewBroadcastButton") + self.NetworkUsageSettings_TotalSection = getValue(dict, "NetworkUsageSettings.TotalSection") + self._PINNED_AUDIO = getValue(dict, "PINNED_AUDIO") + self._PINNED_AUDIO_r = extractArgumentRanges(self._PINNED_AUDIO) + self.Privacy_GroupsAndChannels = getValue(dict, "Privacy.GroupsAndChannels") + self.Conversation_DiscardVoiceMessageDescription = getValue(dict, "Conversation.DiscardVoiceMessageDescription") + self._Notification_ChangedGroupPhoto = getValue(dict, "Notification.ChangedGroupPhoto") + self._Notification_ChangedGroupPhoto_r = extractArgumentRanges(self._Notification_ChangedGroupPhoto) + self.TwoStepAuth_RemovePassword = getValue(dict, "TwoStepAuth.RemovePassword") + self.Privacy_GroupsAndChannels_CustomHelp = getValue(dict, "Privacy.GroupsAndChannels.CustomHelp") + self.Notification_GroupMigratedToChannel = getValue(dict, "Notification.GroupMigratedToChannel") + self.UserInfo_NotificationsDisable = getValue(dict, "UserInfo.NotificationsDisable") + self.Watch_UserInfo_Service = getValue(dict, "Watch.UserInfo.Service") + self.Privacy_Calls_CustomHelp = getValue(dict, "Privacy.Calls.CustomHelp") + self.ChangePhoneNumberCode_Code = getValue(dict, "ChangePhoneNumberCode.Code") + self.UserInfo_Invite = getValue(dict, "UserInfo.Invite") + self.CheckoutInfo_ErrorStateInvalid = getValue(dict, "CheckoutInfo.ErrorStateInvalid") + self.DialogList_ClearHistoryConfirmation = getValue(dict, "DialogList.ClearHistoryConfirmation") + self.CheckoutInfo_ErrorEmailInvalid = getValue(dict, "CheckoutInfo.ErrorEmailInvalid") + self.Month_GenNovember = getValue(dict, "Month.GenNovember") + self.PhotoEditor_TintIntensity = getValue(dict, "PhotoEditor.TintIntensity") + self.UserInfo_NotificationsEnable = getValue(dict, "UserInfo.NotificationsEnable") + self._Target_InviteToGroupConfirmation = getValue(dict, "Target.InviteToGroupConfirmation") + self._Target_InviteToGroupConfirmation_r = extractArgumentRanges(self._Target_InviteToGroupConfirmation) + self.Map_Map = getValue(dict, "Map.Map") + self.Map_OpenInMaps = getValue(dict, "Map.OpenInMaps") + self.Common_OK = getValue(dict, "Common.OK") + self.TwoStepAuth_SetupHintTitle = getValue(dict, "TwoStepAuth.SetupHintTitle") + self.Watch_Suggestion_Nope = getValue(dict, "Watch.Suggestion.Nope") + self.GroupInfo_LeftStatus = getValue(dict, "GroupInfo.LeftStatus") + self.Cache_ClearProgress = getValue(dict, "Cache.ClearProgress") + self.Login_InvalidPhoneError = getValue(dict, "Login.InvalidPhoneError") + self.Cache_ClearEmpty = getValue(dict, "Cache.ClearEmpty") + self.Map_Search = getValue(dict, "Map.Search") + self.ChannelMembers_GroupAdminsTitle = getValue(dict, "ChannelMembers.GroupAdminsTitle") + self._Channel_AdminLog_MessageRemovedGroupUsername = getValue(dict, "Channel.AdminLog.MessageRemovedGroupUsername") + self._Channel_AdminLog_MessageRemovedGroupUsername_r = extractArgumentRanges(self._Channel_AdminLog_MessageRemovedGroupUsername) + self.ChatSettings_AutomaticPhotoDownload = getValue(dict, "ChatSettings.AutomaticPhotoDownload") + self.Update_Update = getValue(dict, "Update.Update") + self.Group_ErrorAddTooMuchAdmins = getValue(dict, "Group.ErrorAddTooMuchAdmins") + self.Login_SelectCountry_Title = getValue(dict, "Login.SelectCountry.Title") + self.Notification_EncryptedChatAccepted = getValue(dict, "Notification.EncryptedChatAccepted") + self.Notifications_GroupNotificationsHelp = getValue(dict, "Notifications.GroupNotificationsHelp") + self.PhotoEditor_CropAspectRatioSquare = getValue(dict, "PhotoEditor.CropAspectRatioSquare") + self.Notification_CallOutgoing = getValue(dict, "Notification.CallOutgoing") + self.Weekday_ShortMonday = getValue(dict, "Weekday.ShortMonday") + self.Channel_Edit_AboutItem = getValue(dict, "Channel.Edit.AboutItem") + self.Checkout_Receipt_Title = getValue(dict, "Checkout.Receipt.Title") + self.Login_InfoLastNamePlaceholder = getValue(dict, "Login.InfoLastNamePlaceholder") + self.Contacts_InvitationText = getValue(dict, "Contacts.InvitationText") + self.Channel_Members_AddMembersHelp = getValue(dict, "Channel.Members.AddMembersHelp") + self.ReportPeer_Report = getValue(dict, "ReportPeer.Report") + self.Channel_EditMessageErrorGeneric = getValue(dict, "Channel.EditMessageErrorGeneric") + self.LoginPassword_FloodError = getValue(dict, "LoginPassword.FloodError") + self.EncryptionKey_TapToEmojify = getValue(dict, "EncryptionKey.TapToEmojify") + self.Conversation_InfoChannel = getValue(dict, "Conversation.InfoChannel") + self.TwoStepAuth_SetupPasswordTitle = getValue(dict, "TwoStepAuth.SetupPasswordTitle") + self.PhotoEditor_DiscardChanges = getValue(dict, "PhotoEditor.DiscardChanges") + self.Group_UpgradeNoticeText2 = getValue(dict, "Group.UpgradeNoticeText2") + self._PINNED_ROUND = getValue(dict, "PINNED_ROUND") + self._PINNED_ROUND_r = extractArgumentRanges(self._PINNED_ROUND) + self._ChannelInfo_ChannelForbidden = getValue(dict, "ChannelInfo.ChannelForbidden") + self._ChannelInfo_ChannelForbidden_r = extractArgumentRanges(self._ChannelInfo_ChannelForbidden) + self.Conversation_ShareMyContactInfo = getValue(dict, "Conversation.ShareMyContactInfo") + self._Profile_ShareContactPersonFormat = getValue(dict, "Profile.ShareContactPersonFormat") + self._Profile_ShareContactPersonFormat_r = extractArgumentRanges(self._Profile_ShareContactPersonFormat) + self._CHANNEL_MESSAGE_GEO = getValue(dict, "CHANNEL_MESSAGE_GEO") + self._CHANNEL_MESSAGE_GEO_r = extractArgumentRanges(self._CHANNEL_MESSAGE_GEO) + self.Group_Info_AdminLog = getValue(dict, "Group.Info.AdminLog") + self.StickerPacksSettings_FeaturedPacks = getValue(dict, "StickerPacksSettings.FeaturedPacks") + self.Month_GenAugust = getValue(dict, "Month.GenAugust") + self.Channel_Username_CreatePublicLinkHelp = getValue(dict, "Channel.Username.CreatePublicLinkHelp") + self.StickerPack_Send = getValue(dict, "StickerPack.Send") + self.Watch_Suggestion_HoldOn = getValue(dict, "Watch.Suggestion.HoldOn") + self.StickerSettings_MaskContextInfo = getValue(dict, "StickerSettings.MaskContextInfo") + self.AttachmentMenu_ImageSearch = getValue(dict, "AttachmentMenu.ImageSearch") + self._PINNED_GEO = getValue(dict, "PINNED_GEO") + self._PINNED_GEO_r = extractArgumentRanges(self._PINNED_GEO) + self.PasscodeSettings_EncryptData = getValue(dict, "PasscodeSettings.EncryptData") + self.Notification_CallCanceled = getValue(dict, "Notification.CallCanceled") + self.Common_NotNow = getValue(dict, "Common.NotNow") + self.PasscodeSettings_Title = getValue(dict, "PasscodeSettings.Title") + self.StickerPack_BuiltinPackName = getValue(dict, "StickerPack.BuiltinPackName") + self.Watch_Suggestion_BRB = getValue(dict, "Watch.Suggestion.BRB") + self.Login_CodeTitle = getValue(dict, "Login.CodeTitle") + self._CHAT_MESSAGE_ROUND = getValue(dict, "CHAT_MESSAGE_ROUND") + self._CHAT_MESSAGE_ROUND_r = extractArgumentRanges(self._CHAT_MESSAGE_ROUND) + self.Notifications_MessageNotificationsAlert = getValue(dict, "Notifications.MessageNotificationsAlert") + self.Username_InvalidCharacters = getValue(dict, "Username.InvalidCharacters") + self.GroupInfo_LabelAdmin = getValue(dict, "GroupInfo.LabelAdmin") + self.GroupInfo_Sound = getValue(dict, "GroupInfo.Sound") + self.Channel_EditAdmin_PermissionBanUsers = getValue(dict, "Channel.EditAdmin.PermissionBanUsers") + self.Wallpaper_PhotoLibrary = getValue(dict, "Wallpaper.PhotoLibrary") + self.Settings_About = getValue(dict, "Settings.About") + self._CHAT_LEFT = getValue(dict, "CHAT_LEFT") + self._CHAT_LEFT_r = extractArgumentRanges(self._CHAT_LEFT) + self.LoginPassword_ForgotPassword = getValue(dict, "LoginPassword.ForgotPassword") + self._DialogList_AwaitingEncryption = getValue(dict, "DialogList.AwaitingEncryption") + self._DialogList_AwaitingEncryption_r = extractArgumentRanges(self._DialogList_AwaitingEncryption) + self.ChatSettings_Appearance = getValue(dict, "ChatSettings.Appearance") + self.Tour_Title1 = getValue(dict, "Tour.Title1") + self._Notification_ChangedUserPhoto = getValue(dict, "Notification.ChangedUserPhoto") + self._Notification_ChangedUserPhoto_r = extractArgumentRanges(self._Notification_ChangedUserPhoto) + self.Conversation_LinkDialogCopy = getValue(dict, "Conversation.LinkDialogCopy") + self._Notification_PinnedLocationMessage = getValue(dict, "Notification.PinnedLocationMessage") + self._Notification_PinnedLocationMessage_r = extractArgumentRanges(self._Notification_PinnedLocationMessage) + self._Notification_PinnedPhotoMessage = getValue(dict, "Notification.PinnedPhotoMessage") + self._Notification_PinnedPhotoMessage_r = extractArgumentRanges(self._Notification_PinnedPhotoMessage) + self._DownloadingStatus = getValue(dict, "DownloadingStatus") + self._DownloadingStatus_r = extractArgumentRanges(self._DownloadingStatus) + self.Calls_All = getValue(dict, "Calls.All") + self._Channel_MessageTitleUpdated = getValue(dict, "Channel.MessageTitleUpdated") + self._Channel_MessageTitleUpdated_r = extractArgumentRanges(self._Channel_MessageTitleUpdated) + self.Call_CallAgain = getValue(dict, "Call.CallAgain") + self.TwoStepAuth_RecoveryCodeHelp = getValue(dict, "TwoStepAuth.RecoveryCodeHelp") + self.UserInfo_SendMessage = getValue(dict, "UserInfo.SendMessage") + self._Channel_Username_LinkHint = getValue(dict, "Channel.Username.LinkHint") + self._Channel_Username_LinkHint_r = extractArgumentRanges(self._Channel_Username_LinkHint) + self.Paint_RecentStickers = getValue(dict, "Paint.RecentStickers") + self.Login_CallRequestState3 = getValue(dict, "Login.CallRequestState3") + self.Channel_Edit_LinkItem = getValue(dict, "Channel.Edit.LinkItem") + self.CallSettings_Title = getValue(dict, "CallSettings.Title") + self.ChangePhoneNumberNumber_Help = getValue(dict, "ChangePhoneNumberNumber.Help") + self.Watch_Suggestion_Thanks = getValue(dict, "Watch.Suggestion.Thanks") + self.Channel_Moderator_Title = getValue(dict, "Channel.Moderator.Title") + self.Message_PinnedPhotoMessage = getValue(dict, "Message.PinnedPhotoMessage") + self.Notification_SecretChatScreenshot = getValue(dict, "Notification.SecretChatScreenshot") + self._Conversation_DeleteMessagesFor = getValue(dict, "Conversation.DeleteMessagesFor") + self._Conversation_DeleteMessagesFor_r = extractArgumentRanges(self._Conversation_DeleteMessagesFor) + self.Activity_UploadingDocument = getValue(dict, "Activity.UploadingDocument") + self.Watch_ChatList_NoConversationsText = getValue(dict, "Watch.ChatList.NoConversationsText") + self.ReportPeer_AlertSuccess = getValue(dict, "ReportPeer.AlertSuccess") + self.Tour_Text4 = getValue(dict, "Tour.Text4") + self.Channel_Info_Description = getValue(dict, "Channel.Info.Description") + self.AccessDenied_LocationTracking = getValue(dict, "AccessDenied.LocationTracking") + self.MessageTimer_Title = getValue(dict, "MessageTimer.Title") + self.Watch_Compose_Send = getValue(dict, "Watch.Compose.Send") + self.Preview_CopyAddress = getValue(dict, "Preview.CopyAddress") + self.Settings_BlockedUsers = getValue(dict, "Settings.BlockedUsers") + self.Month_ShortAugust = getValue(dict, "Month.ShortAugust") + self.Channel_AdminLogFilter_AdminsTitle = getValue(dict, "Channel.AdminLogFilter.AdminsTitle") + self.Channel_EditAdmin_PermissionChangeInfo = getValue(dict, "Channel.EditAdmin.PermissionChangeInfo") + self.Notifications_ResetAllNotificationsHelp = getValue(dict, "Notifications.ResetAllNotificationsHelp") + self.DialogList_EncryptionRejected = getValue(dict, "DialogList.EncryptionRejected") + self.AccessDenied_CameraRestricted = getValue(dict, "AccessDenied.CameraRestricted") + self.Target_InviteToGroupErrorAlreadyInvited = getValue(dict, "Target.InviteToGroupErrorAlreadyInvited") + self.Watch_Message_ForwardedFrom = getValue(dict, "Watch.Message.ForwardedFrom") + self.Channel_AboutItem = getValue(dict, "Channel.AboutItem") + self.PhotoEditor_CurvesGreen = getValue(dict, "PhotoEditor.CurvesGreen") + self.CheckoutInfo_ShippingInfoCountryPlaceholder = getValue(dict, "CheckoutInfo.ShippingInfoCountryPlaceholder") + self.Month_GenJuly = getValue(dict, "Month.GenJuly") + self.Conversation_DeleteChat = getValue(dict, "Conversation.DeleteChat") + self._DialogList_SingleUploadingFileSuffix = getValue(dict, "DialogList.SingleUploadingFileSuffix") + self._DialogList_SingleUploadingFileSuffix_r = extractArgumentRanges(self._DialogList_SingleUploadingFileSuffix) + self.ChannelIntro_CreateChannel = getValue(dict, "ChannelIntro.CreateChannel") + self.WelcomeScreen_ContactsAccessDisabled = getValue(dict, "WelcomeScreen.ContactsAccessDisabled") + self.Channel_Management_AddModerator = getValue(dict, "Channel.Management.AddModerator") + self.Common_ChoosePhoto = getValue(dict, "Common.ChoosePhoto") + self.Group_Username_Help = getValue(dict, "Group.Username.Help") + self.Conversation_Pin = getValue(dict, "Conversation.Pin") + self.Channel_AdminLog_CanStartCalls = getValue(dict, "Channel.AdminLog.CanStartCalls") + self._Login_ResetAccountProtected_Text = getValue(dict, "Login.ResetAccountProtected.Text") + self._Login_ResetAccountProtected_Text_r = extractArgumentRanges(self._Login_ResetAccountProtected_Text) + self.Camera_TapAndHoldForVideo = getValue(dict, "Camera.TapAndHoldForVideo") + self.Bot_DescriptionTitle = getValue(dict, "Bot.DescriptionTitle") + self.FeaturedStickerPacks_Title = getValue(dict, "FeaturedStickerPacks.Title") + self.Map_OpenInGoogleMaps = getValue(dict, "Map.OpenInGoogleMaps") + self.Notification_MessageLifetime5s = getValue(dict, "Notification.MessageLifetime5s") + self.EnterPasscode_SetupTitle = getValue(dict, "EnterPasscode.SetupTitle") + self.Contacts_Title = getValue(dict, "Contacts.Title") + self.Channel_Management_AddModeratorHelp = getValue(dict, "Channel.Management.AddModeratorHelp") + self._CHAT_MESSAGE_FWDS = getValue(dict, "CHAT_MESSAGE_FWDS") + self._CHAT_MESSAGE_FWDS_r = extractArgumentRanges(self._CHAT_MESSAGE_FWDS) + self.WelcomeScreen_UpdatingTitle = getValue(dict, "WelcomeScreen.UpdatingTitle") + self._Login_CodeHelp = getValue(dict, "Login.CodeHelp") + self._Login_CodeHelp_r = extractArgumentRanges(self._Login_CodeHelp) + self.Conversation_MessageDialogEdit = getValue(dict, "Conversation.MessageDialogEdit") + self.PrivacyLastSeenSettings_Title = getValue(dict, "PrivacyLastSeenSettings.Title") + self.Notifications_ClassicTones = getValue(dict, "Notifications.ClassicTones") + self.GoogleDrive_Title = getValue(dict, "GoogleDrive.Title") + self.Conversation_LinkDialogOpen = getValue(dict, "Conversation.LinkDialogOpen") + self.Conversation_ClousStorageInfo_Description4 = getValue(dict, "Conversation.ClousStorageInfo.Description4") + self.Privacy_Calls_AlwaysAllow = getValue(dict, "Privacy.Calls.AlwaysAllow") + self.Privacy_PaymentsClearInfoHelp = getValue(dict, "Privacy.PaymentsClearInfoHelp") + self.Notification_MessageLifetime1h = getValue(dict, "Notification.MessageLifetime1h") + self._Notification_CreatedChatWithTitle = getValue(dict, "Notification.CreatedChatWithTitle") + self._Notification_CreatedChatWithTitle_r = extractArgumentRanges(self._Notification_CreatedChatWithTitle) + self.CheckoutInfo_ReceiverInfoEmail = getValue(dict, "CheckoutInfo.ReceiverInfoEmail") + self.LastSeen_Lately = getValue(dict, "LastSeen.Lately") + self.Month_ShortApril = getValue(dict, "Month.ShortApril") + self.ConversationProfile_ErrorCreatingConversation = getValue(dict, "ConversationProfile.ErrorCreatingConversation") + self._PHONE_CALL_MISSED = getValue(dict, "PHONE_CALL_MISSED") + self._PHONE_CALL_MISSED_r = extractArgumentRanges(self._PHONE_CALL_MISSED) + self.Map_AccessDeniedError = getValue(dict, "Map.AccessDeniedError") + self._Conversation_Kilobytes = getValue(dict, "Conversation.Kilobytes") + self._Conversation_Kilobytes_r = extractArgumentRanges(self._Conversation_Kilobytes) + self.Group_ErrorAddBlocked = getValue(dict, "Group.ErrorAddBlocked") + self.MediaPicker_Videos = getValue(dict, "MediaPicker.Videos") + self.BlockedUsers_AddNew = getValue(dict, "BlockedUsers.AddNew") + self.StickerPacksSettings_StickerPacksSection = getValue(dict, "StickerPacksSettings.StickerPacksSection") + self.Channel_NotificationLoading = getValue(dict, "Channel.NotificationLoading") + self._CHAT_RETURNED = getValue(dict, "CHAT_RETURNED") + self._CHAT_RETURNED_r = extractArgumentRanges(self._CHAT_RETURNED) + self.PhotoEditor_ShadowsTint = getValue(dict, "PhotoEditor.ShadowsTint") + self.ExplicitContent_AlertTitle = getValue(dict, "ExplicitContent.AlertTitle") + self.Channel_AdminLogFilter_EventsLeaving = getValue(dict, "Channel.AdminLogFilter.EventsLeaving") + self.StickerPack_HideStickers = getValue(dict, "StickerPack.HideStickers") + self._Group_MessageTitleUpdated = getValue(dict, "Group.MessageTitleUpdated") + self._Group_MessageTitleUpdated_r = extractArgumentRanges(self._Group_MessageTitleUpdated) + self.Checkout_EnterPassword = getValue(dict, "Checkout.EnterPassword") + self.UserInfo_NotificationsEnabled = getValue(dict, "UserInfo.NotificationsEnabled") + self.Weekday_ShortTuesday = getValue(dict, "Weekday.ShortTuesday") + self.Notification_CallIncomingShort = getValue(dict, "Notification.CallIncomingShort") + self.ConvertToSupergroup_Note = getValue(dict, "ConvertToSupergroup.Note") + self.Conversation_EmptyPlaceholder = getValue(dict, "Conversation.EmptyPlaceholder") + self.Conversation_BroadcastTitle = getValue(dict, "Conversation.BroadcastTitle") + self.Username_Help = getValue(dict, "Username.Help") + self.StickerSettings_ContextHide = getValue(dict, "StickerSettings.ContextHide") + self.Weekday_Sunday = getValue(dict, "Weekday.Sunday") + self.Preview_LoadingImage = getValue(dict, "Preview.LoadingImage") + self._Conversation_DownloadProgressKilobytes = getValue(dict, "Conversation.DownloadProgressKilobytes") + self._Conversation_DownloadProgressKilobytes_r = extractArgumentRanges(self._Conversation_DownloadProgressKilobytes) + self.Settings_ChatBackground = getValue(dict, "Settings.ChatBackground") + self._MessageTimer_Seconds_zero = getValueWithForm(dict, "MessageTimer.Seconds", .zero) + self._MessageTimer_Seconds_one = getValueWithForm(dict, "MessageTimer.Seconds", .one) + self._MessageTimer_Seconds_two = getValueWithForm(dict, "MessageTimer.Seconds", .two) + self._MessageTimer_Seconds_few = getValueWithForm(dict, "MessageTimer.Seconds", .few) + self._MessageTimer_Seconds_many = getValueWithForm(dict, "MessageTimer.Seconds", .many) + self._MessageTimer_Seconds_other = getValueWithForm(dict, "MessageTimer.Seconds", .other) + self._Call_Seconds_zero = getValueWithForm(dict, "Call.Seconds", .zero) + self._Call_Seconds_one = getValueWithForm(dict, "Call.Seconds", .one) + self._Call_Seconds_two = getValueWithForm(dict, "Call.Seconds", .two) + self._Call_Seconds_few = getValueWithForm(dict, "Call.Seconds", .few) + self._Call_Seconds_many = getValueWithForm(dict, "Call.Seconds", .many) + self._Call_Seconds_other = getValueWithForm(dict, "Call.Seconds", .other) + self._MessageTimer_ShortSeconds_zero = getValueWithForm(dict, "MessageTimer.ShortSeconds", .zero) + self._MessageTimer_ShortSeconds_one = getValueWithForm(dict, "MessageTimer.ShortSeconds", .one) + self._MessageTimer_ShortSeconds_two = getValueWithForm(dict, "MessageTimer.ShortSeconds", .two) + self._MessageTimer_ShortSeconds_few = getValueWithForm(dict, "MessageTimer.ShortSeconds", .few) + self._MessageTimer_ShortSeconds_many = getValueWithForm(dict, "MessageTimer.ShortSeconds", .many) + self._MessageTimer_ShortSeconds_other = getValueWithForm(dict, "MessageTimer.ShortSeconds", .other) + self._Notification_GameScoreSimple_zero = getValueWithForm(dict, "Notification.GameScoreSimple", .zero) + self._Notification_GameScoreSimple_one = getValueWithForm(dict, "Notification.GameScoreSimple", .one) + self._Notification_GameScoreSimple_two = getValueWithForm(dict, "Notification.GameScoreSimple", .two) + self._Notification_GameScoreSimple_few = getValueWithForm(dict, "Notification.GameScoreSimple", .few) + self._Notification_GameScoreSimple_many = getValueWithForm(dict, "Notification.GameScoreSimple", .many) + self._Notification_GameScoreSimple_other = getValueWithForm(dict, "Notification.GameScoreSimple", .other) + self._Notification_GameScoreExtended_zero = getValueWithForm(dict, "Notification.GameScoreExtended", .zero) + self._Notification_GameScoreExtended_one = getValueWithForm(dict, "Notification.GameScoreExtended", .one) + self._Notification_GameScoreExtended_two = getValueWithForm(dict, "Notification.GameScoreExtended", .two) + self._Notification_GameScoreExtended_few = getValueWithForm(dict, "Notification.GameScoreExtended", .few) + self._Notification_GameScoreExtended_many = getValueWithForm(dict, "Notification.GameScoreExtended", .many) + self._Notification_GameScoreExtended_other = getValueWithForm(dict, "Notification.GameScoreExtended", .other) + self._PasscodeSettings_FailedAttempts_zero = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .zero) + self._PasscodeSettings_FailedAttempts_one = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .one) + self._PasscodeSettings_FailedAttempts_two = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .two) + self._PasscodeSettings_FailedAttempts_few = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .few) + self._PasscodeSettings_FailedAttempts_many = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .many) + self._PasscodeSettings_FailedAttempts_other = getValueWithForm(dict, "PasscodeSettings.FailedAttempts", .other) + self._MuteFor_Hours_zero = getValueWithForm(dict, "MuteFor.Hours", .zero) + self._MuteFor_Hours_one = getValueWithForm(dict, "MuteFor.Hours", .one) + self._MuteFor_Hours_two = getValueWithForm(dict, "MuteFor.Hours", .two) + self._MuteFor_Hours_few = getValueWithForm(dict, "MuteFor.Hours", .few) + self._MuteFor_Hours_many = getValueWithForm(dict, "MuteFor.Hours", .many) + self._MuteFor_Hours_other = getValueWithForm(dict, "MuteFor.Hours", .other) + self._MessageTimer_ShortMinutes_zero = getValueWithForm(dict, "MessageTimer.ShortMinutes", .zero) + self._MessageTimer_ShortMinutes_one = getValueWithForm(dict, "MessageTimer.ShortMinutes", .one) + self._MessageTimer_ShortMinutes_two = getValueWithForm(dict, "MessageTimer.ShortMinutes", .two) + self._MessageTimer_ShortMinutes_few = getValueWithForm(dict, "MessageTimer.ShortMinutes", .few) + self._MessageTimer_ShortMinutes_many = getValueWithForm(dict, "MessageTimer.ShortMinutes", .many) + self._MessageTimer_ShortMinutes_other = getValueWithForm(dict, "MessageTimer.ShortMinutes", .other) + self._Notification_GameScoreSelfExtended_zero = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .zero) + self._Notification_GameScoreSelfExtended_one = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .one) + self._Notification_GameScoreSelfExtended_two = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .two) + self._Notification_GameScoreSelfExtended_few = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .few) + self._Notification_GameScoreSelfExtended_many = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .many) + self._Notification_GameScoreSelfExtended_other = getValueWithForm(dict, "Notification.GameScoreSelfExtended", .other) + self._MessageTimer_ShortDays_zero = getValueWithForm(dict, "MessageTimer.ShortDays", .zero) + self._MessageTimer_ShortDays_one = getValueWithForm(dict, "MessageTimer.ShortDays", .one) + self._MessageTimer_ShortDays_two = getValueWithForm(dict, "MessageTimer.ShortDays", .two) + self._MessageTimer_ShortDays_few = getValueWithForm(dict, "MessageTimer.ShortDays", .few) + self._MessageTimer_ShortDays_many = getValueWithForm(dict, "MessageTimer.ShortDays", .many) + self._MessageTimer_ShortDays_other = getValueWithForm(dict, "MessageTimer.ShortDays", .other) + self._GroupInfo_ParticipantCount_zero = getValueWithForm(dict, "GroupInfo.ParticipantCount", .zero) + self._GroupInfo_ParticipantCount_one = getValueWithForm(dict, "GroupInfo.ParticipantCount", .one) + self._GroupInfo_ParticipantCount_two = getValueWithForm(dict, "GroupInfo.ParticipantCount", .two) + self._GroupInfo_ParticipantCount_few = getValueWithForm(dict, "GroupInfo.ParticipantCount", .few) + self._GroupInfo_ParticipantCount_many = getValueWithForm(dict, "GroupInfo.ParticipantCount", .many) + self._GroupInfo_ParticipantCount_other = getValueWithForm(dict, "GroupInfo.ParticipantCount", .other) + self._ForwardedPhotos_zero = getValueWithForm(dict, "ForwardedPhotos", .zero) + self._ForwardedPhotos_one = getValueWithForm(dict, "ForwardedPhotos", .one) + self._ForwardedPhotos_two = getValueWithForm(dict, "ForwardedPhotos", .two) + self._ForwardedPhotos_few = getValueWithForm(dict, "ForwardedPhotos", .few) + self._ForwardedPhotos_many = getValueWithForm(dict, "ForwardedPhotos", .many) + self._ForwardedPhotos_other = getValueWithForm(dict, "ForwardedPhotos", .other) + self._ServiceMessage_GameScoreSelfExtended_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .zero) + self._ServiceMessage_GameScoreSelfExtended_one = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .one) + self._ServiceMessage_GameScoreSelfExtended_two = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .two) + self._ServiceMessage_GameScoreSelfExtended_few = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .few) + self._ServiceMessage_GameScoreSelfExtended_many = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .many) + self._ServiceMessage_GameScoreSelfExtended_other = getValueWithForm(dict, "ServiceMessage.GameScoreSelfExtended", .other) + self._Call_ShortSeconds_zero = getValueWithForm(dict, "Call.ShortSeconds", .zero) + self._Call_ShortSeconds_one = getValueWithForm(dict, "Call.ShortSeconds", .one) + self._Call_ShortSeconds_two = getValueWithForm(dict, "Call.ShortSeconds", .two) + self._Call_ShortSeconds_few = getValueWithForm(dict, "Call.ShortSeconds", .few) + self._Call_ShortSeconds_many = getValueWithForm(dict, "Call.ShortSeconds", .many) + self._Call_ShortSeconds_other = getValueWithForm(dict, "Call.ShortSeconds", .other) + self._Time_PreciseDate_zero = getValueWithForm(dict, "Time.PreciseDate", .zero) + self._Time_PreciseDate_one = getValueWithForm(dict, "Time.PreciseDate", .one) + self._Time_PreciseDate_two = getValueWithForm(dict, "Time.PreciseDate", .two) + self._Time_PreciseDate_few = getValueWithForm(dict, "Time.PreciseDate", .few) + self._Time_PreciseDate_many = getValueWithForm(dict, "Time.PreciseDate", .many) + self._Time_PreciseDate_other = getValueWithForm(dict, "Time.PreciseDate", .other) + self._SharedMedia_File_zero = getValueWithForm(dict, "SharedMedia.File", .zero) + self._SharedMedia_File_one = getValueWithForm(dict, "SharedMedia.File", .one) + self._SharedMedia_File_two = getValueWithForm(dict, "SharedMedia.File", .two) + self._SharedMedia_File_few = getValueWithForm(dict, "SharedMedia.File", .few) + self._SharedMedia_File_many = getValueWithForm(dict, "SharedMedia.File", .many) + self._SharedMedia_File_other = getValueWithForm(dict, "SharedMedia.File", .other) + self._PasscodeSettings_AutoLock_IfAwayFor_zero = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .zero) + self._PasscodeSettings_AutoLock_IfAwayFor_one = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .one) + self._PasscodeSettings_AutoLock_IfAwayFor_two = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .two) + self._PasscodeSettings_AutoLock_IfAwayFor_few = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .few) + self._PasscodeSettings_AutoLock_IfAwayFor_many = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .many) + self._PasscodeSettings_AutoLock_IfAwayFor_other = getValueWithForm(dict, "PasscodeSettings.AutoLock.IfAwayFor", .other) + self._ForwardedAudios_zero = getValueWithForm(dict, "ForwardedAudios", .zero) + self._ForwardedAudios_one = getValueWithForm(dict, "ForwardedAudios", .one) + self._ForwardedAudios_two = getValueWithForm(dict, "ForwardedAudios", .two) + self._ForwardedAudios_few = getValueWithForm(dict, "ForwardedAudios", .few) + self._ForwardedAudios_many = getValueWithForm(dict, "ForwardedAudios", .many) + self._ForwardedAudios_other = getValueWithForm(dict, "ForwardedAudios", .other) + self._PrivacyLastSeenSettings_AddUsers_zero = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .zero) + self._PrivacyLastSeenSettings_AddUsers_one = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .one) + self._PrivacyLastSeenSettings_AddUsers_two = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .two) + self._PrivacyLastSeenSettings_AddUsers_few = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .few) + self._PrivacyLastSeenSettings_AddUsers_many = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .many) + self._PrivacyLastSeenSettings_AddUsers_other = getValueWithForm(dict, "PrivacyLastSeenSettings.AddUsers", .other) + self._MuteFor_Weeks_zero = getValueWithForm(dict, "MuteFor.Weeks", .zero) + self._MuteFor_Weeks_one = getValueWithForm(dict, "MuteFor.Weeks", .one) + self._MuteFor_Weeks_two = getValueWithForm(dict, "MuteFor.Weeks", .two) + self._MuteFor_Weeks_few = getValueWithForm(dict, "MuteFor.Weeks", .few) + self._MuteFor_Weeks_many = getValueWithForm(dict, "MuteFor.Weeks", .many) + self._MuteFor_Weeks_other = getValueWithForm(dict, "MuteFor.Weeks", .other) + self._ForwardedVideoMessages_zero = getValueWithForm(dict, "ForwardedVideoMessages", .zero) + self._ForwardedVideoMessages_one = getValueWithForm(dict, "ForwardedVideoMessages", .one) + self._ForwardedVideoMessages_two = getValueWithForm(dict, "ForwardedVideoMessages", .two) + self._ForwardedVideoMessages_few = getValueWithForm(dict, "ForwardedVideoMessages", .few) + self._ForwardedVideoMessages_many = getValueWithForm(dict, "ForwardedVideoMessages", .many) + self._ForwardedVideoMessages_other = getValueWithForm(dict, "ForwardedVideoMessages", .other) + self._SharedMedia_Generic_zero = getValueWithForm(dict, "SharedMedia.Generic", .zero) + self._SharedMedia_Generic_one = getValueWithForm(dict, "SharedMedia.Generic", .one) + self._SharedMedia_Generic_two = getValueWithForm(dict, "SharedMedia.Generic", .two) + self._SharedMedia_Generic_few = getValueWithForm(dict, "SharedMedia.Generic", .few) + self._SharedMedia_Generic_many = getValueWithForm(dict, "SharedMedia.Generic", .many) + self._SharedMedia_Generic_other = getValueWithForm(dict, "SharedMedia.Generic", .other) + self._Conversation_StatusMembers_zero = getValueWithForm(dict, "Conversation.StatusMembers", .zero) + self._Conversation_StatusMembers_one = getValueWithForm(dict, "Conversation.StatusMembers", .one) + self._Conversation_StatusMembers_two = getValueWithForm(dict, "Conversation.StatusMembers", .two) + self._Conversation_StatusMembers_few = getValueWithForm(dict, "Conversation.StatusMembers", .few) + self._Conversation_StatusMembers_many = getValueWithForm(dict, "Conversation.StatusMembers", .many) + self._Conversation_StatusMembers_other = getValueWithForm(dict, "Conversation.StatusMembers", .other) + self._Invitation_Members_zero = getValueWithForm(dict, "Invitation.Members", .zero) + self._Invitation_Members_one = getValueWithForm(dict, "Invitation.Members", .one) + self._Invitation_Members_two = getValueWithForm(dict, "Invitation.Members", .two) + self._Invitation_Members_few = getValueWithForm(dict, "Invitation.Members", .few) + self._Invitation_Members_many = getValueWithForm(dict, "Invitation.Members", .many) + self._Invitation_Members_other = getValueWithForm(dict, "Invitation.Members", .other) + self._ForwardedFiles_zero = getValueWithForm(dict, "ForwardedFiles", .zero) + self._ForwardedFiles_one = getValueWithForm(dict, "ForwardedFiles", .one) + self._ForwardedFiles_two = getValueWithForm(dict, "ForwardedFiles", .two) + self._ForwardedFiles_few = getValueWithForm(dict, "ForwardedFiles", .few) + self._ForwardedFiles_many = getValueWithForm(dict, "ForwardedFiles", .many) + self._ForwardedFiles_other = getValueWithForm(dict, "ForwardedFiles", .other) + self._ForwardedStickers_zero = getValueWithForm(dict, "ForwardedStickers", .zero) + self._ForwardedStickers_one = getValueWithForm(dict, "ForwardedStickers", .one) + self._ForwardedStickers_two = getValueWithForm(dict, "ForwardedStickers", .two) + self._ForwardedStickers_few = getValueWithForm(dict, "ForwardedStickers", .few) + self._ForwardedStickers_many = getValueWithForm(dict, "ForwardedStickers", .many) + self._ForwardedStickers_other = getValueWithForm(dict, "ForwardedStickers", .other) + self._StickerPack_StickerCount_zero = getValueWithForm(dict, "StickerPack.StickerCount", .zero) + self._StickerPack_StickerCount_one = getValueWithForm(dict, "StickerPack.StickerCount", .one) + self._StickerPack_StickerCount_two = getValueWithForm(dict, "StickerPack.StickerCount", .two) + self._StickerPack_StickerCount_few = getValueWithForm(dict, "StickerPack.StickerCount", .few) + self._StickerPack_StickerCount_many = getValueWithForm(dict, "StickerPack.StickerCount", .many) + self._StickerPack_StickerCount_other = getValueWithForm(dict, "StickerPack.StickerCount", .other) + self._SharedMedia_Video_zero = getValueWithForm(dict, "SharedMedia.Video", .zero) + self._SharedMedia_Video_one = getValueWithForm(dict, "SharedMedia.Video", .one) + self._SharedMedia_Video_two = getValueWithForm(dict, "SharedMedia.Video", .two) + self._SharedMedia_Video_few = getValueWithForm(dict, "SharedMedia.Video", .few) + self._SharedMedia_Video_many = getValueWithForm(dict, "SharedMedia.Video", .many) + self._SharedMedia_Video_other = getValueWithForm(dict, "SharedMedia.Video", .other) + self._ForwardedAuthorsOthers_zero = getValueWithForm(dict, "ForwardedAuthorsOthers", .zero) + self._ForwardedAuthorsOthers_one = getValueWithForm(dict, "ForwardedAuthorsOthers", .one) + self._ForwardedAuthorsOthers_two = getValueWithForm(dict, "ForwardedAuthorsOthers", .two) + self._ForwardedAuthorsOthers_few = getValueWithForm(dict, "ForwardedAuthorsOthers", .few) + self._ForwardedAuthorsOthers_many = getValueWithForm(dict, "ForwardedAuthorsOthers", .many) + self._ForwardedAuthorsOthers_other = getValueWithForm(dict, "ForwardedAuthorsOthers", .other) + self._MuteFor_Minutes_zero = getValueWithForm(dict, "MuteFor.Minutes", .zero) + self._MuteFor_Minutes_one = getValueWithForm(dict, "MuteFor.Minutes", .one) + self._MuteFor_Minutes_two = getValueWithForm(dict, "MuteFor.Minutes", .two) + self._MuteFor_Minutes_few = getValueWithForm(dict, "MuteFor.Minutes", .few) + self._MuteFor_Minutes_many = getValueWithForm(dict, "MuteFor.Minutes", .many) + self._MuteFor_Minutes_other = getValueWithForm(dict, "MuteFor.Minutes", .other) + self._AttachmentMenu_SendVideo_zero = getValueWithForm(dict, "AttachmentMenu.SendVideo", .zero) + self._AttachmentMenu_SendVideo_one = getValueWithForm(dict, "AttachmentMenu.SendVideo", .one) + self._AttachmentMenu_SendVideo_two = getValueWithForm(dict, "AttachmentMenu.SendVideo", .two) + self._AttachmentMenu_SendVideo_few = getValueWithForm(dict, "AttachmentMenu.SendVideo", .few) + self._AttachmentMenu_SendVideo_many = getValueWithForm(dict, "AttachmentMenu.SendVideo", .many) + self._AttachmentMenu_SendVideo_other = getValueWithForm(dict, "AttachmentMenu.SendVideo", .other) + self._Call_Minutes_zero = getValueWithForm(dict, "Call.Minutes", .zero) + self._Call_Minutes_one = getValueWithForm(dict, "Call.Minutes", .one) + self._Call_Minutes_two = getValueWithForm(dict, "Call.Minutes", .two) + self._Call_Minutes_few = getValueWithForm(dict, "Call.Minutes", .few) + self._Call_Minutes_many = getValueWithForm(dict, "Call.Minutes", .many) + self._Call_Minutes_other = getValueWithForm(dict, "Call.Minutes", .other) + self._ForwardedContacts_zero = getValueWithForm(dict, "ForwardedContacts", .zero) + self._ForwardedContacts_one = getValueWithForm(dict, "ForwardedContacts", .one) + self._ForwardedContacts_two = getValueWithForm(dict, "ForwardedContacts", .two) + self._ForwardedContacts_few = getValueWithForm(dict, "ForwardedContacts", .few) + self._ForwardedContacts_many = getValueWithForm(dict, "ForwardedContacts", .many) + self._ForwardedContacts_other = getValueWithForm(dict, "ForwardedContacts", .other) + self._Channel_NotificationComments_zero = getValueWithForm(dict, "Channel.NotificationComments", .zero) + self._Channel_NotificationComments_one = getValueWithForm(dict, "Channel.NotificationComments", .one) + self._Channel_NotificationComments_two = getValueWithForm(dict, "Channel.NotificationComments", .two) + self._Channel_NotificationComments_few = getValueWithForm(dict, "Channel.NotificationComments", .few) + self._Channel_NotificationComments_many = getValueWithForm(dict, "Channel.NotificationComments", .many) + self._Channel_NotificationComments_other = getValueWithForm(dict, "Channel.NotificationComments", .other) + self._UserCount_zero = getValueWithForm(dict, "UserCount", .zero) + self._UserCount_one = getValueWithForm(dict, "UserCount", .one) + self._UserCount_two = getValueWithForm(dict, "UserCount", .two) + self._UserCount_few = getValueWithForm(dict, "UserCount", .few) + self._UserCount_many = getValueWithForm(dict, "UserCount", .many) + self._UserCount_other = getValueWithForm(dict, "UserCount", .other) + self._ForwardedGifs_zero = getValueWithForm(dict, "ForwardedGifs", .zero) + self._ForwardedGifs_one = getValueWithForm(dict, "ForwardedGifs", .one) + self._ForwardedGifs_two = getValueWithForm(dict, "ForwardedGifs", .two) + self._ForwardedGifs_few = getValueWithForm(dict, "ForwardedGifs", .few) + self._ForwardedGifs_many = getValueWithForm(dict, "ForwardedGifs", .many) + self._ForwardedGifs_other = getValueWithForm(dict, "ForwardedGifs", .other) + self._MessageTimer_ShortHours_zero = getValueWithForm(dict, "MessageTimer.ShortHours", .zero) + self._MessageTimer_ShortHours_one = getValueWithForm(dict, "MessageTimer.ShortHours", .one) + self._MessageTimer_ShortHours_two = getValueWithForm(dict, "MessageTimer.ShortHours", .two) + self._MessageTimer_ShortHours_few = getValueWithForm(dict, "MessageTimer.ShortHours", .few) + self._MessageTimer_ShortHours_many = getValueWithForm(dict, "MessageTimer.ShortHours", .many) + self._MessageTimer_ShortHours_other = getValueWithForm(dict, "MessageTimer.ShortHours", .other) + self._ServiceMessage_GameScoreExtended_zero = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .zero) + self._ServiceMessage_GameScoreExtended_one = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .one) + self._ServiceMessage_GameScoreExtended_two = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .two) + self._ServiceMessage_GameScoreExtended_few = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .few) + self._ServiceMessage_GameScoreExtended_many = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .many) + self._ServiceMessage_GameScoreExtended_other = getValueWithForm(dict, "ServiceMessage.GameScoreExtended", .other) + self._StickerPack_AddStickerCount_zero = getValueWithForm(dict, "StickerPack.AddStickerCount", .zero) + self._StickerPack_AddStickerCount_one = getValueWithForm(dict, "StickerPack.AddStickerCount", .one) + self._StickerPack_AddStickerCount_two = getValueWithForm(dict, "StickerPack.AddStickerCount", .two) + self._StickerPack_AddStickerCount_few = getValueWithForm(dict, "StickerPack.AddStickerCount", .few) + self._StickerPack_AddStickerCount_many = getValueWithForm(dict, "StickerPack.AddStickerCount", .many) + self._StickerPack_AddStickerCount_other = getValueWithForm(dict, "StickerPack.AddStickerCount", .other) + self._AttachmentMenu_SendPhoto_zero = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .zero) + self._AttachmentMenu_SendPhoto_one = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .one) + self._AttachmentMenu_SendPhoto_two = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .two) + self._AttachmentMenu_SendPhoto_few = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .few) + self._AttachmentMenu_SendPhoto_many = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .many) + self._AttachmentMenu_SendPhoto_other = getValueWithForm(dict, "AttachmentMenu.SendPhoto", .other) + self._Conversation_StatusRecipients_zero = getValueWithForm(dict, "Conversation.StatusRecipients", .zero) + self._Conversation_StatusRecipients_one = getValueWithForm(dict, "Conversation.StatusRecipients", .one) + self._Conversation_StatusRecipients_two = getValueWithForm(dict, "Conversation.StatusRecipients", .two) + self._Conversation_StatusRecipients_few = getValueWithForm(dict, "Conversation.StatusRecipients", .few) + self._Conversation_StatusRecipients_many = getValueWithForm(dict, "Conversation.StatusRecipients", .many) + self._Conversation_StatusRecipients_other = getValueWithForm(dict, "Conversation.StatusRecipients", .other) + self._Channel_Management_LabelRights_zero = getValueWithForm(dict, "Channel.Management.LabelRights", .zero) + self._Channel_Management_LabelRights_one = getValueWithForm(dict, "Channel.Management.LabelRights", .one) + self._Channel_Management_LabelRights_two = getValueWithForm(dict, "Channel.Management.LabelRights", .two) + self._Channel_Management_LabelRights_few = getValueWithForm(dict, "Channel.Management.LabelRights", .few) + self._Channel_Management_LabelRights_many = getValueWithForm(dict, "Channel.Management.LabelRights", .many) + self._Channel_Management_LabelRights_other = getValueWithForm(dict, "Channel.Management.LabelRights", .other) + self._ServiceMessage_GameScoreSelfSimple_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .zero) + self._ServiceMessage_GameScoreSelfSimple_one = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .one) + self._ServiceMessage_GameScoreSelfSimple_two = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .two) + self._ServiceMessage_GameScoreSelfSimple_few = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .few) + self._ServiceMessage_GameScoreSelfSimple_many = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .many) + self._ServiceMessage_GameScoreSelfSimple_other = getValueWithForm(dict, "ServiceMessage.GameScoreSelfSimple", .other) + self._SharedMedia_Photo_zero = getValueWithForm(dict, "SharedMedia.Photo", .zero) + self._SharedMedia_Photo_one = getValueWithForm(dict, "SharedMedia.Photo", .one) + self._SharedMedia_Photo_two = getValueWithForm(dict, "SharedMedia.Photo", .two) + self._SharedMedia_Photo_few = getValueWithForm(dict, "SharedMedia.Photo", .few) + self._SharedMedia_Photo_many = getValueWithForm(dict, "SharedMedia.Photo", .many) + self._SharedMedia_Photo_other = getValueWithForm(dict, "SharedMedia.Photo", .other) + self._MessageTimer_Weeks_zero = getValueWithForm(dict, "MessageTimer.Weeks", .zero) + self._MessageTimer_Weeks_one = getValueWithForm(dict, "MessageTimer.Weeks", .one) + self._MessageTimer_Weeks_two = getValueWithForm(dict, "MessageTimer.Weeks", .two) + self._MessageTimer_Weeks_few = getValueWithForm(dict, "MessageTimer.Weeks", .few) + self._MessageTimer_Weeks_many = getValueWithForm(dict, "MessageTimer.Weeks", .many) + self._MessageTimer_Weeks_other = getValueWithForm(dict, "MessageTimer.Weeks", .other) + self._StickerPack_AddMaskCount_zero = getValueWithForm(dict, "StickerPack.AddMaskCount", .zero) + self._StickerPack_AddMaskCount_one = getValueWithForm(dict, "StickerPack.AddMaskCount", .one) + self._StickerPack_AddMaskCount_two = getValueWithForm(dict, "StickerPack.AddMaskCount", .two) + self._StickerPack_AddMaskCount_few = getValueWithForm(dict, "StickerPack.AddMaskCount", .few) + self._StickerPack_AddMaskCount_many = getValueWithForm(dict, "StickerPack.AddMaskCount", .many) + self._StickerPack_AddMaskCount_other = getValueWithForm(dict, "StickerPack.AddMaskCount", .other) + self._LastSeen_MinutesAgo_zero = getValueWithForm(dict, "LastSeen.MinutesAgo", .zero) + self._LastSeen_MinutesAgo_one = getValueWithForm(dict, "LastSeen.MinutesAgo", .one) + self._LastSeen_MinutesAgo_two = getValueWithForm(dict, "LastSeen.MinutesAgo", .two) + self._LastSeen_MinutesAgo_few = getValueWithForm(dict, "LastSeen.MinutesAgo", .few) + self._LastSeen_MinutesAgo_many = getValueWithForm(dict, "LastSeen.MinutesAgo", .many) + self._LastSeen_MinutesAgo_other = getValueWithForm(dict, "LastSeen.MinutesAgo", .other) + self._LastSeen_HoursAgo_zero = getValueWithForm(dict, "LastSeen.HoursAgo", .zero) + self._LastSeen_HoursAgo_one = getValueWithForm(dict, "LastSeen.HoursAgo", .one) + self._LastSeen_HoursAgo_two = getValueWithForm(dict, "LastSeen.HoursAgo", .two) + self._LastSeen_HoursAgo_few = getValueWithForm(dict, "LastSeen.HoursAgo", .few) + self._LastSeen_HoursAgo_many = getValueWithForm(dict, "LastSeen.HoursAgo", .many) + self._LastSeen_HoursAgo_other = getValueWithForm(dict, "LastSeen.HoursAgo", .other) + self._MuteExpires_Days_zero = getValueWithForm(dict, "MuteExpires.Days", .zero) + self._MuteExpires_Days_one = getValueWithForm(dict, "MuteExpires.Days", .one) + self._MuteExpires_Days_two = getValueWithForm(dict, "MuteExpires.Days", .two) + self._MuteExpires_Days_few = getValueWithForm(dict, "MuteExpires.Days", .few) + self._MuteExpires_Days_many = getValueWithForm(dict, "MuteExpires.Days", .many) + self._MuteExpires_Days_other = getValueWithForm(dict, "MuteExpires.Days", .other) + self._MuteExpires_Hours_zero = getValueWithForm(dict, "MuteExpires.Hours", .zero) + self._MuteExpires_Hours_one = getValueWithForm(dict, "MuteExpires.Hours", .one) + self._MuteExpires_Hours_two = getValueWithForm(dict, "MuteExpires.Hours", .two) + self._MuteExpires_Hours_few = getValueWithForm(dict, "MuteExpires.Hours", .few) + self._MuteExpires_Hours_many = getValueWithForm(dict, "MuteExpires.Hours", .many) + self._MuteExpires_Hours_other = getValueWithForm(dict, "MuteExpires.Hours", .other) + self._Watch_LastSeen_HoursAgo_zero = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .zero) + self._Watch_LastSeen_HoursAgo_one = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .one) + self._Watch_LastSeen_HoursAgo_two = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .two) + self._Watch_LastSeen_HoursAgo_few = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .few) + self._Watch_LastSeen_HoursAgo_many = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .many) + self._Watch_LastSeen_HoursAgo_other = getValueWithForm(dict, "Watch.LastSeen.HoursAgo", .other) + self._Forward_ConfirmMultipleFiles_zero = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .zero) + self._Forward_ConfirmMultipleFiles_one = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .one) + self._Forward_ConfirmMultipleFiles_two = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .two) + self._Forward_ConfirmMultipleFiles_few = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .few) + self._Forward_ConfirmMultipleFiles_many = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .many) + self._Forward_ConfirmMultipleFiles_other = getValueWithForm(dict, "Forward.ConfirmMultipleFiles", .other) + self._AttachmentMenu_SendGif_zero = getValueWithForm(dict, "AttachmentMenu.SendGif", .zero) + self._AttachmentMenu_SendGif_one = getValueWithForm(dict, "AttachmentMenu.SendGif", .one) + self._AttachmentMenu_SendGif_two = getValueWithForm(dict, "AttachmentMenu.SendGif", .two) + self._AttachmentMenu_SendGif_few = getValueWithForm(dict, "AttachmentMenu.SendGif", .few) + self._AttachmentMenu_SendGif_many = getValueWithForm(dict, "AttachmentMenu.SendGif", .many) + self._AttachmentMenu_SendGif_other = getValueWithForm(dict, "AttachmentMenu.SendGif", .other) + self._StickerPack_RemoveStickerCount_zero = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .zero) + self._StickerPack_RemoveStickerCount_one = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .one) + self._StickerPack_RemoveStickerCount_two = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .two) + self._StickerPack_RemoveStickerCount_few = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .few) + self._StickerPack_RemoveStickerCount_many = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .many) + self._StickerPack_RemoveStickerCount_other = getValueWithForm(dict, "StickerPack.RemoveStickerCount", .other) + self._SharedMedia_Link_zero = getValueWithForm(dict, "SharedMedia.Link", .zero) + self._SharedMedia_Link_one = getValueWithForm(dict, "SharedMedia.Link", .one) + self._SharedMedia_Link_two = getValueWithForm(dict, "SharedMedia.Link", .two) + self._SharedMedia_Link_few = getValueWithForm(dict, "SharedMedia.Link", .few) + self._SharedMedia_Link_many = getValueWithForm(dict, "SharedMedia.Link", .many) + self._SharedMedia_Link_other = getValueWithForm(dict, "SharedMedia.Link", .other) + self._Map_ETAHours_zero = getValueWithForm(dict, "Map.ETAHours", .zero) + self._Map_ETAHours_one = getValueWithForm(dict, "Map.ETAHours", .one) + self._Map_ETAHours_two = getValueWithForm(dict, "Map.ETAHours", .two) + self._Map_ETAHours_few = getValueWithForm(dict, "Map.ETAHours", .few) + self._Map_ETAHours_many = getValueWithForm(dict, "Map.ETAHours", .many) + self._Map_ETAHours_other = getValueWithForm(dict, "Map.ETAHours", .other) + self._SharedMedia_DeleteItemsConfirmation_zero = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .zero) + self._SharedMedia_DeleteItemsConfirmation_one = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .one) + self._SharedMedia_DeleteItemsConfirmation_two = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .two) + self._SharedMedia_DeleteItemsConfirmation_few = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .few) + self._SharedMedia_DeleteItemsConfirmation_many = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .many) + self._SharedMedia_DeleteItemsConfirmation_other = getValueWithForm(dict, "SharedMedia.DeleteItemsConfirmation", .other) + self._Watch_LastSeen_MinutesAgo_zero = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .zero) + self._Watch_LastSeen_MinutesAgo_one = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .one) + self._Watch_LastSeen_MinutesAgo_two = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .two) + self._Watch_LastSeen_MinutesAgo_few = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .few) + self._Watch_LastSeen_MinutesAgo_many = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .many) + self._Watch_LastSeen_MinutesAgo_other = getValueWithForm(dict, "Watch.LastSeen.MinutesAgo", .other) + self._ForwardedMessages_zero = getValueWithForm(dict, "ForwardedMessages", .zero) + self._ForwardedMessages_one = getValueWithForm(dict, "ForwardedMessages", .one) + self._ForwardedMessages_two = getValueWithForm(dict, "ForwardedMessages", .two) + self._ForwardedMessages_few = getValueWithForm(dict, "ForwardedMessages", .few) + self._ForwardedMessages_many = getValueWithForm(dict, "ForwardedMessages", .many) + self._ForwardedMessages_other = getValueWithForm(dict, "ForwardedMessages", .other) + self._SharedMedia_ItemsSelected_zero = getValueWithForm(dict, "SharedMedia.ItemsSelected", .zero) + self._SharedMedia_ItemsSelected_one = getValueWithForm(dict, "SharedMedia.ItemsSelected", .one) + self._SharedMedia_ItemsSelected_two = getValueWithForm(dict, "SharedMedia.ItemsSelected", .two) + self._SharedMedia_ItemsSelected_few = getValueWithForm(dict, "SharedMedia.ItemsSelected", .few) + self._SharedMedia_ItemsSelected_many = getValueWithForm(dict, "SharedMedia.ItemsSelected", .many) + self._SharedMedia_ItemsSelected_other = getValueWithForm(dict, "SharedMedia.ItemsSelected", .other) + self._MessageTimer_Hours_zero = getValueWithForm(dict, "MessageTimer.Hours", .zero) + self._MessageTimer_Hours_one = getValueWithForm(dict, "MessageTimer.Hours", .one) + self._MessageTimer_Hours_two = getValueWithForm(dict, "MessageTimer.Hours", .two) + self._MessageTimer_Hours_few = getValueWithForm(dict, "MessageTimer.Hours", .few) + self._MessageTimer_Hours_many = getValueWithForm(dict, "MessageTimer.Hours", .many) + self._MessageTimer_Hours_other = getValueWithForm(dict, "MessageTimer.Hours", .other) + self._MessageTimer_Years_zero = getValueWithForm(dict, "MessageTimer.Years", .zero) + self._MessageTimer_Years_one = getValueWithForm(dict, "MessageTimer.Years", .one) + self._MessageTimer_Years_two = getValueWithForm(dict, "MessageTimer.Years", .two) + self._MessageTimer_Years_few = getValueWithForm(dict, "MessageTimer.Years", .few) + self._MessageTimer_Years_many = getValueWithForm(dict, "MessageTimer.Years", .many) + self._MessageTimer_Years_other = getValueWithForm(dict, "MessageTimer.Years", .other) + self._Map_ETAMinutes_zero = getValueWithForm(dict, "Map.ETAMinutes", .zero) + self._Map_ETAMinutes_one = getValueWithForm(dict, "Map.ETAMinutes", .one) + self._Map_ETAMinutes_two = getValueWithForm(dict, "Map.ETAMinutes", .two) + self._Map_ETAMinutes_few = getValueWithForm(dict, "Map.ETAMinutes", .few) + self._Map_ETAMinutes_many = getValueWithForm(dict, "Map.ETAMinutes", .many) + self._Map_ETAMinutes_other = getValueWithForm(dict, "Map.ETAMinutes", .other) + self._ForwardedVideos_zero = getValueWithForm(dict, "ForwardedVideos", .zero) + self._ForwardedVideos_one = getValueWithForm(dict, "ForwardedVideos", .one) + self._ForwardedVideos_two = getValueWithForm(dict, "ForwardedVideos", .two) + self._ForwardedVideos_few = getValueWithForm(dict, "ForwardedVideos", .few) + self._ForwardedVideos_many = getValueWithForm(dict, "ForwardedVideos", .many) + self._ForwardedVideos_other = getValueWithForm(dict, "ForwardedVideos", .other) + self._Notification_GameScoreSelfSimple_zero = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .zero) + self._Notification_GameScoreSelfSimple_one = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .one) + self._Notification_GameScoreSelfSimple_two = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .two) + self._Notification_GameScoreSelfSimple_few = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .few) + self._Notification_GameScoreSelfSimple_many = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .many) + self._Notification_GameScoreSelfSimple_other = getValueWithForm(dict, "Notification.GameScoreSelfSimple", .other) + self._ServiceMessage_GameScoreSimple_zero = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .zero) + self._ServiceMessage_GameScoreSimple_one = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .one) + self._ServiceMessage_GameScoreSimple_two = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .two) + self._ServiceMessage_GameScoreSimple_few = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .few) + self._ServiceMessage_GameScoreSimple_many = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .many) + self._ServiceMessage_GameScoreSimple_other = getValueWithForm(dict, "ServiceMessage.GameScoreSimple", .other) + self._QuickSend_Photos_zero = getValueWithForm(dict, "QuickSend.Photos", .zero) + self._QuickSend_Photos_one = getValueWithForm(dict, "QuickSend.Photos", .one) + self._QuickSend_Photos_two = getValueWithForm(dict, "QuickSend.Photos", .two) + self._QuickSend_Photos_few = getValueWithForm(dict, "QuickSend.Photos", .few) + self._QuickSend_Photos_many = getValueWithForm(dict, "QuickSend.Photos", .many) + self._QuickSend_Photos_other = getValueWithForm(dict, "QuickSend.Photos", .other) + self._MuteFor_Days_zero = getValueWithForm(dict, "MuteFor.Days", .zero) + self._MuteFor_Days_one = getValueWithForm(dict, "MuteFor.Days", .one) + self._MuteFor_Days_two = getValueWithForm(dict, "MuteFor.Days", .two) + self._MuteFor_Days_few = getValueWithForm(dict, "MuteFor.Days", .few) + self._MuteFor_Days_many = getValueWithForm(dict, "MuteFor.Days", .many) + self._MuteFor_Days_other = getValueWithForm(dict, "MuteFor.Days", .other) + self._Conversation_StatusOnline_zero = getValueWithForm(dict, "Conversation.StatusOnline", .zero) + self._Conversation_StatusOnline_one = getValueWithForm(dict, "Conversation.StatusOnline", .one) + self._Conversation_StatusOnline_two = getValueWithForm(dict, "Conversation.StatusOnline", .two) + self._Conversation_StatusOnline_few = getValueWithForm(dict, "Conversation.StatusOnline", .few) + self._Conversation_StatusOnline_many = getValueWithForm(dict, "Conversation.StatusOnline", .many) + self._Conversation_StatusOnline_other = getValueWithForm(dict, "Conversation.StatusOnline", .other) + self._AttachmentMenu_SendItem_zero = getValueWithForm(dict, "AttachmentMenu.SendItem", .zero) + self._AttachmentMenu_SendItem_one = getValueWithForm(dict, "AttachmentMenu.SendItem", .one) + self._AttachmentMenu_SendItem_two = getValueWithForm(dict, "AttachmentMenu.SendItem", .two) + self._AttachmentMenu_SendItem_few = getValueWithForm(dict, "AttachmentMenu.SendItem", .few) + self._AttachmentMenu_SendItem_many = getValueWithForm(dict, "AttachmentMenu.SendItem", .many) + self._AttachmentMenu_SendItem_other = getValueWithForm(dict, "AttachmentMenu.SendItem", .other) + self._Time_MonthOfYear_zero = getValueWithForm(dict, "Time.MonthOfYear", .zero) + self._Time_MonthOfYear_one = getValueWithForm(dict, "Time.MonthOfYear", .one) + self._Time_MonthOfYear_two = getValueWithForm(dict, "Time.MonthOfYear", .two) + self._Time_MonthOfYear_few = getValueWithForm(dict, "Time.MonthOfYear", .few) + self._Time_MonthOfYear_many = getValueWithForm(dict, "Time.MonthOfYear", .many) + self._Time_MonthOfYear_other = getValueWithForm(dict, "Time.MonthOfYear", .other) + self._Watch_UserInfo_Mute_zero = getValueWithForm(dict, "Watch.UserInfo.Mute", .zero) + self._Watch_UserInfo_Mute_one = getValueWithForm(dict, "Watch.UserInfo.Mute", .one) + self._Watch_UserInfo_Mute_two = getValueWithForm(dict, "Watch.UserInfo.Mute", .two) + self._Watch_UserInfo_Mute_few = getValueWithForm(dict, "Watch.UserInfo.Mute", .few) + self._Watch_UserInfo_Mute_many = getValueWithForm(dict, "Watch.UserInfo.Mute", .many) + self._Watch_UserInfo_Mute_other = getValueWithForm(dict, "Watch.UserInfo.Mute", .other) + self._StickerPack_MaskCount_zero = getValueWithForm(dict, "StickerPack.MaskCount", .zero) + self._StickerPack_MaskCount_one = getValueWithForm(dict, "StickerPack.MaskCount", .one) + self._StickerPack_MaskCount_two = getValueWithForm(dict, "StickerPack.MaskCount", .two) + self._StickerPack_MaskCount_few = getValueWithForm(dict, "StickerPack.MaskCount", .few) + self._StickerPack_MaskCount_many = getValueWithForm(dict, "StickerPack.MaskCount", .many) + self._StickerPack_MaskCount_other = getValueWithForm(dict, "StickerPack.MaskCount", .other) + self._Call_ShortMinutes_zero = getValueWithForm(dict, "Call.ShortMinutes", .zero) + self._Call_ShortMinutes_one = getValueWithForm(dict, "Call.ShortMinutes", .one) + self._Call_ShortMinutes_two = getValueWithForm(dict, "Call.ShortMinutes", .two) + self._Call_ShortMinutes_few = getValueWithForm(dict, "Call.ShortMinutes", .few) + self._Call_ShortMinutes_many = getValueWithForm(dict, "Call.ShortMinutes", .many) + self._Call_ShortMinutes_other = getValueWithForm(dict, "Call.ShortMinutes", .other) + self._StickerPack_RemoveMaskCount_zero = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .zero) + self._StickerPack_RemoveMaskCount_one = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .one) + self._StickerPack_RemoveMaskCount_two = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .two) + self._StickerPack_RemoveMaskCount_few = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .few) + self._StickerPack_RemoveMaskCount_many = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .many) + self._StickerPack_RemoveMaskCount_other = getValueWithForm(dict, "StickerPack.RemoveMaskCount", .other) + self._ForwardedLocations_zero = getValueWithForm(dict, "ForwardedLocations", .zero) + self._ForwardedLocations_one = getValueWithForm(dict, "ForwardedLocations", .one) + self._ForwardedLocations_two = getValueWithForm(dict, "ForwardedLocations", .two) + self._ForwardedLocations_few = getValueWithForm(dict, "ForwardedLocations", .few) + self._ForwardedLocations_many = getValueWithForm(dict, "ForwardedLocations", .many) + self._ForwardedLocations_other = getValueWithForm(dict, "ForwardedLocations", .other) + self._MessageTimer_ShortWeeks_zero = getValueWithForm(dict, "MessageTimer.ShortWeeks", .zero) + self._MessageTimer_ShortWeeks_one = getValueWithForm(dict, "MessageTimer.ShortWeeks", .one) + self._MessageTimer_ShortWeeks_two = getValueWithForm(dict, "MessageTimer.ShortWeeks", .two) + self._MessageTimer_ShortWeeks_few = getValueWithForm(dict, "MessageTimer.ShortWeeks", .few) + self._MessageTimer_ShortWeeks_many = getValueWithForm(dict, "MessageTimer.ShortWeeks", .many) + self._MessageTimer_ShortWeeks_other = getValueWithForm(dict, "MessageTimer.ShortWeeks", .other) + self._MessageTimer_Minutes_zero = getValueWithForm(dict, "MessageTimer.Minutes", .zero) + self._MessageTimer_Minutes_one = getValueWithForm(dict, "MessageTimer.Minutes", .one) + self._MessageTimer_Minutes_two = getValueWithForm(dict, "MessageTimer.Minutes", .two) + self._MessageTimer_Minutes_few = getValueWithForm(dict, "MessageTimer.Minutes", .few) + self._MessageTimer_Minutes_many = getValueWithForm(dict, "MessageTimer.Minutes", .many) + self._MessageTimer_Minutes_other = getValueWithForm(dict, "MessageTimer.Minutes", .other) + self._MessageTimer_Months_zero = getValueWithForm(dict, "MessageTimer.Months", .zero) + self._MessageTimer_Months_one = getValueWithForm(dict, "MessageTimer.Months", .one) + self._MessageTimer_Months_two = getValueWithForm(dict, "MessageTimer.Months", .two) + self._MessageTimer_Months_few = getValueWithForm(dict, "MessageTimer.Months", .few) + self._MessageTimer_Months_many = getValueWithForm(dict, "MessageTimer.Months", .many) + self._MessageTimer_Months_other = getValueWithForm(dict, "MessageTimer.Months", .other) + self._MessageTimer_Days_zero = getValueWithForm(dict, "MessageTimer.Days", .zero) + self._MessageTimer_Days_one = getValueWithForm(dict, "MessageTimer.Days", .one) + self._MessageTimer_Days_two = getValueWithForm(dict, "MessageTimer.Days", .two) + self._MessageTimer_Days_few = getValueWithForm(dict, "MessageTimer.Days", .few) + self._MessageTimer_Days_many = getValueWithForm(dict, "MessageTimer.Days", .many) + self._MessageTimer_Days_other = getValueWithForm(dict, "MessageTimer.Days", .other) + self._MuteExpires_Minutes_zero = getValueWithForm(dict, "MuteExpires.Minutes", .zero) + self._MuteExpires_Minutes_one = getValueWithForm(dict, "MuteExpires.Minutes", .one) + self._MuteExpires_Minutes_two = getValueWithForm(dict, "MuteExpires.Minutes", .two) + self._MuteExpires_Minutes_few = getValueWithForm(dict, "MuteExpires.Minutes", .few) + self._MuteExpires_Minutes_many = getValueWithForm(dict, "MuteExpires.Minutes", .many) + self._MuteExpires_Minutes_other = getValueWithForm(dict, "MuteExpires.Minutes", .other) + + } +} + diff --git a/TelegramUI/PresentationTheme.swift b/TelegramUI/PresentationTheme.swift new file mode 100644 index 0000000000..ae57ac3b64 --- /dev/null +++ b/TelegramUI/PresentationTheme.swift @@ -0,0 +1,971 @@ +import Foundation +import UIKit +import Display +import Postbox + +public enum PresentationThemeParsingError: Error { + case generic +} + +private func parseColor(_ decoder: Decoder, _ key: String) throws -> UIColor { + if let value = decoder.decodeOptionalInt32ForKey(key) { + return UIColor(argb: UInt32(bitPattern: value)) + } else { + throw PresentationThemeParsingError.generic + } +} + +public final class PresentationThemeRootTabBar { + public let backgroundColor: UIColor + public let separatorColor: UIColor + public let iconColor: UIColor + public let selectedIconColor: UIColor + public let textColor: UIColor + public let selectedTextColor: UIColor + public let badgeBackgroundColor: UIColor + public let badgeTextColor: UIColor + + public init(backgroundColor: UIColor, separatorColor: UIColor, iconColor: UIColor, selectedIconColor: UIColor, textColor: UIColor, selectedTextColor: UIColor, badgeBackgroundColor: UIColor, badgeTextColor: UIColor) { + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + self.iconColor = iconColor + self.selectedIconColor = selectedIconColor + self.textColor = textColor + self.selectedTextColor = selectedTextColor + self.badgeBackgroundColor = badgeBackgroundColor + self.badgeTextColor = badgeTextColor + } + + public init(decoder: Decoder) throws { + self.backgroundColor = try parseColor(decoder, "backgroundColor") + self.separatorColor = try parseColor(decoder, "separatorColor") + self.iconColor = try parseColor(decoder, "iconColor") + self.selectedIconColor = try parseColor(decoder, "selectedIconColor") + self.textColor = try parseColor(decoder, "textColor") + self.selectedTextColor = try parseColor(decoder, "selectedTextColor") + self.badgeBackgroundColor = try parseColor(decoder, "badgeBackgroundColor") + self.badgeTextColor = try parseColor(decoder, "badgeTextColor") + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public enum PresentationThemeStatusBarStyle: Int32 { + case black = 0 + case white = 1 + + var style: StatusBarStyle { + switch self { + case .black: + return .Black + case .white: + return .White + } + } +} + +public final class PresentationThemeRootNavigationStatusBar { + public let style: PresentationThemeStatusBarStyle + + public init(style: PresentationThemeStatusBarStyle) { + self.style = style + } + + public init(decoder: Decoder) throws { + if let styleValue = decoder.decodeOptionalInt32ForKey("style"), let style = PresentationThemeStatusBarStyle(rawValue: styleValue) { + self.style = style + } else { + throw PresentationThemeParsingError.generic + } + } + + public func encode(_ encoder: Encoder) { + encoder.encodeInt32(self.style.rawValue, forKey: "style") + } +} + +public final class PresentationThemeRootNavigationBar { + public let buttonColor: UIColor + public let primaryTextColor: UIColor + public let secondaryTextColor: UIColor + public let controlColor: UIColor + public let accentTextColor: UIColor + public let backgroundColor: UIColor + public let separatorColor: UIColor + + public init(buttonColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlColor: UIColor, accentTextColor: UIColor, backgroundColor: UIColor, separatorColor: UIColor) { + self.buttonColor = buttonColor + self.primaryTextColor = primaryTextColor + self.secondaryTextColor = secondaryTextColor + self.controlColor = controlColor + self.accentTextColor = accentTextColor + self.backgroundColor = backgroundColor + self.separatorColor = separatorColor + } + + public init(decoder: Decoder) throws { + self.buttonColor = try parseColor(decoder, "buttonColor") + self.primaryTextColor = try parseColor(decoder, "primaryTextColor") + self.secondaryTextColor = try parseColor(decoder, "secondaryTextColor") + self.controlColor = try parseColor(decoder, "controlColor") + self.accentTextColor = try parseColor(decoder, "accentTextColor") + self.backgroundColor = try parseColor(decoder, "backgroundColor") + self.separatorColor = try parseColor(decoder, "separatorColor") + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public final class PresentationThemeActiveNavigationSearchBar { + public let backgroundColor: UIColor + public let accentColor: UIColor + public let inputFillColor: UIColor + public let inputTextColor: UIColor + public let inputPlaceholderTextColor: UIColor + public let inputIconColor: UIColor + public let separatorColor: UIColor + + public init(backgroundColor: UIColor, accentColor: UIColor, inputFillColor: UIColor, inputTextColor: UIColor, inputPlaceholderTextColor: UIColor, inputIconColor: UIColor, separatorColor: UIColor) { + self.backgroundColor = backgroundColor + self.accentColor = accentColor + self.inputFillColor = inputFillColor + self.inputTextColor = inputTextColor + self.inputPlaceholderTextColor = inputPlaceholderTextColor + self.inputIconColor = inputIconColor + self.separatorColor = separatorColor + } + + public init(decoder: Decoder) throws { + self.backgroundColor = try parseColor(decoder, "backgroundColor") + self.accentColor = try parseColor(decoder, "accentColor") + self.inputFillColor = try parseColor(decoder, "inputFillColor") + self.inputTextColor = try parseColor(decoder, "inputTextColor") + self.inputPlaceholderTextColor = try parseColor(decoder, "inputPlaceholderTextColor") + self.inputIconColor = try parseColor(decoder, "inputIconColor") + self.separatorColor = try parseColor(decoder, "separatorColor") + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public final class PresentationThemeRootController { + public let statusBar: PresentationThemeRootNavigationStatusBar + public let tabBar: PresentationThemeRootTabBar + public let navigationBar: PresentationThemeRootNavigationBar + public let activeNavigationSearchBar: PresentationThemeActiveNavigationSearchBar + + public init(statusBar: PresentationThemeRootNavigationStatusBar, tabBar: PresentationThemeRootTabBar, navigationBar: PresentationThemeRootNavigationBar, activeNavigationSearchBar: PresentationThemeActiveNavigationSearchBar) { + self.statusBar = statusBar + self.tabBar = tabBar + self.navigationBar = navigationBar + self.activeNavigationSearchBar = activeNavigationSearchBar + } + + public init(decoder: Decoder) throws { + if let statusBar = (try? decoder.decodeObjectForKeyThrowing("statusBar", decoder: { try PresentationThemeRootNavigationStatusBar(decoder: $0) })) as? PresentationThemeRootNavigationStatusBar { + self.statusBar = statusBar + } else { + throw PresentationThemeParsingError.generic + } + if let tabBar = (try? decoder.decodeObjectForKeyThrowing("tabBar", decoder: { try PresentationThemeRootTabBar(decoder: $0) })) as? PresentationThemeRootTabBar { + self.tabBar = tabBar + } else { + throw PresentationThemeParsingError.generic + } + if let navigationBar = (try? decoder.decodeObjectForKeyThrowing("navigationBar", decoder: { try PresentationThemeRootNavigationBar(decoder: $0) })) as? PresentationThemeRootNavigationBar { + self.navigationBar = navigationBar + } else { + throw PresentationThemeParsingError.generic + } + if let activeNavigationSearchBar = (try? decoder.decodeObjectForKeyThrowing("activeNavigationSearchBar", decoder: { try PresentationThemeActiveNavigationSearchBar(decoder: $0) })) as? PresentationThemeActiveNavigationSearchBar { + self.activeNavigationSearchBar = activeNavigationSearchBar + } else { + throw PresentationThemeParsingError.generic + } + } + + public func encode(_ encoder: Encoder) { + encoder.encodeObjectWithEncoder(self.statusBar, encoder: { self.statusBar.encode($0) }, forKey: "statusBar") + encoder.encodeObjectWithEncoder(self.tabBar, encoder: { self.tabBar.encode($0) }, forKey: "tabBar") + encoder.encodeObjectWithEncoder(self.navigationBar, encoder: { self.navigationBar.encode($0) }, forKey: "navigationBar") + encoder.encodeObjectWithEncoder(self.activeNavigationSearchBar, encoder: { self.activeNavigationSearchBar.encode($0) }, forKey: "activeNavigationSearchBar") + } +} + +public final class PresentationThemeSwitch { + public let frameColor: UIColor + public let handleColor: UIColor + public let contentColor: UIColor + + public init(frameColor: UIColor, handleColor: UIColor, contentColor: UIColor) { + self.frameColor = frameColor + self.handleColor = handleColor + self.contentColor = contentColor + } + + public init(decoder: Decoder) throws { + self.frameColor = try parseColor(decoder, "frameColor") + self.handleColor = try parseColor(decoder, "handleColor") + self.contentColor = try parseColor(decoder, "contentColor") + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public final class PresentationThemeList { + public let blocksBackgroundColor: UIColor + public let plainBackgroundColor: UIColor + public let itemPrimaryTextColor: UIColor + public let itemSecondaryTextColor: UIColor + public let itemDisabledTextColor: UIColor + public let itemAccentColor: UIColor + public let itemDestructiveColor: UIColor + public let itemPlaceholderTextColor: UIColor + public let itemBackgroundColor: UIColor + public let itemHighlightedBackgroundColor: UIColor + public let itemSeparatorColor: UIColor + public let disclosureArrowColor: UIColor + public let sectionHeaderTextColor: UIColor + public let freeTextColor: UIColor + public let freeTextErrorColor: UIColor + public let freeTextSuccessColor: UIColor + public let itemSwitchColors: PresentationThemeSwitch + + public init(blocksBackgroundColor: UIColor, plainBackgroundColor: UIColor, itemPrimaryTextColor: UIColor, itemSecondaryTextColor: UIColor, itemDisabledTextColor: UIColor, itemAccentColor: UIColor, itemDestructiveColor: UIColor, itemPlaceholderTextColor: UIColor, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, itemSeparatorColor: UIColor, disclosureArrowColor: UIColor, sectionHeaderTextColor: UIColor, freeTextColor: UIColor, freeTextErrorColor: UIColor, freeTextSuccessColor: UIColor, itemSwitchColors: PresentationThemeSwitch) { + self.blocksBackgroundColor = blocksBackgroundColor + self.plainBackgroundColor = plainBackgroundColor + self.itemPrimaryTextColor = itemPrimaryTextColor + self.itemSecondaryTextColor = itemSecondaryTextColor + self.itemDisabledTextColor = itemDisabledTextColor + self.itemAccentColor = itemAccentColor + self.itemDestructiveColor = itemDestructiveColor + self.itemPlaceholderTextColor = itemPlaceholderTextColor + self.itemBackgroundColor = itemBackgroundColor + self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor + self.itemSeparatorColor = itemSeparatorColor + self.disclosureArrowColor = disclosureArrowColor + self.sectionHeaderTextColor = sectionHeaderTextColor + self.freeTextColor = freeTextColor + self.freeTextErrorColor = freeTextErrorColor + self.freeTextSuccessColor = freeTextSuccessColor + self.itemSwitchColors = itemSwitchColors + } + + public init(decoder: Decoder) throws { + self.blocksBackgroundColor = try parseColor(decoder, "blocksBackgroundColor") + self.plainBackgroundColor = try parseColor(decoder, "plainBackgroundColor") + self.itemPrimaryTextColor = try parseColor(decoder, "itemPrimaryTextColor") + self.itemSecondaryTextColor = try parseColor(decoder, "itemSecondaryTextColor") + self.itemDisabledTextColor = try parseColor(decoder, "itemDisabledTextColor") + self.itemAccentColor = try parseColor(decoder, "itemAccentColor") + self.itemDestructiveColor = try parseColor(decoder, "itemDestructiveColor") + self.itemPlaceholderTextColor = try parseColor(decoder, "itemPlaceholderTextColor") + self.itemBackgroundColor = try parseColor(decoder, "itemBackgroundColor") + self.itemHighlightedBackgroundColor = try parseColor(decoder, "itemHighlightedBackgroundColor") + self.itemSeparatorColor = try parseColor(decoder, "itemSeparatorColor") + self.disclosureArrowColor = try parseColor(decoder, "disclosureArrowColor") + self.sectionHeaderTextColor = try parseColor(decoder, "sectionHeaderTextColor") + self.freeTextColor = try parseColor(decoder, "freeTextColor") + self.freeTextErrorColor = try parseColor(decoder, "freeTextErrorColor") + self.freeTextSuccessColor = try parseColor(decoder, "freeTextSuccessColor") + if let itemSwitchColors = (try? decoder.decodeObjectForKeyThrowing("itemSwitchColors", decoder: { try PresentationThemeSwitch(decoder: $0) })) as? PresentationThemeSwitch { + self.itemSwitchColors = itemSwitchColors + } else { + throw PresentationThemeParsingError.generic + } + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else if let value = child.value as? PresentationThemeSwitch { + encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public final class PresentationThemeChatList { + public let backgroundColor: UIColor + public let itemSeparatorColor: UIColor + public let itemBackgroundColor: UIColor + public let pinnedItemBackgroundColor: UIColor + public let itemHighlightedBackgroundColor: UIColor + public let titleColor: UIColor + public let secretTitleColor: UIColor + public let dateTextColor: UIColor + public let authorNameColor: UIColor + public let messageTextColor: UIColor + public let messageDraftTextColor: UIColor + public let checkmarkColor: UIColor + public let pendingIndicatorColor: UIColor + public let unreadBadgeActiveBackgroundColor: UIColor + public let unreadBadgeActiveTextColor: UIColor + public let unreadBadgeInactiveBackgroundColor: UIColor + public let unreadBadgeInactiveTextColor: UIColor + public let pinnedSearchBarColor: UIColor + public let regularSearchBarColor: UIColor + public let sectionHeaderFillColor: UIColor + public let sectionHeaderTextColor: UIColor + public let searchBarKeyboardColor: PresentationThemeKeyboardColor + + init(backgroundColor: UIColor, itemSeparatorColor: UIColor, itemBackgroundColor: UIColor, pinnedItemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, titleColor: UIColor, secretTitleColor: UIColor, dateTextColor: UIColor, authorNameColor: UIColor, messageTextColor: UIColor, messageDraftTextColor: UIColor, checkmarkColor: UIColor, pendingIndicatorColor: UIColor, unreadBadgeActiveBackgroundColor: UIColor, unreadBadgeActiveTextColor: UIColor, unreadBadgeInactiveBackgroundColor: UIColor, unreadBadgeInactiveTextColor: UIColor, pinnedSearchBarColor: UIColor, regularSearchBarColor: UIColor, sectionHeaderFillColor: UIColor, sectionHeaderTextColor: UIColor, searchBarKeyboardColor: PresentationThemeKeyboardColor) { + self.backgroundColor = backgroundColor + self.itemSeparatorColor = itemSeparatorColor + self.itemBackgroundColor = itemBackgroundColor + self.pinnedItemBackgroundColor = pinnedItemBackgroundColor + self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor + self.titleColor = titleColor + self.secretTitleColor = secretTitleColor + self.dateTextColor = dateTextColor + self.authorNameColor = authorNameColor + self.messageTextColor = messageTextColor + self.messageDraftTextColor = messageDraftTextColor + self.checkmarkColor = checkmarkColor + self.pendingIndicatorColor = pendingIndicatorColor + self.unreadBadgeActiveBackgroundColor = unreadBadgeActiveBackgroundColor + self.unreadBadgeActiveTextColor = unreadBadgeActiveTextColor + self.unreadBadgeInactiveBackgroundColor = unreadBadgeInactiveBackgroundColor + self.unreadBadgeInactiveTextColor = unreadBadgeInactiveTextColor + self.pinnedSearchBarColor = pinnedSearchBarColor + self.regularSearchBarColor = regularSearchBarColor + self.sectionHeaderFillColor = sectionHeaderFillColor + self.sectionHeaderTextColor = sectionHeaderTextColor + self.searchBarKeyboardColor = searchBarKeyboardColor + } + + init(decoder: Decoder) throws { + self.backgroundColor = try parseColor(decoder, "backgroundColor") + self.itemSeparatorColor = try parseColor(decoder, "itemSeparatorColor") + self.itemBackgroundColor = try parseColor(decoder, "itemBackgroundColor") + self.pinnedItemBackgroundColor = try parseColor(decoder, "pinnedItemBackgroundColor") + self.itemHighlightedBackgroundColor = try parseColor(decoder, "itemHighlightedBackgroundColor") + self.titleColor = try parseColor(decoder, "titleColor") + self.secretTitleColor = try parseColor(decoder, "secretTitleColor") + self.dateTextColor = try parseColor(decoder, "dateTextColor") + self.authorNameColor = try parseColor(decoder, "authorNameColor") + self.messageTextColor = try parseColor(decoder, "messageTextColor") + self.messageDraftTextColor = try parseColor(decoder, "messageDraftTextColor") + self.checkmarkColor = try parseColor(decoder, "checkmarkColor") + self.pendingIndicatorColor = try parseColor(decoder, "pendingIndicatorColor") + self.unreadBadgeActiveBackgroundColor = try parseColor(decoder, "unreadBadgeActiveBackgroundColor") + self.unreadBadgeActiveTextColor = try parseColor(decoder, "unreadBadgeActiveTextColor") + self.unreadBadgeInactiveBackgroundColor = try parseColor(decoder, "unreadBadgeInactiveBackgroundColor") + self.unreadBadgeInactiveTextColor = try parseColor(decoder, "unreadBadgeInactiveTextColor") + self.pinnedSearchBarColor = try parseColor(decoder, "pinnedSearchBarColor") + self.regularSearchBarColor = try parseColor(decoder, "regularSearchBarColor") + self.sectionHeaderFillColor = try parseColor(decoder, "sectionHeaderFillColor") + self.sectionHeaderTextColor = try parseColor(decoder, "sectionHeaderTextColor") + if let value = decoder.decodeOptionalInt32ForKey("searchBarKeyboardColor"), let color = PresentationThemeKeyboardColor(rawValue: value) { + self.searchBarKeyboardColor = color + } else { + throw PresentationThemeParsingError.generic + } + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else if let value = child.value as? PresentationThemeSwitch { + encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) + } else if let value = child.value as? PresentationThemeKeyboardColor { + encoder.encodeInt32(value.rawValue, forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public final class PresentationThemeChatBubble { + public let incomingFillColor: UIColor + public let incomingFillHighlightedColor: UIColor + public let incomingStrokeColor: UIColor + + public let outgoingFillColor: UIColor + public let outgoingFillHighlightedColor: UIColor + public let outgoingStrokeColor: UIColor + + public let freeformFillColor: UIColor + public let freeformFillHighlightedColor: UIColor + public let freeformStrokeColor: UIColor + + public let infoFillColor: UIColor + public let infoStrokeColor: UIColor + + public let incomingPrimaryTextColor: UIColor + public let incomingSecondaryTextColor: UIColor + public let incomingLinkTextColor: UIColor + public let incomingLinkHighlightColor: UIColor + public let outgoingPrimaryTextColor: UIColor + public let outgoingSecondaryTextColor: UIColor + public let outgoingLinkTextColor: UIColor + public let outgoingLinkHighlightColor: UIColor + public let infoPrimaryTextColor: UIColor + public let infoLinkTextColor: UIColor + + public let incomingAccentColor: UIColor + public let outgoingAccentColor: UIColor + + public let outgoingCheckColor: UIColor + public let incomingPendingActivityColor: UIColor + public let outgoingPendingActivityColor: UIColor + + public let mediaDateAndStatusFillColor: UIColor + public let mediaDateAndStatusTextColor: UIColor + + public let incomingFileTitleColor: UIColor + public let outgoingFileTitleColor: UIColor + public let incomingFileDescriptionColor: UIColor + public let outgoingFileDescriptionColor: UIColor + public let incomingFileDurationColor: UIColor + public let outgoingFileDurationColor: UIColor + + public let shareButtonFillColor: UIColor + public let shareButtonForegroundColor: UIColor + + public let mediaOverlayControlBackgroundColor: UIColor + public let mediaOverlayControlForegroundColor: UIColor + + public let actionButtonsFillColor: UIColor + public let actionButtonsTextColor: UIColor + + public init(incomingFillColor: UIColor, incomingFillHighlightedColor: UIColor, incomingStrokeColor: UIColor, outgoingFillColor: UIColor, outgoingFillHighlightedColor: UIColor, outgoingStrokeColor: UIColor, freeformFillColor: UIColor, freeformFillHighlightedColor: UIColor, freeformStrokeColor: UIColor, infoFillColor: UIColor, infoStrokeColor: UIColor, incomingPrimaryTextColor: UIColor, incomingSecondaryTextColor: UIColor, incomingLinkTextColor: UIColor, incomingLinkHighlightColor: UIColor, outgoingPrimaryTextColor: UIColor, outgoingSecondaryTextColor: UIColor, outgoingLinkTextColor: UIColor, outgoingLinkHighlightColor: UIColor, infoPrimaryTextColor: UIColor, infoLinkTextColor: UIColor, incomingAccentColor: UIColor, outgoingAccentColor: UIColor, outgoingCheckColor: UIColor, incomingPendingActivityColor: UIColor, outgoingPendingActivityColor: UIColor, mediaDateAndStatusFillColor: UIColor, mediaDateAndStatusTextColor: UIColor, incomingFileTitleColor: UIColor, outgoingFileTitleColor: UIColor, incomingFileDescriptionColor: UIColor, outgoingFileDescriptionColor: UIColor, incomingFileDurationColor: UIColor, outgoingFileDurationColor: UIColor, shareButtonFillColor: UIColor, shareButtonForegroundColor: UIColor, mediaOverlayControlBackgroundColor: UIColor, mediaOverlayControlForegroundColor: UIColor, actionButtonsFillColor: UIColor, actionButtonsTextColor: UIColor) { + self.incomingFillColor = incomingFillColor + self.incomingFillHighlightedColor = incomingFillHighlightedColor + self.incomingStrokeColor = incomingStrokeColor + self.outgoingFillColor = outgoingFillColor + self.outgoingFillHighlightedColor = outgoingFillHighlightedColor + self.outgoingStrokeColor = outgoingStrokeColor + self.freeformFillColor = freeformFillColor + self.freeformFillHighlightedColor = freeformFillHighlightedColor + self.freeformStrokeColor = freeformStrokeColor + self.infoFillColor = infoFillColor + self.infoStrokeColor = infoStrokeColor + + self.incomingPrimaryTextColor = incomingPrimaryTextColor + self.incomingSecondaryTextColor = incomingSecondaryTextColor + self.incomingLinkTextColor = incomingLinkTextColor + self.incomingLinkHighlightColor = incomingLinkHighlightColor + self.outgoingPrimaryTextColor = outgoingPrimaryTextColor + self.outgoingSecondaryTextColor = outgoingSecondaryTextColor + self.outgoingLinkTextColor = outgoingLinkTextColor + self.outgoingLinkHighlightColor = outgoingLinkHighlightColor + self.infoPrimaryTextColor = infoPrimaryTextColor + self.infoLinkTextColor = infoLinkTextColor + + self.incomingAccentColor = incomingAccentColor + self.outgoingAccentColor = outgoingAccentColor + + self.outgoingCheckColor = outgoingCheckColor + self.incomingPendingActivityColor = incomingPendingActivityColor + self.outgoingPendingActivityColor = outgoingPendingActivityColor + self.mediaDateAndStatusFillColor = mediaDateAndStatusFillColor + self.mediaDateAndStatusTextColor = mediaDateAndStatusTextColor + + self.incomingFileTitleColor = incomingFileTitleColor + self.outgoingFileTitleColor = outgoingFileTitleColor + self.incomingFileDescriptionColor = incomingFileDescriptionColor + self.outgoingFileDescriptionColor = outgoingFileDescriptionColor + self.incomingFileDurationColor = incomingFileDurationColor + self.outgoingFileDurationColor = outgoingFileDurationColor + + self.shareButtonFillColor = shareButtonFillColor + self.shareButtonForegroundColor = shareButtonForegroundColor + + self.mediaOverlayControlBackgroundColor = mediaOverlayControlBackgroundColor + self.mediaOverlayControlForegroundColor = mediaOverlayControlForegroundColor + + self.actionButtonsFillColor = actionButtonsFillColor + self.actionButtonsTextColor = actionButtonsTextColor + } + + public init(decoder: Decoder) throws { + self.incomingFillColor = try parseColor(decoder, "incomingFillColor") + self.incomingFillHighlightedColor = try parseColor(decoder, "incomingFillHighlightedColor") + self.incomingStrokeColor = try parseColor(decoder, "incomingStrokeColor") + self.outgoingFillColor = try parseColor(decoder, "outgoingFillColor") + self.outgoingFillHighlightedColor = try parseColor(decoder, "outgoingFillHighlightedColor") + self.outgoingStrokeColor = try parseColor(decoder, "outgoingStrokeColor") + self.freeformFillColor = try parseColor(decoder, "freeformFillColor") + self.freeformFillHighlightedColor = try parseColor(decoder, "freeformFillHighlightedColor") + self.freeformStrokeColor = try parseColor(decoder, "freeformStrokeColor") + self.infoFillColor = try parseColor(decoder, "infoFillColor") + self.infoStrokeColor = try parseColor(decoder, "infoStrokeColor") + + self.incomingPrimaryTextColor = try parseColor(decoder, "incomingPrimaryTextColor") + self.incomingSecondaryTextColor = try parseColor(decoder, "incomingSecondaryTextColor") + self.incomingLinkTextColor = try parseColor(decoder, "incomingLinkTextColor") + self.incomingLinkHighlightColor = try parseColor(decoder, "incomingLinkHighlightColor") + self.outgoingPrimaryTextColor = try parseColor(decoder, "outgoingPrimaryTextColor") + self.outgoingSecondaryTextColor = try parseColor(decoder, "outgoingSecondaryTextColor") + self.outgoingLinkTextColor = try parseColor(decoder, "outgoingLinkTextColor") + self.outgoingLinkHighlightColor = try parseColor(decoder, "outgoingLinkhighlightColor") + self.infoPrimaryTextColor = try parseColor(decoder, "infoPrimaryTextColor") + self.infoLinkTextColor = try parseColor(decoder, "infoLinkTextColor") + + self.incomingAccentColor = try parseColor(decoder, "incomingAccentColor") + self.outgoingAccentColor = try parseColor(decoder, "outgoingAccentColor") + + self.outgoingCheckColor = try parseColor(decoder, "outgoingCheckColor") + self.incomingPendingActivityColor = try parseColor(decoder, "incomingPendingActivityColor") + self.outgoingPendingActivityColor = try parseColor(decoder, "outgoingPendingActivityColor") + self.mediaDateAndStatusFillColor = try parseColor(decoder, "mediaDateAndStatusFillColor") + self.mediaDateAndStatusTextColor = try parseColor(decoder, "mediaDateAndStatusTextColor") + + self.incomingFileTitleColor = try parseColor(decoder, "incomingFileTitleColor") + self.outgoingFileTitleColor = try parseColor(decoder, "outgoingFileTitleColor") + self.incomingFileDescriptionColor = try parseColor(decoder, "incomingFileDescriptionColor") + self.outgoingFileDescriptionColor = try parseColor(decoder, "outgoingFileDescriptionColor") + self.incomingFileDurationColor = try parseColor(decoder, "incomingFileDurationColor") + self.outgoingFileDurationColor = try parseColor(decoder, "outgoingFileDurationColor") + + self.shareButtonFillColor = try parseColor(decoder, "shareButtonFillColor") + self.shareButtonForegroundColor = try parseColor(decoder, "shareButtonForegroundColor") + + self.mediaOverlayControlBackgroundColor = try parseColor(decoder, "mediaOverlayControlBackgroundColor") + self.mediaOverlayControlForegroundColor = try parseColor(decoder, "mediaOverlayControlForegroundColor") + + self.actionButtonsFillColor = try parseColor(decoder, "actionButtonsFillColor") + self.actionButtonsTextColor = try parseColor(decoder, "actionButtonsTextColor") + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else if let value = child.value as? PresentationThemeSwitch { + encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public final class PresentationThemeServiceMessage { + public let serviceMessageFillColor: UIColor + public let serviceMessagePrimaryTextColor: UIColor + + public let unreadBarFillColor: UIColor + public let unreadBarStrokeColor: UIColor + public let unreadBarTextColor: UIColor + + public let dateFillStaticColor: UIColor + public let dateFillFloatingColor: UIColor + public let dateTextColor: UIColor + + public init(serviceMessageFillColor: UIColor, serviceMessagePrimaryTextColor: UIColor, unreadBarFillColor: UIColor, unreadBarStrokeColor: UIColor, unreadBarTextColor: UIColor, dateFillStaticColor: UIColor, dateFillFloatingColor: UIColor, dateTextColor: UIColor) { + self.serviceMessageFillColor = serviceMessageFillColor + self.serviceMessagePrimaryTextColor = serviceMessagePrimaryTextColor + self.unreadBarFillColor = unreadBarFillColor + self.unreadBarStrokeColor = unreadBarStrokeColor + self.unreadBarTextColor = unreadBarTextColor + self.dateFillStaticColor = dateFillStaticColor + self.dateFillFloatingColor = dateFillFloatingColor + self.dateTextColor = dateTextColor + } + + public init(decoder: Decoder) throws { + self.serviceMessageFillColor = try parseColor(decoder, "serviceMessageFillColor") + self.serviceMessagePrimaryTextColor = try parseColor(decoder, "serviceMessagePrimaryTextColor") + self.unreadBarFillColor = try parseColor(decoder, "unreadBarFillColor") + self.unreadBarStrokeColor = try parseColor(decoder, "unreadBarStrokeColor") + self.unreadBarTextColor = try parseColor(decoder, "unreadBarTextColor") + self.dateFillStaticColor = try parseColor(decoder, "dateFillStaticColor") + self.dateFillFloatingColor = try parseColor(decoder, "dateFillFloatingColor") + self.dateTextColor = try parseColor(decoder, "dateTextColor") + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else if let value = child.value as? PresentationThemeSwitch { + encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public enum PresentationThemeKeyboardColor: Int32 { + case light = 0 + case dark = 1 + + public var keyboardAppearance: UIKeyboardAppearance { + switch self { + case .light: + return .default + case .dark: + return .dark + } + } +} + +public final class PresentationThemeChatInputPanel { + public let panelBackgroundColor: UIColor + public let panelStrokeColor: UIColor + public let panelControlAccentColor: UIColor + public let panelControlColor: UIColor + public let panelControlDisabledColor: UIColor + public let panelControlDestructiveColor: UIColor + public let inputBackgroundColor: UIColor + public let inputStrokeColor: UIColor + public let inputPlaceholderColor: UIColor + public let inputTextColor: UIColor + public let inputControlColor: UIColor + public let primaryTextColor: UIColor + public let mediaRecordingDotColor: UIColor + public let keyboardColor: PresentationThemeKeyboardColor + + public init(panelBackgroundColor: UIColor, panelStrokeColor: UIColor, panelControlAccentColor: UIColor, panelControlColor: UIColor, panelControlDisabledColor: UIColor, panelControlDestructiveColor: UIColor, inputBackgroundColor: UIColor, inputStrokeColor: UIColor, inputPlaceholderColor: UIColor, inputTextColor: UIColor, inputControlColor: UIColor, primaryTextColor: UIColor, mediaRecordingDotColor: UIColor, keyboardColor: PresentationThemeKeyboardColor) { + self.panelBackgroundColor = panelBackgroundColor + self.panelStrokeColor = panelStrokeColor + self.panelControlAccentColor = panelControlAccentColor + self.panelControlColor = panelControlColor + self.panelControlDisabledColor = panelControlDisabledColor + self.panelControlDestructiveColor = panelControlDestructiveColor + self.inputBackgroundColor = inputBackgroundColor + self.inputStrokeColor = inputStrokeColor + self.inputPlaceholderColor = inputPlaceholderColor + self.inputTextColor = inputTextColor + self.inputControlColor = inputControlColor + self.primaryTextColor = primaryTextColor + self.mediaRecordingDotColor = mediaRecordingDotColor + self.keyboardColor = keyboardColor + } + + public init(decoder: Decoder) throws { + self.panelBackgroundColor = try parseColor(decoder, "panelBackgroundColor") + self.panelStrokeColor = try parseColor(decoder, "panelStrokeColor") + self.panelControlAccentColor = try parseColor(decoder, "panelControlAccentColor") + self.panelControlColor = try parseColor(decoder, "panelControlColor") + self.panelControlDisabledColor = try parseColor(decoder, "panelControlDisabledColor") + self.panelControlDestructiveColor = try parseColor(decoder, "panelControlDestructiveColor") + self.inputBackgroundColor = try parseColor(decoder, "inputBackgroundColor") + self.inputStrokeColor = try parseColor(decoder, "inputStrokeColor") + self.inputPlaceholderColor = try parseColor(decoder, "inputPlaceholderColor") + self.inputTextColor = try parseColor(decoder, "inputTextColor") + self.inputControlColor = try parseColor(decoder, "inputControlColor") + self.primaryTextColor = try parseColor(decoder, "primaryTextColor") + self.mediaRecordingDotColor = try parseColor(decoder, "mediaRecordingDotColor") + if let value = decoder.decodeOptionalInt32ForKey("keyboardColor"), let color = PresentationThemeKeyboardColor(rawValue: value) { + self.keyboardColor = color + } else { + throw PresentationThemeParsingError.generic + } + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else if let value = child.value as? PresentationThemeSwitch { + encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) + } else if let value = child.value as? PresentationThemeKeyboardColor { + encoder.encodeInt32(value.rawValue, forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public final class PresentationThemeInputMediaPanel { + public let panelSerapatorColor: UIColor + public let panelIconColor: UIColor + public let panelHighlightedIconBackgroundColor: UIColor + public let stickersBackgroundColor: UIColor + public let stickersSectionTextColor: UIColor + public let gifsBackgroundColor: UIColor + + public init(panelSerapatorColor: UIColor, panelIconColor: UIColor, panelHighlightedIconBackgroundColor: UIColor, stickersBackgroundColor: UIColor, stickersSectionTextColor: UIColor, gifsBackgroundColor: UIColor) { + self.panelSerapatorColor = panelSerapatorColor + self.panelIconColor = panelIconColor + self.panelHighlightedIconBackgroundColor = panelHighlightedIconBackgroundColor + self.stickersBackgroundColor = stickersBackgroundColor + self.stickersSectionTextColor = stickersSectionTextColor + self.gifsBackgroundColor = gifsBackgroundColor + } + + public init(decoder: Decoder) throws { + self.panelSerapatorColor = try parseColor(decoder, "panelSerapatorColor") + self.panelIconColor = try parseColor(decoder, "panelIconColor") + self.panelHighlightedIconBackgroundColor = try parseColor(decoder, "panelHighlightedIconBackgroundColor") + self.stickersBackgroundColor = try parseColor(decoder, "stickersBackgroundColor") + self.stickersSectionTextColor = try parseColor(decoder, "stickersSectionTextColor") + self.gifsBackgroundColor = try parseColor(decoder, "gifsBackgroundColor") + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else if let value = child.value as? PresentationThemeSwitch { + encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public final class PresentationThemeInputButtonPanel { + public let panelSerapatorColor: UIColor + public let panelBackgroundColor: UIColor + public let buttonFillColor: UIColor + public let buttonStrokeColor: UIColor + public let buttonHighlightedFillColor: UIColor + public let buttonHighlightedStrokeColor: UIColor + public let buttonTextColor: UIColor + + public init(panelSerapatorColor: UIColor, panelBackgroundColor: UIColor, buttonFillColor: UIColor, buttonStrokeColor: UIColor, buttonHighlightedFillColor: UIColor, buttonHighlightedStrokeColor: UIColor, buttonTextColor: UIColor) { + self.panelSerapatorColor = panelSerapatorColor + self.panelBackgroundColor = panelBackgroundColor + self.buttonFillColor = buttonFillColor + self.buttonStrokeColor = buttonStrokeColor + self.buttonHighlightedFillColor = buttonHighlightedFillColor + self.buttonHighlightedStrokeColor = buttonHighlightedStrokeColor + self.buttonTextColor = buttonTextColor + } + + public init(decoder: Decoder) throws { + self.panelSerapatorColor = try parseColor(decoder, "panelSerapatorColor") + self.panelBackgroundColor = try parseColor(decoder, "panelBackgroundColor") + self.buttonFillColor = try parseColor(decoder, "buttonFillColor") + self.buttonStrokeColor = try parseColor(decoder, "buttonStrokeColor") + self.buttonHighlightedFillColor = try parseColor(decoder, "buttonHighlightedFillColor") + self.buttonHighlightedStrokeColor = try parseColor(decoder, "buttonHighlightedStrokeColor") + self.buttonTextColor = try parseColor(decoder, "buttonTextColor") + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else if let value = child.value as? PresentationThemeSwitch { + encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public final class PresentationThemeChatHistoryNavigation { + public let fillColor: UIColor + public let strokeColor: UIColor + public let foregroundColor: UIColor + public let badgeBackgroundColor: UIColor + public let badgeTextColor: UIColor + + public init(fillColor: UIColor, strokeColor: UIColor, foregroundColor: UIColor, badgeBackgroundColor: UIColor, badgeTextColor: UIColor) { + self.fillColor = fillColor + self.strokeColor = strokeColor + self.foregroundColor = foregroundColor + self.badgeBackgroundColor = badgeBackgroundColor + self.badgeTextColor = badgeTextColor + } + + public init(decoder: Decoder) throws { + self.fillColor = try parseColor(decoder, "fillColor") + self.strokeColor = try parseColor(decoder, "strokeColor") + self.foregroundColor = try parseColor(decoder, "foregroundColor") + self.badgeBackgroundColor = try parseColor(decoder, "badgeBackgroundColor") + self.badgeTextColor = try parseColor(decoder, "badgeTextColor") + } + + public func encode(_ encoder: Encoder) { + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? UIColor { + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else if let value = child.value as? PresentationThemeSwitch { + encoder.encodeObjectWithEncoder(value, encoder: { value.encode($0) }, forKey: label) + } else if let value = child.value as? PresentationThemeKeyboardColor { + encoder.encodeInt32(value.rawValue, forKey: label) + } else { + assertionFailure() + } + } + } + } +} + +public final class PresentationThemeChat { + public let bubble: PresentationThemeChatBubble + public let serviceMessage: PresentationThemeServiceMessage + public let inputPanel: PresentationThemeChatInputPanel + public let inputMediaPanel: PresentationThemeInputMediaPanel + public let inputButtonPanel: PresentationThemeInputButtonPanel + public let historyNavigation: PresentationThemeChatHistoryNavigation + + public init(bubble: PresentationThemeChatBubble, serviceMessage: PresentationThemeServiceMessage, inputPanel: PresentationThemeChatInputPanel, inputMediaPanel: PresentationThemeInputMediaPanel, inputButtonPanel: PresentationThemeInputButtonPanel, historyNavigation: PresentationThemeChatHistoryNavigation) { + self.bubble = bubble + self.serviceMessage = serviceMessage + self.inputPanel = inputPanel + self.inputMediaPanel = inputMediaPanel + self.inputButtonPanel = inputButtonPanel + self.historyNavigation = historyNavigation + } + + public init(decoder: Decoder) throws { + if let bubble = (try? decoder.decodeObjectForKeyThrowing("bubble", decoder: { try PresentationThemeChatBubble(decoder: $0) })) as? PresentationThemeChatBubble { + self.bubble = bubble + } else { + throw PresentationThemeParsingError.generic + } + if let serviceMessage = (try? decoder.decodeObjectForKeyThrowing("serviceMessage", decoder: { try PresentationThemeServiceMessage(decoder: $0) })) as? PresentationThemeServiceMessage { + self.serviceMessage = serviceMessage + } else { + throw PresentationThemeParsingError.generic + } + if let inputPanel = (try? decoder.decodeObjectForKeyThrowing("inputPanel", decoder: { try PresentationThemeChatInputPanel(decoder: $0) })) as? PresentationThemeChatInputPanel { + self.inputPanel = inputPanel + } else { + throw PresentationThemeParsingError.generic + } + if let inputMediaPanel = (try? decoder.decodeObjectForKeyThrowing("inputMediaPanel", decoder: { try PresentationThemeInputMediaPanel(decoder: $0) })) as? PresentationThemeInputMediaPanel { + self.inputMediaPanel = inputMediaPanel + } else { + throw PresentationThemeParsingError.generic + } + if let inputButtonPanel = (try? decoder.decodeObjectForKeyThrowing("inputButtonPanel", decoder: { try PresentationThemeInputButtonPanel(decoder: $0) })) as? PresentationThemeInputButtonPanel { + self.inputButtonPanel = inputButtonPanel + } else { + throw PresentationThemeParsingError.generic + } + if let historyNavigation = (try? decoder.decodeObjectForKeyThrowing("historyNavigation", decoder: { try PresentationThemeChatHistoryNavigation(decoder: $0) })) as? PresentationThemeChatHistoryNavigation { + self.historyNavigation = historyNavigation + } else { + throw PresentationThemeParsingError.generic + } + } + + public func encode(_ encoder: Encoder) { + encoder.encodeObjectWithEncoder(self.bubble, encoder: { self.bubble.encode($0) }, forKey: "bubble") + encoder.encodeObjectWithEncoder(self.serviceMessage, encoder: { self.serviceMessage.encode($0) }, forKey: "serviceMessage") + encoder.encodeObjectWithEncoder(self.inputPanel, encoder: { self.inputPanel.encode($0) }, forKey: "inputPanel") + encoder.encodeObjectWithEncoder(self.inputMediaPanel, encoder: { self.inputMediaPanel.encode($0) }, forKey: "inputMediaPanel") + encoder.encodeObjectWithEncoder(self.inputButtonPanel, encoder: { self.inputButtonPanel.encode($0) }, forKey: "inputButtonPanel") + encoder.encodeObjectWithEncoder(self.historyNavigation, encoder: { self.historyNavigation.encode($0) }, forKey: "historyNavigation") + } +} + +public final class PresentationTheme: Equatable { + public let rootController: PresentationThemeRootController + public let list: PresentationThemeList + public let chatList: PresentationThemeChatList + public let chat: PresentationThemeChat + + public let resourceCache: PresentationsResourceCache = PresentationsResourceCache() + + public init(rootController: PresentationThemeRootController, list: PresentationThemeList, chatList: PresentationThemeChatList, chat: PresentationThemeChat) { + self.rootController = rootController + self.list = list + self.chatList = chatList + self.chat = chat + } + + public init(decoder: Decoder) throws { + if let rootController = (try? decoder.decodeObjectForKeyThrowing("rootController", decoder: { try PresentationThemeRootController(decoder: $0) })) as? PresentationThemeRootController { + self.rootController = rootController + } else { + throw PresentationThemeParsingError.generic + } + if let list = (try? decoder.decodeObjectForKeyThrowing("list", decoder: { try PresentationThemeList(decoder: $0) })) as? PresentationThemeList { + self.list = list + } else { + throw PresentationThemeParsingError.generic + } + if let chatList = (try? decoder.decodeObjectForKeyThrowing("chatList", decoder: { try PresentationThemeChatList(decoder: $0) })) as? PresentationThemeChatList { + self.chatList = chatList + } else { + throw PresentationThemeParsingError.generic + } + if let chat = (try? decoder.decodeObjectForKeyThrowing("chat", decoder: { try PresentationThemeChat(decoder: $0) })) as? PresentationThemeChat { + self.chat = chat + } else { + throw PresentationThemeParsingError.generic + } + } + + public func encode(_ encoder: Encoder) { + encoder.encodeObjectWithEncoder(self.rootController, encoder: { self.rootController.encode($0) }, forKey: "list") + encoder.encodeObjectWithEncoder(self.list, encoder: { self.list.encode($0) }, forKey: "list") + encoder.encodeObjectWithEncoder(self.chatList, encoder: { self.chatList.encode($0) }, forKey: "chatList") + encoder.encodeObjectWithEncoder(self.chat, encoder: { self.chat.encode($0) }, forKey: "chat") + } + + public static func ==(lhs: PresentationTheme, rhs: PresentationTheme) -> Bool { + return lhs === rhs + } + + public func image(_ key: Int32, _ generate: (PresentationTheme) -> UIImage?) -> UIImage? { + return self.resourceCache.image(key, self, generate) + } + + public func object(_ key: Int32, _ generate: (PresentationTheme) -> AnyObject?) -> AnyObject? { + return self.resourceCache.object(key, self, generate) + } +} diff --git a/TelegramUI/PresentationThemeEssentialGraphics.swift b/TelegramUI/PresentationThemeEssentialGraphics.swift new file mode 100644 index 0000000000..6c01d1c884 --- /dev/null +++ b/TelegramUI/PresentationThemeEssentialGraphics.swift @@ -0,0 +1,140 @@ +import Foundation +import UIKit +import Display + +private func generateCheckImage(partial: Bool, color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 11.0, height: 9.0), contextGenerator: { size, context in + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context.clear(CGRect(origin: CGPoint(), size: size)) + context.scaleBy(x: 0.5, y: 0.5) + context.setStrokeColor(color.cgColor) + context.setLineWidth(2.5) + if partial { + let _ = try? drawSvgPath(context, path: "M1,14.5 L2.5,16 L16.4985125,1 ") + } else { + let _ = try? drawSvgPath(context, path: "M1,10 L7,16 L20.9985125,1 ") + } + context.strokePath() + }) +} + +private func generateClockFrameImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(color.cgColor) + context.setFillColor(color.cgColor) + let strokeWidth: CGFloat = 1.0 + context.setLineWidth(strokeWidth) + context.strokeEllipse(in: CGRect(x: strokeWidth / 2.0, y: strokeWidth / 2.0, width: size.width - strokeWidth, height: size.height - strokeWidth)) + context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: strokeWidth * 3.0, width: strokeWidth, height: 11.0 / 2.0 - strokeWidth * 3.0)) + }) +} + +private func generateClockMinImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 11.0, height: 11.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + let strokeWidth: CGFloat = 1.0 + context.fill(CGRect(x: (11.0 - strokeWidth) / 2.0, y: (11.0 - strokeWidth) / 2.0, width: 11.0 / 2.0 - strokeWidth, height: strokeWidth)) + }) +} + +public final class PrincipalThemeEssentialGraphics { + public let chatMessageBackgroundIncomingImage: UIImage + public let chatMessageBackgroundIncomingHighlightedImage: UIImage + public let chatMessageBackgroundIncomingMergedTopImage: UIImage + public let chatMessageBackgroundIncomingMergedTopHighlightedImage: UIImage + public let chatMessageBackgroundIncomingMergedBottomImage: UIImage + public let chatMessageBackgroundIncomingMergedBottomHighlightedImage: UIImage + public let chatMessageBackgroundIncomingMergedBothImage: UIImage + public let chatMessageBackgroundIncomingMergedBothHighlightedImage: UIImage + + public let chatMessageBackgroundOutgoingImage: UIImage + public let chatMessageBackgroundOutgoingHighlightedImage: UIImage + public let chatMessageBackgroundOutgoingMergedTopImage: UIImage + public let chatMessageBackgroundOutgoingMergedTopHighlightedImage: UIImage + public let chatMessageBackgroundOutgoingMergedBottomImage: UIImage + public let chatMessageBackgroundOutgoingMergedBottomHighlightedImage: UIImage + public let chatMessageBackgroundOutgoingMergedBothImage: UIImage + public let chatMessageBackgroundOutgoingMergedBothHighlightedImage: UIImage + + public let checkBubbleFullImage: UIImage + public let checkBubblePartialImage: UIImage + + public let checkMediaFullImage: UIImage + public let checkMediaPartialImage: UIImage + + public let clockBubbleIncomingFrameImage: UIImage + public let clockBubbleIncomingMinImage: UIImage + public let clockBubbleOutgoingFrameImage: UIImage + public let clockBubbleOutgoingMinImage: UIImage + public let clockMediaFrameImage: UIImage + public let clockMediaMinImage: UIImage + + public let dateAndStatusMediaBackground: UIImage + public let dateAndStatusFreeBackground: UIImage + public let incomingDateAndStatusImpressionIcon: UIImage + public let outgoingDateAndStatusImpressionIcon: UIImage + public let mediaImpressionIcon: UIImage + + public let dateStaticBackground: UIImage + public let dateFloatingBackground: UIImage + + init(_ theme: PresentationThemeChat) { + let bubble = theme.bubble + self.chatMessageBackgroundIncomingImage = messageBubbleImage(incoming: true, fillColor: bubble.incomingFillColor, strokeColor: bubble.incomingStrokeColor, neighbors: .none) + self.chatMessageBackgroundIncomingHighlightedImage = messageBubbleImage(incoming: true, fillColor: bubble.incomingFillHighlightedColor, strokeColor: bubble.incomingStrokeColor, neighbors: .none) + self.chatMessageBackgroundIncomingMergedTopImage = messageBubbleImage(incoming: true, fillColor: bubble.incomingFillColor, strokeColor: bubble.incomingStrokeColor, neighbors: .top) + self.chatMessageBackgroundIncomingMergedTopHighlightedImage = messageBubbleImage(incoming: true, fillColor: bubble.incomingFillHighlightedColor, strokeColor: bubble.incomingStrokeColor, neighbors: .top) + self.chatMessageBackgroundIncomingMergedBottomImage = messageBubbleImage(incoming: true, fillColor: bubble.incomingFillColor, strokeColor: bubble.incomingStrokeColor, neighbors: .bottom) + self.chatMessageBackgroundIncomingMergedBottomHighlightedImage = messageBubbleImage(incoming: true, fillColor: bubble.incomingFillHighlightedColor, strokeColor: bubble.incomingStrokeColor, neighbors: .bottom) + self.chatMessageBackgroundIncomingMergedBothImage = messageBubbleImage(incoming: true, fillColor: bubble.incomingFillColor, strokeColor: bubble.incomingStrokeColor, neighbors: .both) + self.chatMessageBackgroundIncomingMergedBothHighlightedImage = messageBubbleImage(incoming: true, fillColor: bubble.incomingFillHighlightedColor, strokeColor: bubble.incomingStrokeColor, neighbors: .both) + + self.chatMessageBackgroundOutgoingImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .none) + self.chatMessageBackgroundOutgoingHighlightedImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillHighlightedColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .none) + self.chatMessageBackgroundOutgoingMergedTopImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .top) + self.chatMessageBackgroundOutgoingMergedTopHighlightedImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillHighlightedColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .top) + self.chatMessageBackgroundOutgoingMergedBottomImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .bottom) + self.chatMessageBackgroundOutgoingMergedBottomHighlightedImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillHighlightedColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .bottom) + self.chatMessageBackgroundOutgoingMergedBothImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .both) + self.chatMessageBackgroundOutgoingMergedBothHighlightedImage = messageBubbleImage(incoming: false, fillColor: bubble.outgoingFillHighlightedColor, strokeColor: bubble.outgoingStrokeColor, neighbors: .both) + + self.checkBubbleFullImage = generateCheckImage(partial: false, color: theme.bubble.outgoingCheckColor)! + self.checkBubblePartialImage = generateCheckImage(partial: true, color: theme.bubble.outgoingCheckColor)! + + self.checkMediaFullImage = generateCheckImage(partial: false, color: .white)! + self.checkMediaPartialImage = generateCheckImage(partial: true, color: .white)! + + self.clockBubbleIncomingFrameImage = generateClockFrameImage(color: theme.bubble.incomingPendingActivityColor)! + self.clockBubbleIncomingMinImage = generateClockMinImage(color: theme.bubble.incomingPendingActivityColor)! + self.clockBubbleOutgoingFrameImage = generateClockFrameImage(color: theme.bubble.outgoingPendingActivityColor)! + self.clockBubbleOutgoingMinImage = generateClockMinImage(color: theme.bubble.outgoingPendingActivityColor)! + + self.clockMediaFrameImage = generateClockFrameImage(color: .white)! + self.clockMediaMinImage = generateClockMinImage(color: .white)! + + self.dateAndStatusMediaBackground = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.bubble.mediaDateAndStatusFillColor)! + self.dateAndStatusFreeBackground = generateStretchableFilledCircleImage(diameter: 18.0, color: theme.serviceMessage.serviceMessageFillColor)! + + let impressionCountImage = UIImage(bundleImageName: "Chat/Message/ImpressionCount")! + self.incomingDateAndStatusImpressionIcon = generateTintedImage(image: impressionCountImage, color: theme.bubble.incomingSecondaryTextColor)! + self.outgoingDateAndStatusImpressionIcon = generateTintedImage(image: impressionCountImage, color: theme.bubble.outgoingSecondaryTextColor)! + self.mediaImpressionIcon = generateTintedImage(image: impressionCountImage, color: .white)! + + self.dateStaticBackground = generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context -> Void in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.serviceMessage.dateFillStaticColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })!.stretchableImage(withLeftCapWidth: 13, topCapHeight: 13) + + self.dateFloatingBackground = generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context -> Void in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(theme.serviceMessage.dateFillFloatingColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })!.stretchableImage(withLeftCapWidth: 13, topCapHeight: 13) + } +} diff --git a/TelegramUI/PresentationThemeSettings.swift b/TelegramUI/PresentationThemeSettings.swift new file mode 100644 index 0000000000..c9f66e0c94 --- /dev/null +++ b/TelegramUI/PresentationThemeSettings.swift @@ -0,0 +1,91 @@ +import Foundation +import Postbox +import SwiftSignalKit + +public enum PresentationBuilinThemeReference: Int32 { + case light + case dark +} + +public enum PresentationThemeReference: Coding, Equatable { + case builtin(PresentationBuilinThemeReference) + + public init(decoder: Decoder) { + switch decoder.decodeInt32ForKey("v", orElse: 0) { + case 0: + self = .builtin(PresentationBuilinThemeReference(rawValue: decoder.decodeInt32ForKey("t", orElse: 0))!) + default: + //assertionFailure() + self = .builtin(.light) + } + } + + public func encode(_ encoder: Encoder) { + switch self { + case let .builtin(reference): + encoder.encodeInt32(0, forKey: "v") + encoder.encodeInt32(reference.rawValue, forKey: "t") + } + } + + public static func ==(lhs: PresentationThemeReference, rhs: PresentationThemeReference) -> Bool { + switch lhs { + case let .builtin(reference): + if case .builtin(reference) = rhs { + return true + } else { + return false + } + } + } +} + +public struct PresentationThemeSettings: PreferencesEntry { + public let chatWallpaper: TelegramWallpaper + public let theme: PresentationThemeReference + + public static var defaultSettings: PresentationThemeSettings { + return PresentationThemeSettings(chatWallpaper: .builtin, theme: .builtin(.light)) + } + + init(chatWallpaper: TelegramWallpaper, theme: PresentationThemeReference) { + self.chatWallpaper = chatWallpaper + self.theme = theme + } + + public init(decoder: Decoder) { + self.chatWallpaper = (decoder.decodeObjectForKey("w", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper) ?? .builtin + self.theme = decoder.decodeObjectForKey("t", decoder: { PresentationThemeReference(decoder: $0) }) as! PresentationThemeReference + } + + public func encode(_ encoder: Encoder) { + encoder.encodeObject(self.chatWallpaper, forKey: "w") + encoder.encodeObject(self.theme, forKey: "t") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? PresentationThemeSettings { + return self == to + } else { + return false + } + } + + public static func ==(lhs: PresentationThemeSettings, rhs: PresentationThemeSettings) -> Bool { + return lhs.chatWallpaper == rhs.chatWallpaper && lhs.theme == rhs.theme + } +} + +func updatePresentationThemeSettingsInteractively(postbox: Postbox, _ f: @escaping (PresentationThemeSettings) -> PresentationThemeSettings) -> Signal { + return postbox.modify { modifier -> Void in + modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings, { entry in + let currentSettings: PresentationThemeSettings + if let entry = entry as? PresentationThemeSettings { + currentSettings = entry + } else { + currentSettings = PresentationThemeSettings.defaultSettings + } + return f(currentSettings) + }) + } +} diff --git a/TelegramUI/PresentationsResourceCache.swift b/TelegramUI/PresentationsResourceCache.swift new file mode 100644 index 0000000000..7ceb00ac7a --- /dev/null +++ b/TelegramUI/PresentationsResourceCache.swift @@ -0,0 +1,51 @@ +import Foundation +import SwiftSignalKit + +private final class PresentationsResourceCacheHolder { + var images: [Int32: UIImage] = [:] +} + +private final class PresentationsResourceAnyCacheHolder { + var objects: [Int32: AnyObject] = [:] +} + +public final class PresentationsResourceCache { + private let imageCache = Atomic(value: PresentationsResourceCacheHolder()) + private let objectCache = Atomic(value: PresentationsResourceAnyCacheHolder()) + + public func image(_ key: Int32, _ theme: PresentationTheme, _ generate: (PresentationTheme) -> UIImage?) -> UIImage? { + let result = self.imageCache.with { holder -> UIImage? in + return holder.images[key] + } + if let result = result { + return result + } else { + if let image = generate(theme) { + self.imageCache.with { holder -> Void in + holder.images[key] = image + } + return image + } else { + return nil + } + } + } + + public func object(_ key: Int32, _ theme: PresentationTheme, _ generate: (PresentationTheme) -> AnyObject?) -> AnyObject? { + let result = self.objectCache.with { holder -> AnyObject? in + return holder.objects[key] + } + if let result = result { + return result + } else { + if let object = generate(theme) { + self.objectCache.with { holder -> Void in + holder.objects[key] = object + } + return object + } else { + return nil + } + } + } +} diff --git a/TelegramUI/PrivacyAndSecurityController.swift b/TelegramUI/PrivacyAndSecurityController.swift index ecde644e4e..837bf888bb 100644 --- a/TelegramUI/PrivacyAndSecurityController.swift +++ b/TelegramUI/PrivacyAndSecurityController.swift @@ -35,18 +35,18 @@ private enum PrivacyAndSecuritySection: Int32 { } private enum PrivacyAndSecurityEntry: ItemListNodeEntry { - case privacyHeader - case blockedPeers - case lastSeenPrivacy(String) - case groupPrivacy(String) - case voiceCallPrivacy(String) - case securityHeader - case passcode - case twoStepVerification - case activeSessions - case accountHeader - case accountTimeout(String) - case accountInfo + case privacyHeader(PresentationTheme, String) + case blockedPeers(PresentationTheme, String) + case lastSeenPrivacy(PresentationTheme, String, String) + case groupPrivacy(PresentationTheme, String, String) + case voiceCallPrivacy(PresentationTheme, String, String) + case securityHeader(PresentationTheme, String) + case passcode(PresentationTheme, String) + case twoStepVerification(PresentationTheme, String) + case activeSessions(PresentationTheme, String) + case accountHeader(PresentationTheme, String) + case accountTimeout(PresentationTheme, String, String) + case accountInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -90,28 +90,74 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { static func ==(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool { switch lhs { - case .privacyHeader, .blockedPeers, .securityHeader, .passcode, .twoStepVerification, .activeSessions, .accountHeader, .accountInfo: - return lhs.stableId == rhs.stableId - case let .lastSeenPrivacy(text): - if case .lastSeenPrivacy(text) = rhs { + case let .privacyHeader(lhsTheme, lhsText): + if case let .privacyHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .groupPrivacy(text): - if case .groupPrivacy(text) = rhs { + case let .blockedPeers(lhsTheme, lhsText): + if case let .blockedPeers(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .voiceCallPrivacy(text): - if case .voiceCallPrivacy(text) = rhs { + case let .lastSeenPrivacy(lhsTheme, lhsText, lhsValue): + if case let .lastSeenPrivacy(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .accountTimeout(text): - if case .accountTimeout(text) = rhs { + case let .groupPrivacy(lhsTheme, lhsText, lhsValue): + if case let .groupPrivacy(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .voiceCallPrivacy(lhsTheme, lhsText, lhsValue): + if case let .voiceCallPrivacy(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .securityHeader(lhsTheme, lhsText): + if case let .securityHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .passcode(lhsTheme, lhsText): + if case let .passcode(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .twoStepVerification(lhsTheme, lhsText): + if case let .twoStepVerification(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .activeSessions(lhsTheme, lhsText): + if case let .activeSessions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .accountHeader(lhsTheme, lhsText): + if case let .accountHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .accountTimeout(lhsTheme, lhsText, lhsValue): + if case let .accountTimeout(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .accountInfo(lhsTheme, lhsText): + if case let .accountInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -125,46 +171,46 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { func item(_ arguments: PrivacyAndSecurityControllerArguments) -> ListViewItem { switch self { - case .privacyHeader: - return ItemListSectionHeaderItem(text: "PRIVACY", sectionId: self.section) - case .blockedPeers: - return ItemListDisclosureItem(title: "Blocked Users", label: "", sectionId: self.section, style: .blocks, action: { + case let .privacyHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .blockedPeers(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openBlockedUsers() }) - case let .lastSeenPrivacy(text): - return ItemListDisclosureItem(title: "Last Seen", label: text, sectionId: self.section, style: .blocks, action: { + case let .lastSeenPrivacy(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openLastSeenPrivacy() }) - case let .groupPrivacy(text): - return ItemListDisclosureItem(title: "Groups", label: text, sectionId: self.section, style: .blocks, action: { + case let .groupPrivacy(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openGroupsPrivacy() }) - case let .voiceCallPrivacy(text): - return ItemListDisclosureItem(title: "Voice Calls", label: text, sectionId: self.section, style: .blocks, action: { + case let .voiceCallPrivacy(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openVoiceCallPrivacy() }) - case .securityHeader: - return ItemListSectionHeaderItem(text: "SECURITY", sectionId: self.section) - case .passcode: - return ItemListDisclosureItem(title: "Passcode Lock", label: "", sectionId: self.section, style: .blocks, action: { + case let .securityHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .passcode(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openPasscode() }) - case .twoStepVerification: - return ItemListDisclosureItem(title: "Two-Step Verification", label: "", sectionId: self.section, style: .blocks, action: { + case let .twoStepVerification(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openTwoStepVerification() }) - case .activeSessions: - return ItemListDisclosureItem(title: "Active Sessions", label: "", sectionId: self.section, style: .blocks, action: { + case let .activeSessions(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openActiveSessions() }) - case .accountHeader: - return ItemListSectionHeaderItem(text: "DELETE MY ACCOUNT", sectionId: self.section) - case let .accountTimeout(text): - return ItemListDisclosureItem(title: "If Away For", label: text, sectionId: self.section, style: .blocks, action: { + case let .accountHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .accountTimeout(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.setupAccountAutoremove() }) - case .accountInfo: - return ItemListTextItem(text: .plain("If you do not log in at least once within this period, your account will be deleted along with all groups, messages and contacts."), sectionId: self.section) + case let .accountInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } @@ -232,26 +278,26 @@ private func stringForAccountTimeout(_ timeout: Int32) -> String { } } -private func privacyAndSecurityControllerEntries(state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?) -> [PrivacyAndSecurityEntry] { +private func privacyAndSecurityControllerEntries(presentationData: PresentationData, state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?) -> [PrivacyAndSecurityEntry] { var entries: [PrivacyAndSecurityEntry] = [] - entries.append(.privacyHeader) - entries.append(.blockedPeers) + entries.append(.privacyHeader(presentationData.theme, presentationData.strings.PrivacySettings_PrivacyTitle)) + entries.append(.blockedPeers(presentationData.theme, presentationData.strings.Settings_BlockedUsers)) if let privacySettings = privacySettings { - entries.append(.lastSeenPrivacy(stringForSelectiveSettings(privacySettings.presence))) - entries.append(.groupPrivacy(stringForSelectiveSettings(privacySettings.groupInvitations))) - entries.append(.voiceCallPrivacy(stringForSelectiveSettings(privacySettings.voiceCalls))) + entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, stringForSelectiveSettings(privacySettings.presence))) + entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, stringForSelectiveSettings(privacySettings.groupInvitations))) + entries.append(.voiceCallPrivacy(presentationData.theme, presentationData.strings.Privacy_Calls, stringForSelectiveSettings(privacySettings.voiceCalls))) } else { - entries.append(.lastSeenPrivacy("Loading")) - entries.append(.groupPrivacy("Loading")) - entries.append(.voiceCallPrivacy("Loading")) + entries.append(.lastSeenPrivacy(presentationData.theme, presentationData.strings.PrivacySettings_LastSeen, presentationData.strings.Channel_NotificationLoading)) + entries.append(.groupPrivacy(presentationData.theme, presentationData.strings.Privacy_GroupsAndChannels, presentationData.strings.Channel_NotificationLoading)) + entries.append(.voiceCallPrivacy(presentationData.theme, presentationData.strings.Privacy_Calls, presentationData.strings.Channel_NotificationLoading)) } - entries.append(.securityHeader) - entries.append(.passcode) - entries.append(.twoStepVerification) - entries.append(.activeSessions) - entries.append(.accountHeader) + entries.append(.securityHeader(presentationData.theme, presentationData.strings.PrivacySettings_SecurityTitle)) + entries.append(.passcode(presentationData.theme, presentationData.strings.PrivacySettings_Passcode)) + entries.append(.twoStepVerification(presentationData.theme, presentationData.strings.PrivacySettings_TwoStepAuth)) + entries.append(.activeSessions(presentationData.theme, presentationData.strings.PrivacySettings_AuthSessions)) + entries.append(.accountHeader(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountTitle)) if let privacySettings = privacySettings { let value: Int32 if let updatingAccountTimeoutValue = state.updatingAccountTimeoutValue { @@ -259,11 +305,11 @@ private func privacyAndSecurityControllerEntries(state: PrivacyAndSecurityContro } else { value = privacySettings.accountRemovalTimeout } - entries.append(.accountTimeout(stringForAccountTimeout(value))) + entries.append(.accountTimeout(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountIfAwayFor, stringForAccountTimeout(value))) } else { - entries.append(.accountTimeout("Loading")) + entries.append(.accountTimeout(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountIfAwayFor, presentationData.strings.Channel_NotificationLoading)) } - entries.append(.accountInfo) + entries.append(.accountInfo(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountHelp)) return entries } @@ -437,24 +483,23 @@ public func privacyAndSecurityController(account: Account, initialSettings: Sign })) }) - let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, privacySettingsPromise.get()) - |> map { state, privacySettings -> (ItemListControllerState, (ItemListNodeState, PrivacyAndSecurityEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, privacySettingsPromise.get()) + |> map { presentationData, state, privacySettings -> (ItemListControllerState, (ItemListNodeState, PrivacyAndSecurityEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? if privacySettings == nil || state.updatingAccountTimeoutValue != nil { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } - let controllerState = ItemListControllerState(title: .text("Privacy and Security"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: false) - let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(state: state, privacySettings: privacySettings), style: .blocks, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Privacy and Security"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: false) + let listState = ItemListNodeState(entries: privacyAndSecurityControllerEntries(presentationData: presentationData, state: state, privacySettings: privacySettings), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + let controller = ItemListController(account: account, state: signal) pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } diff --git a/TelegramUI/ProgressNavigationButtonNode.swift b/TelegramUI/ProgressNavigationButtonNode.swift index 2cc3c53eae..428d91f751 100644 --- a/TelegramUI/ProgressNavigationButtonNode.swift +++ b/TelegramUI/ProgressNavigationButtonNode.swift @@ -2,33 +2,16 @@ import Foundation import AsyncDisplayKit import Display -private let indicatorImage = generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0x007ee5).cgColor) - let _ = try? drawSvgPath(context, path: "M11,22 C17.0751322,22 22,17.0751322 22,11 C22,4.92486775 17.0751322,0 11,0 C4.92486775,0 0,4.92486775 0,11 C0,12.4564221 0.28362493,13.8747731 0.827833595,15.1935223 C1.00609922,15.6255031 1.50080164,15.8311798 1.93278238,15.6529142 C2.36476311,15.4746485 2.57043984,14.9799461 2.39217421,14.5479654 C1.93209084,13.4330721 1.69230769,12.233965 1.69230769,11 C1.69230769,5.85950348 5.85950348,1.69230769 11,1.69230769 C16.1404965,1.69230769 20.3076923,5.85950348 20.3076923,11 C20.3076923,16.1404965 16.1404965,20.3076923 11,20.3076923 C10.5326821,20.3076923 10.1538462,20.6865283 10.1538462,21.1538462 C10.1538462,21.621164 10.5326821,22 11,22 Z ") - /* - - - - - Created with Sketch. - - - - */ - - -}) - final class ProgressNavigationButtonNode: ASDisplayNode { private var indicatorNode: ASImageNode - override init() { + init(theme: PresentationTheme = defaultPresentationTheme) { self.indicatorNode = ASImageNode() self.indicatorNode.isLayerBacked = true self.indicatorNode.displayWithoutProcessing = true self.indicatorNode.displaysAsynchronously = false - self.indicatorNode.image = indicatorImage + + self.indicatorNode.image = PresentationResourcesRootController.navigationIndefiniteActivityImage(theme) super.init() @@ -44,7 +27,7 @@ final class ProgressNavigationButtonNode: ASDisplayNode { basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) basicAnimation.duration = 0.5 basicAnimation.fromValue = NSNumber(value: Float(0.0)) - basicAnimation.toValue = NSNumber(value: Float(M_PI * 2.0)) + basicAnimation.toValue = NSNumber(value: Float.pi * 2.0) basicAnimation.repeatCount = Float.infinity basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) diff --git a/TelegramUI/RadialProgressNode.swift b/TelegramUI/RadialProgressNode.swift index 18cd308eb4..a1b22e2415 100644 --- a/TelegramUI/RadialProgressNode.swift +++ b/TelegramUI/RadialProgressNode.swift @@ -33,7 +33,7 @@ private class RadialProgressOverlayParameters: NSObject { } private class RadialProgressOverlayNode: ASDisplayNode { - let theme: RadialProgressTheme + var theme: RadialProgressTheme var previousProgress: Float? var effectiveProgress: Float = 0.0 { @@ -82,9 +82,14 @@ private class RadialProgressOverlayNode: ASDisplayNode { self.displaysAsynchronously = true } + func updateTheme(_ theme: RadialProgressTheme) { + self.theme = theme + self.setNeedsDisplay() + } + override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { var updatedState = self.state - if case let .Fetching = updatedState { + if case .Fetching = updatedState { updatedState = .Fetching(progress: self.effectiveProgress) } return RadialProgressOverlayParameters(theme: self.theme, diameter: self.frame.size.width, state: updatedState) @@ -108,8 +113,8 @@ private class RadialProgressOverlayNode: ASDisplayNode { case .None, .Remote, .Play, .Pause, .Icon, .Image: break case let .Fetching(progress): - let startAngle = -CGFloat(M_PI_2) - let endAngle = 2.0 * CGFloat(M_PI) * CGFloat(progress) - CGFloat(M_PI_2) + let startAngle = -CGFloat.pi / 2.0 + let endAngle = 2.0 * (CGFloat.pi / 2.0) * CGFloat(progress) - CGFloat(M_PI_2) let pathDiameter = parameters.diameter - 2.25 - 2.5 * 2.0 @@ -128,7 +133,7 @@ private class RadialProgressOverlayNode: ASDisplayNode { basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) basicAnimation.duration = 2.0 basicAnimation.fromValue = NSNumber(value: Float(0.0)) - basicAnimation.toValue = NSNumber(value: Float(M_PI * 2.0)) + basicAnimation.toValue = NSNumber(value: Float.pi * 2.0) basicAnimation.repeatCount = Float.infinity basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) @@ -159,7 +164,7 @@ public struct RadialProgressTheme { } class RadialProgressNode: ASControlNode { - private let theme: RadialProgressTheme + private var theme: RadialProgressTheme private let overlay: RadialProgressOverlayNode var state: RadialProgressState = .None { @@ -235,10 +240,6 @@ class RadialProgressNode: ASControlNode { } } - convenience override init() { - self.init(theme: RadialProgressTheme(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor.white, icon: nil)) - } - init(theme: RadialProgressTheme) { self.theme = theme self.overlay = RadialProgressOverlayNode(theme: theme) @@ -248,6 +249,12 @@ class RadialProgressNode: ASControlNode { self.isOpaque = false } + func updateTheme(_ theme: RadialProgressTheme) { + self.theme = theme + self.setNeedsDisplay() + self.overlay.updateTheme(theme) + } + override var frame: CGRect { get { return super.frame diff --git a/TelegramUI/RadialTimeoutNode.swift b/TelegramUI/RadialTimeoutNode.swift index 1dba21a79a..313ac1af56 100644 --- a/TelegramUI/RadialTimeoutNode.swift +++ b/TelegramUI/RadialTimeoutNode.swift @@ -30,8 +30,8 @@ private final class RadialTimeoutNodeTimer: NSObject { } public final class RadialTimeoutNode: ASDisplayNode { - private let nodeBackgroundColor: UIColor - private let nodeForegroundColor: UIColor + private var nodeBackgroundColor: UIColor + private var nodeForegroundColor: UIColor private var timeout: (Double, Double)? @@ -46,6 +46,13 @@ public final class RadialTimeoutNode: ASDisplayNode { self.isOpaque = false } + public func updateTheme(backgroundColor: UIColor, foregroundColor: UIColor) { + self.nodeBackgroundColor = backgroundColor + self.nodeForegroundColor = foregroundColor + + self.setNeedsDisplay() + } + deinit { self.animationTimer?.invalidate() } diff --git a/TelegramUI/RecentSessionsController.swift b/TelegramUI/RecentSessionsController.swift index f9a76b57ac..5fa626db53 100644 --- a/TelegramUI/RecentSessionsController.swift +++ b/TelegramUI/RecentSessionsController.swift @@ -56,13 +56,13 @@ private enum RecentSessionsEntryStableId: Hashable { } private enum RecentSessionsEntry: ItemListNodeEntry { - case currentSessionHeader - case currentSession(RecentAccountSession) - case terminateOtherSessions - case currentSessionInfo + case currentSessionHeader(PresentationTheme, String) + case currentSession(PresentationTheme, PresentationStrings, RecentAccountSession) + case terminateOtherSessions(PresentationTheme, String) + case currentSessionInfo(PresentationTheme, String) - case otherSessionsHeader - case session(index: Int32, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) + case otherSessionsHeader(PresentationTheme, String) + case session(index: Int32, theme: PresentationTheme, strings: PresentationStrings, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) var section: ItemListSectionId { switch self { @@ -85,23 +85,45 @@ private enum RecentSessionsEntry: ItemListNodeEntry { return .index(3) case .otherSessionsHeader: return .index(4) - case let .session(_, session, _, _, _): + case let .session(_, _, _, session, _, _, _): return .session(session.hash) } } static func ==(lhs: RecentSessionsEntry, rhs: RecentSessionsEntry) -> Bool { switch lhs { - case .currentSessionHeader, .terminateOtherSessions, .currentSessionInfo, .otherSessionsHeader: - return lhs.stableId == rhs.stableId - case let .currentSession(session): - if case .currentSession(session) = rhs { + case let .currentSessionHeader(lhsTheme, lhsText): + if case let .currentSessionHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .session(index, session, enabled, editing, revealed): - if case .session(index, session, enabled, editing, revealed) = rhs { + case let .terminateOtherSessions(lhsTheme, lhsText): + if case let .terminateOtherSessions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .currentSessionInfo(lhsTheme, lhsText): + if case let .currentSessionInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .otherSessionsHeader(lhsTheme, lhsText): + if case let .otherSessionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .currentSession(lhsTheme, lhsStrings, lhsSession): + if case let .currentSession(rhsTheme, rhsStrings, rhsSession) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsSession == rhsSession { + return true + } else { + return false + } + case let .session(lhsIndex, lhsTheme, lhsStrings, lhsSession, lhsEnabled, lhsEditing, lhsRevealed): + if case let .session(rhsIndex, rhsTheme, rhsStrings, rhsSession, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsSession == rhsSession, lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { return true } else { return false @@ -119,8 +141,8 @@ private enum RecentSessionsEntry: ItemListNodeEntry { } case .session: switch lhs { - case let .session(lhsIndex, _, _, _, _): - if case let .session(rhsIndex, _, _, _, _) = rhs { + case let .session(lhsIndex, _, _, _, _, _, _): + if case let .session(rhsIndex, _, _, _, _, _, _) = rhs { return lhsIndex <= rhsIndex } else { return false @@ -133,22 +155,22 @@ private enum RecentSessionsEntry: ItemListNodeEntry { func item(_ arguments: RecentSessionsControllerArguments) -> ListViewItem { switch self { - case .currentSessionHeader: - return ItemListSectionHeaderItem(text: "CURRENT SESSION", sectionId: self.section) - case let .currentSession(session): - return ItemListRecentSessionItem(session: session, enabled: true, editable: false, editing: false, revealed: false, sectionId: self.section, setSessionIdWithRevealedOptions: { _, _ in + case let .currentSessionHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .currentSession(theme, strings, session): + return ItemListRecentSessionItem(theme: theme, strings: strings, session: session, enabled: true, editable: false, editing: false, revealed: false, sectionId: self.section, setSessionIdWithRevealedOptions: { _, _ in }, removeSession: { _ in }) - case .terminateOtherSessions: - return ItemListActionItem(title: "Terminate all other sessions", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .terminateOtherSessions(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.terminateOtherSessions() }) - case .currentSessionInfo: - return ItemListTextItem(text: .plain("Logs out all devices except for this one."), sectionId: self.section) - case .otherSessionsHeader: - return ItemListSectionHeaderItem(text: "ACTIVE SESSIONS", sectionId: self.section) - case let .session(_, session, enabled, editing, revealed): - return ItemListRecentSessionItem(session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in + case let .currentSessionInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .otherSessionsHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .session(_, theme, strings, session, enabled, editing, revealed): + return ItemListRecentSessionItem(theme: theme, strings: strings, session: session, enabled: enabled, editable: true, editing: editing, revealed: revealed, sectionId: self.section, setSessionIdWithRevealedOptions: { previousId, id in arguments.setSessionIdWithRevealedOptions(previousId, id) }, removeSession: { id in arguments.removeSession(id) @@ -211,22 +233,22 @@ private struct RecentSessionsControllerState: Equatable { } } -private func recentSessionsControllerEntries(state: RecentSessionsControllerState, sessions: [RecentAccountSession]?) -> [RecentSessionsEntry] { +private func recentSessionsControllerEntries(presentationData: PresentationData, state: RecentSessionsControllerState, sessions: [RecentAccountSession]?) -> [RecentSessionsEntry] { var entries: [RecentSessionsEntry] = [] if let sessions = sessions { var existingSessionIds = Set() - entries.append(.currentSessionHeader) + entries.append(.currentSessionHeader(presentationData.theme, presentationData.strings.AuthSessions_CurrentSession)) if let index = sessions.index(where: { $0.hash == 0 }) { existingSessionIds.insert(sessions[index].hash) - entries.append(.currentSession(sessions[index])) + entries.append(.currentSession(presentationData.theme, presentationData.strings, sessions[index])) } if sessions.count > 1 { - entries.append(.terminateOtherSessions) - entries.append(.currentSessionInfo) + entries.append(.terminateOtherSessions(presentationData.theme, presentationData.strings.AuthSessions_TerminateOtherSessions)) + entries.append(.currentSessionInfo(presentationData.theme, presentationData.strings.AuthSessions_TerminateOtherSessionsHelp)) - entries.append(.otherSessionsHeader) + entries.append(.otherSessionsHeader(presentationData.theme, presentationData.strings.AuthSessions_OtherSessions)) let filteredSessions: [RecentAccountSession] = sessions.sorted(by: { lhs, rhs in return lhs.activityDate > rhs.activityDate @@ -235,7 +257,7 @@ private func recentSessionsControllerEntries(state: RecentSessionsControllerStat for i in 0 ..< filteredSessions.count { if !existingSessionIds.contains(sessions[i].hash) { existingSessionIds.insert(sessions[i].hash) - entries.append(.session(index: Int32(i), session: sessions[i], enabled: state.removingSessionId != sessions[i].hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == sessions[i].hash)) + entries.append(.session(index: Int32(i), theme: presentationData.theme, strings: presentationData.strings, session: sessions[i], enabled: state.removingSessionId != sessions[i].hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == sessions[i].hash)) } } } @@ -353,21 +375,21 @@ public func recentSessionsController(account: Account) -> ViewController { var previousSessions: [RecentAccountSession]? - let signal = combineLatest(statePromise.get(), sessionsPromise.get()) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), sessionsPromise.get()) |> deliverOnMainQueue - |> map { state, sessions -> (ItemListControllerState, (ItemListNodeState, RecentSessionsEntry.ItemGenerationArguments)) in + |> map { presentationData, state, sessions -> (ItemListControllerState, (ItemListNodeState, RecentSessionsEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? if let sessions = sessions, sessions.count > 1 { if state.terminatingOtherSessions { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } else if state.editing { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } @@ -383,15 +405,15 @@ public func recentSessionsController(account: Account) -> ViewController { let previous = previousSessions previousSessions = sessions - let controllerState = ItemListControllerState(title: .text("Active Sessions"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) - let listState = ItemListNodeState(entries: recentSessionsControllerEntries(state: state, sessions: sessions), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && sessions != nil && previous!.count >= sessions!.count) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.AuthSessions_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(entries: recentSessionsControllerEntries(presentationData: presentationData, state: state, sessions: sessions), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && sessions != nil && previous!.count >= sessions!.count) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/ReplyAccessoryPanelNode.swift b/TelegramUI/ReplyAccessoryPanelNode.swift index 78bc5f68e8..c7cec5f5b4 100644 --- a/TelegramUI/ReplyAccessoryPanelNode.swift +++ b/TelegramUI/ReplyAccessoryPanelNode.swift @@ -5,20 +5,6 @@ import Postbox import SwiftSignalKit import Display -private let lineImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(0x007ee5)) -private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x9099A2).cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - context.move(to: CGPoint(x: 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) - context.strokePath() - context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) - context.strokePath() -}) - final class ReplyAccessoryPanelNode: AccessoryPanelNode { private let messageDisposable = MetaDisposable() let messageId: MessageId @@ -31,18 +17,22 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { let textNode: ASTextNode let imageNode: TransformImageNode - init(account: Account, messageId: MessageId) { + var theme: PresentationTheme + + init(account: Account, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings) { self.messageId = messageId + self.theme = theme + self.closeButton = ASButtonNode() - self.closeButton.setImage(closeButtonImage, for: []) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.closeButton.displaysAsynchronously = false self.lineNode = ASImageNode() self.lineNode.displayWithoutProcessing = true self.lineNode.displaysAsynchronously = false - self.lineNode.image = lineImage + self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) self.titleNode = ASTextNode() self.titleNode.truncationMode = .byTruncatingTail @@ -127,8 +117,8 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { } } - strongSelf.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: UIColor(0x007ee5)) - strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: UIColor.black) + strongSelf.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) + strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.primaryTextColor) if let applyImage = applyImage { applyImage() @@ -150,6 +140,26 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.messageDisposable.dispose() } + override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if self.theme !== theme { + self.theme = theme + + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) + + self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) + + if let text = self.titleNode.attributedText?.string { + self.titleNode.attributedText = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + } + + if let text = self.textNode.attributedText?.string { + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) + } + + self.setNeedsLayout() + } + } + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { return CGSize(width: constrainedSize.width, height: 45.0) } diff --git a/TelegramUI/SearchBarNode.swift b/TelegramUI/SearchBarNode.swift index 7ea6450c03..a35d2b91f8 100644 --- a/TelegramUI/SearchBarNode.swift +++ b/TelegramUI/SearchBarNode.swift @@ -4,18 +4,16 @@ import UIKit import AsyncDisplayKit import Display -private func generateBackground() -> UIImage? { +private func generateBackground(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { let diameter: CGFloat = 8.0 return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in - context.setFillColor(UIColor.white.cgColor) + context.setFillColor(backgroundColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0xededed).cgColor) + context.setFillColor(foregroundColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - }, opaque: true)?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) + }, opaque: true)?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) } -private let searchBarBackground = generateBackground() - private class SearchBarTextField: UITextField { fileprivate let placeholderLabel: UILabel private var placeholderLabelConstrainedSize: CGSize? @@ -74,29 +72,43 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { } } - override init() { + private var theme: PresentationTheme + private var strings: PresentationStrings + + init(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings + self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true - self.backgroundNode.backgroundColor = UIColor.white + self.backgroundNode.backgroundColor = theme.rootController.activeNavigationSearchBar.backgroundColor self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) + self.separatorNode.backgroundColor = theme.rootController.activeNavigationSearchBar.separatorColor self.textBackgroundNode = ASImageNode() self.textBackgroundNode.isLayerBacked = false self.textBackgroundNode.displaysAsynchronously = false self.textBackgroundNode.displayWithoutProcessing = true - self.textBackgroundNode.image = searchBarBackground + self.textBackgroundNode.image = generateBackground(backgroundColor: theme.rootController.activeNavigationSearchBar.backgroundColor, foregroundColor: theme.rootController.activeNavigationSearchBar.inputFillColor) self.textField = SearchBarTextField() - self.textField.font = Font.regular(15.0) self.textField.autocorrectionType = .no self.textField.returnKeyType = .done + self.textField.font = Font.regular(15.0) + self.textField.textColor = theme.rootController.activeNavigationSearchBar.inputTextColor + + switch theme.chatList.searchBarKeyboardColor { + case .light: + self.textField.keyboardAppearance = .default + case .dark: + self.textField.keyboardAppearance = .dark + } self.cancelButton = ASButtonNode() self.cancelButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) - self.cancelButton.setAttributedTitle(NSAttributedString(string: "Cancel", font: Font.regular(17.0), textColor: UIColor(0x007ee5)), for: []) + self.cancelButton.setAttributedTitle(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.activeNavigationSearchBar.accentColor), for: []) self.cancelButton.displaysAsynchronously = false super.init() @@ -104,7 +116,6 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.addSubnode(self.backgroundNode) self.addSubnode(self.separatorNode) - self.backgroundColor = UIColor.white.withAlphaComponent(0.5) self.addSubnode(self.textBackgroundNode) self.view.addSubview(self.textField) self.addSubnode(self.cancelButton) @@ -115,6 +126,19 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) } + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if self.theme !== theme { + self.cancelButton.setAttributedTitle(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.activeNavigationSearchBar.accentColor), for: []) + self.backgroundNode.backgroundColor = theme.rootController.activeNavigationSearchBar.backgroundColor + self.separatorNode.backgroundColor = theme.rootController.activeNavigationSearchBar.separatorColor + self.textBackgroundNode.image = generateBackground(backgroundColor: theme.rootController.activeNavigationSearchBar.backgroundColor, foregroundColor: theme.rootController.activeNavigationSearchBar.inputFillColor) + + } + + self.theme = theme + self.strings = strings + } + override func layout() { self.backgroundNode.frame = self.bounds self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel)) @@ -161,10 +185,12 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { node.isHidden = true } - func deactivate() { + func deactivate(clear: Bool = true) { self.textField.resignFirstResponder() - self.textField.text = nil - self.textField.placeholderLabel.isHidden = false + if clear { + self.textField.text = nil + self.textField.placeholderLabel.isHidden = false + } } func transitionOut(to node: SearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: () -> Void) { diff --git a/TelegramUI/SearchBarPlaceholderNode.swift b/TelegramUI/SearchBarPlaceholderNode.swift index df485b44fb..5e6bf47bae 100644 --- a/TelegramUI/SearchBarPlaceholderNode.swift +++ b/TelegramUI/SearchBarPlaceholderNode.swift @@ -38,7 +38,7 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate { self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displayWithoutProcessing = true - self.foregroundColor = UIColor(0xededed) + self.foregroundColor = UIColor(rgb: 0xededed) self.backgroundNode.image = generateBackground(backgroundColor: UIColor.white, foregroundColor: self.foregroundColor) @@ -48,9 +48,6 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate { self.labelNode.backgroundColor = self.foregroundColor super.init() - /*super.init(viewBlock: { - return SearchBarPlaceholderNodeView() - }, didLoad: nil)*/ self.addSubnode(self.backgroundNode) self.addSubnode(self.labelNode) @@ -64,16 +61,16 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate { self.backgroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(backgroundTap(_:)))) } - func asyncLayout() -> (_ placeholderString: NSAttributedString?, _ constrainedSize: CGSize, _ foregoundColor: UIColor) -> (() -> Void) { + func asyncLayout() -> (_ placeholderString: NSAttributedString?, _ constrainedSize: CGSize, _ foregoundColor: UIColor, _ backgroundColor: UIColor) -> (() -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) let currentForegroundColor = self.foregroundColor - return { placeholderString, constrainedSize, foregroundColor in + return { placeholderString, constrainedSize, foregroundColor, backgroundColor in let (labelLayoutResult, labelApply) = labelLayout(placeholderString, foregroundColor, 1, .end, constrainedSize, .natural, nil, UIEdgeInsets()) var updatedBackgroundImage: UIImage? if !currentForegroundColor.isEqual(foregroundColor) { - updatedBackgroundImage = generateBackground(backgroundColor: UIColor.white, foregroundColor: foregroundColor) + updatedBackgroundImage = generateBackground(backgroundColor: backgroundColor, foregroundColor: foregroundColor) } return { [weak self] in diff --git a/TelegramUI/SearchDisplayController.swift b/TelegramUI/SearchDisplayController.swift index ce075a8029..64997db45f 100644 --- a/TelegramUI/SearchDisplayController.swift +++ b/TelegramUI/SearchDisplayController.swift @@ -9,8 +9,8 @@ final class SearchDisplayController { private var containerLayout: (ContainerViewLayout, CGFloat)? - init(contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) { - self.searchBar = SearchBarNode() + init(theme: PresentationTheme, strings: PresentationStrings, contentNode: SearchDisplayControllerContentNode, cancel: @escaping () -> Void) { + self.searchBar = SearchBarNode(theme: theme, strings: strings) self.contentNode = contentNode self.searchBar.textUpdated = { [weak contentNode] text in @@ -19,6 +19,10 @@ final class SearchDisplayController { self.searchBar.cancel = cancel } + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.searchBar.updateThemeAndStrings(theme: theme, strings: strings) + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: (layout.statusBarHeight ?? 0.0) - 20.0), size: CGSize(width: layout.size.width, height: 64.0)) transition.updateFrame(node: self.searchBar, frame: searchBarFrame) @@ -26,7 +30,7 @@ final class SearchDisplayController { self.containerLayout = (layout, searchBarFrame.maxY) transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: layout.intrinsicInsets, statusBarHeight: nil, inputHeight: layout.inputHeight), navigationBarHeight: searchBarFrame.maxY, transition: transition) + self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), intrinsicInsets: layout.intrinsicInsets, statusBarHeight: nil, inputHeight: layout.inputHeight), navigationBarHeight: searchBarFrame.maxY, transition: transition) } func activate(insertSubnode: (ASDisplayNode) -> Void, placeholder: SearchBarPlaceholderNode) { @@ -37,7 +41,7 @@ final class SearchDisplayController { insertSubnode(self.contentNode) self.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), navigationBarHeight: navigationBarHeight, transition: .immediate) + self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), navigationBarHeight: navigationBarHeight, transition: .immediate) let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: self.contentNode.supernode) diff --git a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift index 1c6415f1d7..126407891e 100644 --- a/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift +++ b/TelegramUI/SecretChatHandshakeStatusInputPanelNode.swift @@ -10,7 +10,7 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { private var statusDisposable: Disposable? - private var presentationInterfaceState = ChatPresentationInterfaceState() + private var presentationInterfaceState: ChatPresentationInterfaceState? override init() { self.button = HighlightableButtonNode() @@ -46,7 +46,7 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { if let peer = interfaceState.peer as? TelegramSecretChat { switch peer.embeddedState { case .handshake: - self.button.setAttributedTitle(NSAttributedString(string: "Exchanging encryption keys...", font: Font.regular(15.0), textColor: .black), for: []) + self.button.setAttributedTitle(NSAttributedString(string: interfaceState.strings.Conversation_EncryptionProcessing, font: Font.regular(15.0), textColor: interfaceState.theme.chat.inputPanel.primaryTextColor), for: []) case .active, .terminated: break } diff --git a/TelegramUI/SecretChatKeyVisualization.h b/TelegramUI/SecretChatKeyVisualization.h index f92ae001f4..54bea900a1 100644 --- a/TelegramUI/SecretChatKeyVisualization.h +++ b/TelegramUI/SecretChatKeyVisualization.h @@ -2,3 +2,4 @@ #import UIImage *SecretChatKeyVisualization(NSData *data, NSData *additionalData, CGSize size); +NSString *stringForEmojiHashOfData(NSData *data, NSInteger count); diff --git a/TelegramUI/SecretChatKeyVisualization.m b/TelegramUI/SecretChatKeyVisualization.m index ab1a95160a..0251779086 100644 --- a/TelegramUI/SecretChatKeyVisualization.m +++ b/TelegramUI/SecretChatKeyVisualization.m @@ -1,5 +1,7 @@ #import "SecretChatKeyVisualization.h" +#import + #define UIColorRGB(rgb) ([[UIColor alloc] initWithRed:(((rgb >> 16) & 0xff) / 255.0f) green:(((rgb >> 8) & 0xff) / 255.0f) blue:(((rgb) & 0xff) / 255.0f) alpha:1.0f]) static int32_t get_bits(uint8_t const *bytes, unsigned int bitOffset, unsigned int numBits) @@ -107,3 +109,29 @@ UIImage *SecretChatKeyVisualization(NSData *data, NSData *additionalData, CGSize return image; } + +static int32_t positionExtractor(uint8_t *bytes, int32_t i, int32_t count) { + int offset = i * 8; + int64_t num = (((int64_t)bytes[offset] & 0x7F) << 56) | (((int64_t)bytes[offset+1] & 0xFF) << 48) | (((int64_t)bytes[offset+2] & 0xFF) << 40) | (((int64_t)bytes[offset+3] & 0xFF) << 32) | (((int64_t)bytes[offset+4] & 0xFF) << 24) | (((int64_t)bytes[offset+5] & 0xFF) << 16) | (((int64_t)bytes[offset+6] & 0xFF) << 8) | (((int64_t)bytes[offset+7] & 0xFF)); + return num % count; +} + +NSString *stringForEmojiHashOfData(NSData *data, NSInteger count) { + if (data.length != 32) + return @""; + + NSArray *emojis = @[ @"😉", @"😍", @"😛", @"😭", @"😱", @"😡", @"😎", @"😴", @"😵", @"😈", @"😬", @"😇", @"😏", @"👮", @"👷", @"💂", @"👶", @"👨", @"👩", @"👴", @"👵", @"😻", @"😽", @"🙀", @"👺", @"🙈", @"🙉", @"🙊", @"💀", @"👽", @"💩", @"🔥", @"💥", @"💤", @"👂", @"👀", @"👃", @"👅", @"👄", @"👍", @"👎", @"👌", @"👊", @"✌️", @"✋️", @"👐", @"👆", @"👇", @"👉", @"👈", @"🙏", @"👏", @"💪", @"🚶", @"🏃", @"💃", @"👫", @"👪", @"👬", @"👭", @"💅", @"🎩", @"👑", @"👒", @"👟", @"👞", @"👠", @"👕", @"👗", @"👖", @"👙", @"👜", @"👓", @"🎀", @"💄", @"💛", @"💙", @"💜", @"💚", @"💍", @"💎", @"🐶", @"🐺", @"🐱", @"🐭", @"🐹", @"🐰", @"🐸", @"🐯", @"🐨", @"🐻", @"🐷", @"🐮", @"🐗", @"🐴", @"🐑", @"🐘", @"🐼", @"🐧", @"🐥", @"🐔", @"🐍", @"🐢", @"🐛", @"🐝", @"🐜", @"🐞", @"🐌", @"🐙", @"🐚", @"🐟", @"🐬", @"🐋", @"🐐", @"🐊", @"🐫", @"🍀", @"🌹", @"🌻", @"🍁", @"🌾", @"🍄", @"🌵", @"🌴", @"🌳", @"🌞", @"🌚", @"🌙", @"🌎", @"🌋", @"⚡️", @"☔️", @"❄️", @"⛄️", @"🌀", @"🌈", @"🌊", @"🎓", @"🎆", @"🎃", @"👻", @"🎅", @"🎄", @"🎁", @"🎈", @"🔮", @"🎥", @"📷", @"💿", @"💻", @"☎️", @"📡", @"📺", @"📻", @"🔉", @"🔔", @"⏳", @"⏰", @"⌚️", @"🔒", @"🔑", @"🔎", @"💡", @"🔦", @"🔌", @"🔋", @"🚿", @"🚽", @"🔧", @"🔨", @"🚪", @"🚬", @"💣", @"🔫", @"🔪", @"💊", @"💉", @"💰", @"💵", @"💳", @"✉️", @"📫", @"📦", @"📅", @"📁", @"✂️", @"📌", @"📎", @"✒️", @"✏️", @"📐", @"📚", @"🔬", @"🔭", @"🎨", @"🎬", @"🎤", @"🎧", @"🎵", @"🎹", @"🎻", @"🎺", @"🎸", @"👾", @"🎮", @"🃏", @"🎲", @"🎯", @"🏈", @"🏀", @"⚽️", @"⚾️", @"🎾", @"🎱", @"🏉", @"🎳", @"🏁", @"🏇", @"🏆", @"🏊", @"🏄", @"☕️", @"🍼", @"🍺", @"🍷", @"🍴", @"🍕", @"🍔", @"🍟", @"🍗", @"🍱", @"🍚", @"🍜", @"🍡", @"🍳", @"🍞", @"🍩", @"🍦", @"🎂", @"🍰", @"🍪", @"🍫", @"🍭", @"🍯", @"🍎", @"🍏", @"🍊", @"🍋", @"🍒", @"🍇", @"🍉", @"🍓", @"🍑", @"🍌", @"🍐", @"🍍", @"🍆", @"🍅", @"🌽", @"🏡", @"🏥", @"🏦", @"⛪️", @"🏰", @"⛺️", @"🏭", @"🗻", @"🗽", @"🎠", @"🎡", @"⛲️", @"🎢", @"🚢", @"🚤", @"⚓️", @"🚀", @"✈️", @"🚁", @"🚂", @"🚋", @"🚎", @"🚌", @"🚙", @"🚗", @"🚕", @"🚛", @"🚨", @"🚔", @"🚒", @"🚑", @"🚲", @"🚠", @"🚜", @"🚦", @"⚠️", @"🚧", @"⛽️", @"🎰", @"🗿", @"🎪", @"🎭", @"🇯🇵", @"🇰🇷", @"🇩🇪", @"🇨🇳", @"🇺🇸", @"🇫🇷", @"🇪🇸", @"🇮🇹", @"🇷🇺", @"🇬🇧", @"1️⃣", @"2️⃣", @"3️⃣", @"4️⃣", @"5️⃣", @"6️⃣", @"7️⃣", @"8️⃣", @"9️⃣", @"0️⃣", @"🔟", @"❗️", @"❓", @"♥️", @"♦️", @"💯", @"🔗", @"🔱", @"🔴", @"🔵", @"🔶", @"🔷" ]; + + uint8_t bytes[32]; + [data getBytes:bytes length:32]; + + NSString *result = @""; + for (int32_t i = 0; i < count; i++) + { + int32_t position = positionExtractor(bytes, i, (int32_t)emojis.count); + NSString *emoji = emojis[position]; + result = [result stringByAppendingString:emoji]; + } + + return result; +} diff --git a/TelegramUI/SecretMediaPreviewController.swift b/TelegramUI/SecretMediaPreviewController.swift index ad00f85d53..4e18f2cdc2 100644 --- a/TelegramUI/SecretMediaPreviewController.swift +++ b/TelegramUI/SecretMediaPreviewController.swift @@ -24,12 +24,14 @@ public final class SecretMediaPreviewController: ViewController { private var messageView: MessageView? private var currentNodeMessageId: MessageId? + private let presentationData: PresentationData + public init(account: Account, messageId: MessageId) { self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - super.init() + super.init(navigationBarTheme: nil) - self.navigationBar.isHidden = true self.statusBar.alpha = 0.0 self.disposable.set((account.postbox.messageView(messageId) |> deliverOnMainQueue).start(next: { [weak self] view in @@ -68,7 +70,7 @@ public final class SecretMediaPreviewController: ViewController { if let messageView = self.messageView, let message = messageView.message { if self.currentNodeMessageId != message.id { self.currentNodeMessageId = message.id - let item = galleryItemForEntry(account: account, entry: .MessageEntry(message, false, nil, nil)) + let item = galleryItemForEntry(account: account, theme: self.presentationData.theme, strings: self.presentationData.strings, entry: .MessageEntry(message, false, nil, nil)) let itemNode = item.node() self.controllerNode.setItemNode(itemNode) diff --git a/TelegramUI/SelectivePrivacySettingsController.swift b/TelegramUI/SelectivePrivacySettingsController.swift index 6866a1464f..d297c5fbe3 100644 --- a/TelegramUI/SelectivePrivacySettingsController.swift +++ b/TelegramUI/SelectivePrivacySettingsController.swift @@ -58,14 +58,14 @@ private func stringForUserCount(_ count: Int) -> String { } private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { - case settingHeader(String) - case everybody(Bool) - case contacts(Bool) - case nobody(Bool) - case settingInfo(String) - case disableFor(String, Int) - case enableFor(String, Int) - case peersInfo + case settingHeader(PresentationTheme, String) + case everybody(PresentationTheme, String, Bool) + case contacts(PresentationTheme, String, Bool) + case nobody(PresentationTheme, String, Bool) + case settingInfo(PresentationTheme, String) + case disableFor(PresentationTheme, String, String) + case enableFor(PresentationTheme, String, String) + case peersInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -99,50 +99,50 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { static func ==(lhs: SelectivePrivacySettingsEntry, rhs: SelectivePrivacySettingsEntry) -> Bool { switch lhs { - case let .settingHeader(text): - if case .settingHeader(text) = rhs { + case let .settingHeader(lhsTheme, lhsText): + if case let .settingHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .everybody(value): - if case .everybody(value) = rhs { + case let .everybody(lhsTheme, lhsText, lhsValue): + if case let .everybody(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .contacts(value): - if case .contacts(value) = rhs { + case let .contacts(lhsTheme, lhsText, lhsValue): + if case let .contacts(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .nobody(value): - if case .nobody(value) = rhs { + case let .nobody(lhsTheme, lhsText, lhsValue): + if case let nobody(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .settingInfo(text): - if case .settingInfo(text) = rhs { + case let .settingInfo(lhsTheme, lhsText): + if case let .settingInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .disableFor(title, count): - if case .disableFor(title, count) = rhs { + case let .disableFor(lhsTheme, lhsText, lhsValue): + if case let .disableFor(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .enableFor(title, count): - if case .enableFor(title, count) = rhs { + case let .enableFor(lhsTheme, lhsText, lhsValue): + if case let .enableFor(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case .peersInfo: - if case .peersInfo = rhs { + case let .peersInfo(lhsTheme, lhsText): + if case let .peersInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -156,32 +156,32 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { func item(_ arguments: SelectivePrivacySettingsControllerArguments) -> ListViewItem { switch self { - case let .settingHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .everybody(value): - return ItemListCheckboxItem(title: "Everybody", checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + case let .settingHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .everybody(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateType(.everybody) }) - case let .contacts(value): - return ItemListCheckboxItem(title: "My Contacts", checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + case let .contacts(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateType(.contacts) }) - case let .nobody(value): - return ItemListCheckboxItem(title: "Nobody", checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + case let .nobody(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateType(.nobody) }) - case let .settingInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .disableFor(title, count): - return ItemListDisclosureItem(title: title, label: stringForUserCount(count), sectionId: self.section, style: .blocks, action: { + case let .settingInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .disableFor(theme, title, value): + return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.openDisableFor() }) - case let .enableFor(title, count): - return ItemListDisclosureItem(title: title, label: stringForUserCount(count), sectionId: self.section, style: .blocks, action: { + case let .enableFor(theme, title, value): + return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.openEnableFor() }) - case .peersInfo: - return ItemListTextItem(text: .plain("These settings will override the values above."), sectionId: self.section) + case let .peersInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } @@ -234,7 +234,7 @@ private struct SelectivePrivacySettingsControllerState: Equatable { } } -private func selectivePrivacySettingsControllerEntries(kind: SelectivePrivacySettingsKind, state: SelectivePrivacySettingsControllerState) -> [SelectivePrivacySettingsEntry] { +private func selectivePrivacySettingsControllerEntries(presentationData: PresentationData, kind: SelectivePrivacySettingsKind, state: SelectivePrivacySettingsControllerState) -> [SelectivePrivacySettingsEntry] { var entries: [SelectivePrivacySettingsEntry] = [] let settingTitle: String @@ -243,44 +243,44 @@ private func selectivePrivacySettingsControllerEntries(kind: SelectivePrivacySet let enableForText: String switch kind { case .presence: - settingTitle = "WHO CAN SEE MY TIMESTAMP" - settingInfoText = "Important: you won't be able to see Last Seen times for people with whom you don't share your Last Seen time. Approximate last seen will be shown instead (recently, within a week, within a month)." - disableForText = "Never Share With" - enableForText = "Always Share With" + settingTitle = presentationData.strings.PrivacyLastSeenSettings_WhoCanSeeMyTimestamp + settingInfoText = presentationData.strings.PrivacyLastSeenSettings_CustomHelp + disableForText = presentationData.strings.PrivacyLastSeenSettings_NeverShareWith + enableForText = presentationData.strings.PrivacyLastSeenSettings_AlwaysShareWith case .groupInvitations: - settingTitle = "WHO CAN ADD ME TO GROUP CHATS" - settingInfoText = "You can restrict who can add you to groups and channels with granular precision." - disableForText = "Never Allow" - enableForText = "Always Allow" + settingTitle = presentationData.strings.Privacy_GroupsAndChannels_WhoCanAddMe + settingInfoText = presentationData.strings.Privacy_GroupsAndChannels_CustomHelp + disableForText = presentationData.strings.Privacy_GroupsAndChannels_NeverAllow + enableForText = presentationData.strings.Privacy_GroupsAndChannels_AlwaysAllow case .voiceCalls: - settingTitle = "WHO CAN CALL ME" - settingInfoText = "You can restrict who can call you with granular precision." - disableForText = "Never Allow" - enableForText = "Always Allow" + settingTitle = presentationData.strings.Privacy_Calls_WhoCanCallMe + settingInfoText = presentationData.strings.Privacy_Calls_CustomHelp + disableForText = presentationData.strings.Privacy_GroupsAndChannels_NeverAllow + enableForText = presentationData.strings.Privacy_GroupsAndChannels_AlwaysAllow } - entries.append(.settingHeader(settingTitle)) + entries.append(.settingHeader(presentationData.theme, settingTitle)) - entries.append(.everybody(state.setting == .everybody)) - entries.append(.contacts(state.setting == .contacts)) + entries.append(.everybody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenEverybody, state.setting == .everybody)) + entries.append(.contacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.setting == .contacts)) switch kind { case .presence, .voiceCalls: - entries.append(.nobody(state.setting == .nobody)) + entries.append(.nobody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenNobody, state.setting == .nobody)) case .groupInvitations: break } - entries.append(.settingInfo(settingInfoText)) + entries.append(.settingInfo(presentationData.theme, settingInfoText)) switch state.setting { case .everybody: - entries.append(.disableFor(disableForText, state.disableFor.count)) + entries.append(.disableFor(presentationData.theme, disableForText, stringForUserCount(state.disableFor.count))) case .contacts: - entries.append(.disableFor(disableForText, state.disableFor.count)) - entries.append(.enableFor(enableForText, state.enableFor.count)) + entries.append(.disableFor(presentationData.theme, disableForText, stringForUserCount(state.disableFor.count))) + entries.append(.enableFor(presentationData.theme, enableForText, stringForUserCount(state.enableFor.count))) case .nobody: - entries.append(.enableFor(enableForText, state.enableFor.count)) + entries.append(.enableFor(presentationData.theme, enableForText, stringForUserCount(state.enableFor.count))) } - entries.append(.peersInfo) + entries.append(.peersInfo(presentationData.theme, presentationData.strings.PrivacyLastSeenSettings_CustomShareSettingsHelp)) return entries } @@ -360,10 +360,10 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy })) }) - let signal = statePromise.get() |> deliverOnMainQueue - |> map { state -> (ItemListControllerState, (ItemListNodeState, SelectivePrivacySettingsEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> deliverOnMainQueue + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, SelectivePrivacySettingsEntry.ItemGenerationArguments)) in - let leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { dismissImpl?() }) @@ -371,7 +371,7 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy if state.saving { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { var wasSaving = false var settings: SelectivePrivacySettings? updateState { state in @@ -412,22 +412,21 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy let title: String switch kind { case .presence: - title = "Last Seen" + title = presentationData.strings.PrivacySettings_LastSeen case .groupInvitations: - title = "Groups" + title = presentationData.strings.Privacy_GroupsAndChannels case .voiceCalls: - title = "Voice Calls" + title = presentationData.strings.Settings_CallSettings } - let controllerState = ItemListControllerState(title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, animateChanges: false) - let listState = ItemListNodeState(entries: selectivePrivacySettingsControllerEntries(kind: kind, state: state), style: .blocks, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: selectivePrivacySettingsControllerEntries(presentationData: presentationData, kind: kind, state: state), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + let controller = ItemListController(account: account, state: signal) pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } @@ -435,7 +434,7 @@ func selectivePrivacySettingsController(account: Account, kind: SelectivePrivacy controller?.present(c, in: .window) } dismissImpl = { [weak controller] in - (controller?.navigationController as? NavigationController)?.popViewController(animated: true) + let _ = (controller?.navigationController as? NavigationController)?.popViewController(animated: true) } return controller diff --git a/TelegramUI/SelectivePrivacySettingsPeersController.swift b/TelegramUI/SelectivePrivacySettingsPeersController.swift index 26d06990ef..11dc31771e 100644 --- a/TelegramUI/SelectivePrivacySettingsPeersController.swift +++ b/TelegramUI/SelectivePrivacySettingsPeersController.swift @@ -4,8 +4,6 @@ import SwiftSignalKit import Postbox import TelegramCore -private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed() - private final class SelectivePrivacyPeersControllerArguments { let account: Account @@ -58,8 +56,8 @@ private enum SelectivePrivacyPeersEntryStableId: Hashable { } private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { - case peerItem(Int32, Peer, ItemListPeerItemEditing, Bool) - case addItem(Bool) + case peerItem(Int32, PresentationTheme, PresentationStrings, Peer, ItemListPeerItemEditing, Bool) + case addItem(PresentationTheme, String, Bool) var section: ItemListSectionId { switch self { @@ -72,7 +70,7 @@ private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { var stableId: SelectivePrivacyPeersEntryStableId { switch self { - case let .peerItem(_, peer, _, _): + case let .peerItem(_, _, _, peer, _, _): return .peer(peer.id) case .addItem: return .add @@ -81,14 +79,20 @@ private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { static func ==(lhs: SelectivePrivacyPeersEntry, rhs: SelectivePrivacyPeersEntry) -> Bool { switch lhs { - case let .peerItem(lhsIndex, lhsPeer, lhsEditing, lhsEnabled): - if case let .peerItem(rhsIndex, rhsPeer, rhsEditing, rhsEnabled) = rhs { + case let .peerItem(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsEditing, lhsEnabled): + if case let .peerItem(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsEditing, rhsEnabled) = rhs { if lhsIndex != rhsIndex { return false } if !lhsPeer.isEqual(rhsPeer) { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if lhsEditing != rhsEditing { return false } @@ -99,8 +103,8 @@ private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { } else { return false } - case let .addItem(editing): - if case .addItem(editing) = rhs { + case let .addItem(lhsTheme, lhsText, lhsEditing): + if case let .addItem(rhsTheme, rhsText, rhsEditing) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEditing == rhsEditing { return true } else { return false @@ -110,9 +114,9 @@ private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { static func <(lhs: SelectivePrivacyPeersEntry, rhs: SelectivePrivacyPeersEntry) -> Bool { switch lhs { - case let .peerItem(index, _, _, _): + case let .peerItem(index, _, _, _, _, _): switch rhs { - case let .peerItem(rhsIndex, _, _, _): + case let .peerItem(rhsIndex, _, _, _, _, _): return index < rhsIndex case .addItem: return true @@ -124,14 +128,14 @@ private enum SelectivePrivacyPeersEntry: ItemListNodeEntry { func item(_ arguments: SelectivePrivacyPeersControllerArguments) -> ListViewItem { switch self { - case let .peerItem(_, peer, editing, enabled): - return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in + case let .peerItem(_, theme, strings, peer, editing, enabled): + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .none, label: .none, editing: editing, switchValue: nil, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removePeer(peerId) }) - case let .addItem(editing): - return ItemListPeerActionItem(icon: addMemberPlusIcon, title: "Add New...", sectionId: self.section, editing: editing, action: { + case let .addItem(theme, text, editing): + return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: editing, action: { arguments.addPeer() }) } @@ -171,16 +175,16 @@ private struct SelectivePrivacyPeersControllerState: Equatable { } } -private func selectivePrivacyPeersControllerEntries(state: SelectivePrivacyPeersControllerState, peers: [Peer]) -> [SelectivePrivacyPeersEntry] { +private func selectivePrivacyPeersControllerEntries(presentationData: PresentationData, state: SelectivePrivacyPeersControllerState, peers: [Peer]) -> [SelectivePrivacyPeersEntry] { var entries: [SelectivePrivacyPeersEntry] = [] var index: Int32 = 0 for peer in peers { - entries.append(.peerItem(index, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.id == state.peerIdWithRevealedOptions), true)) + entries.append(.peerItem(index, presentationData.theme, presentationData.strings, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.id == state.peerIdWithRevealedOptions), true)) index += 1 } - entries.append(.addItem(state.editing)) + entries.append(.addItem(presentationData.theme, presentationData.strings.BlockedUsers_AddNew, state.editing)) return entries } @@ -273,19 +277,19 @@ public func selectivePrivacyPeersController(account: Account, title: String, ini var previousPeers: [Peer]? - let signal = combineLatest(statePromise.get(), peersPromise.get()) + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peersPromise.get()) |> deliverOnMainQueue - |> map { state, peers -> (ItemListControllerState, (ItemListNodeState, SelectivePrivacyPeersEntry.ItemGenerationArguments)) in + |> map { presentationData, state, peers -> (ItemListControllerState, (ItemListNodeState, SelectivePrivacyPeersEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? if !peers.isEmpty { if state.editing { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { updateState { state in return state.withUpdatedEditing(false) } }) } else { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { updateState { state in return state.withUpdatedEditing(true) } @@ -296,15 +300,15 @@ public func selectivePrivacyPeersController(account: Account, title: String, ini let previous = previousPeers previousPeers = peers - let controllerState = ItemListControllerState(title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) - let listState = ItemListNodeState(entries: selectivePrivacyPeersControllerEntries(state: state, peers: peers), style: .blocks, emptyStateItem: nil, animateChanges: previous != nil && previous!.count >= peers.count) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(entries: selectivePrivacyPeersControllerEntries(presentationData: presentationData, state: state, peers: peers), style: .blocks, emptyStateItem: nil, animateChanges: previous != nil && previous!.count >= peers.count) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/SettingsAccountInfoItem.swift b/TelegramUI/SettingsAccountInfoItem.swift deleted file mode 100644 index 8c10a6c8df..0000000000 --- a/TelegramUI/SettingsAccountInfoItem.swift +++ /dev/null @@ -1,134 +0,0 @@ -import Foundation -import Display -import SwiftSignalKit -import AsyncDisplayKit -import Postbox -import TelegramCore - -class SettingsAccountInfoItem: ListControllerGroupableItem { - let account: Account - let peer: Peer? - let connectionStatus: ConnectionStatus - - init(account: Account, peer: Peer?, connectionStatus: ConnectionStatus) { - self.account = account - self.peer = peer - self.connectionStatus = connectionStatus - } - - func setupNode(async: @escaping (@escaping () -> Void) -> Void, completion: @escaping (ListControllerGroupableItemNode) -> Void) { - async { - let node = SettingsAccountInfoItemNode() - completion(node) - } - } -} - -private let nameFont = Font.medium(19.0) -private let statusFont = Font.regular(15.0) - -class SettingsAccountInfoItemNode: ListControllerGroupableItemNode { - let avatarNode: AvatarNode - - let nameNode: TextNode - let statusNode: TextNode - - override init() { - self.avatarNode = AvatarNode(font: Font.regular(20.0)) - - self.nameNode = TextNode() - self.nameNode.isLayerBacked = true - self.nameNode.contentMode = .left - self.nameNode.contentsScale = UIScreen.main.scale - - self.statusNode = TextNode() - self.statusNode.isLayerBacked = true - self.statusNode.contentMode = .left - self.statusNode.contentsScale = UIScreen.main.scale - - super.init() - - self.addSubnode(self.avatarNode) - self.addSubnode(self.nameNode) - self.addSubnode(self.statusNode) - } - - deinit { - } - - override func asyncLayoutContent() -> (_ item: ListControllerGroupableItem, _ width: CGFloat) -> (CGSize, () -> Void) { - let layoutNameNode = TextNode.asyncLayout(self.nameNode) - let layoutStatusNode = TextNode.asyncLayout(self.statusNode) - - return { item, width in - if let item = item as? SettingsAccountInfoItem { - let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: item.peer?.displayTitle ?? "", font: nameFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - - let statusText: String - let statusColor: UIColor - switch item.connectionStatus { - case .WaitingForNetwork: - statusText = "waiting for network" - statusColor = UIColor(0xb3b3b3) - case .Connecting: - statusText = "waiting for network" - statusColor = UIColor(0xb3b3b3) - case .Updating: - statusText = "updating" - statusColor = UIColor(0xb3b3b3) - case .Online: - statusText = "online" - statusColor = UIColor(0x007ee5) - } - - let (statusNodeLayout, statusNodeApply) = layoutStatusNode(NSAttributedString(string: statusText, font: statusFont, textColor: statusColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - - return (CGSize(width: width, height: 97.0), { [weak self] in - if let strongSelf = self { - let _ = nameNodeApply() - let _ = statusNodeApply() - - if let peer = item.peer { - strongSelf.avatarNode.setPeer(account: item.account, peer: peer) - } - - strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 15.0, y: 15.0), size: CGSize(width: 66.0, height: 66.0)) - strongSelf.nameNode.frame = CGRect(origin: CGPoint(x: 94.0, y: 25.0), size: nameNodeLayout.size) - - - strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: 94.0, y: 25.0 + nameNodeLayout.size.height + 4.0), size: statusNodeLayout.size) - } - }) - } else { - return (CGSize(width: width, height: 0.0), { - }) - } - } - } - - func setupWithAccount1(account: Account, peer: Peer?) { - /*self.peerDisposable.set((account.postbox.peerWithId(account.peerId) - |> deliverOnMainQueue).start(next: {[weak self] peer in - if let strongSelf = self { - strongSelf.avatarNode.setPeer(account, peer: peer) - let width = strongSelf.bounds.size.width - if width > CGFloat.ulpOfOne { - strongSelf.layoutContentForWidth(width) - strongSelf.nameNode.setNeedsDisplay() - } - } - })) - self.connectingStatusDisposable.set((account.network.connectionStatus - |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self { - - //strongSelf.statusNode.attributedString = NSAttributedString(string: statusText, font: statusFont, textColor: statusColor) - let width = strongSelf.bounds.size.width - if width > CGFloat.ulpOfOne { - strongSelf.layoutContentForWidth(width) - strongSelf.statusNode.setNeedsDisplay() - } - } - }))*/ - } -} diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index 7588a6cb25..1e99e18e42 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -15,10 +15,13 @@ private struct SettingsItemArguments { let changeProfilePhoto: () -> Void let openPrivacyAndSecurity: () -> Void let openDataAndStorage: () -> Void + let openThemes: () -> Void + let openTheme: (TelegramWallpaper) -> Void let pushController: (ViewController) -> Void let presentController: (ViewController) -> Void let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void let saveEditingState: () -> Void + let openLanguage: () -> Void let openSupport: () -> Void let openFaq: () -> Void let logout: () -> Void @@ -33,29 +36,31 @@ private enum SettingsSection: Int32 { } private enum SettingsEntry: ItemListNodeEntry { - case userInfo(Peer?, CachedPeerData?, ItemListAvatarAndNameInfoItemState, TelegramMediaImageRepresentation?) - case setProfilePhoto + case userInfo(PresentationTheme, PresentationStrings, Peer?, CachedPeerData?, ItemListAvatarAndNameInfoItemState, TelegramMediaImageRepresentation?) + case setProfilePhoto(PresentationTheme, String) - case notificationsAndSounds - case privacyAndSecurity - case dataAndStorage - case stickers - case phoneNumber(String) - case username(String) - case askAQuestion(Bool) - case faq - case debug - case logOut + case notificationsAndSounds(PresentationTheme, String) + case privacyAndSecurity(PresentationTheme, String) + case dataAndStorage(PresentationTheme, String) + case stickers(PresentationTheme, String) + case themes(PresentationTheme, String, [TelegramWallpaper]) + case phoneNumber(PresentationTheme, String, String) + case username(PresentationTheme, String, String) + case language(PresentationTheme, String, String) + case askAQuestion(PresentationTheme, String, Bool) + case faq(PresentationTheme, String) + case debug(PresentationTheme, String) + case logOut(PresentationTheme, String) var section: ItemListSectionId { switch self { case .userInfo, .setProfilePhoto: return SettingsSection.info.rawValue - case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .stickers: + case .notificationsAndSounds, .privacyAndSecurity, .dataAndStorage, .stickers, .themes: return SettingsSection.generalSettings.rawValue case .phoneNumber, .username: return SettingsSection.accountSettings.rawValue - case .askAQuestion, .faq, .debug: + case .language, .askAQuestion, .faq, .debug: return SettingsSection.help.rawValue case .logOut: return SettingsSection.logOut.rawValue @@ -76,25 +81,35 @@ private enum SettingsEntry: ItemListNodeEntry { return 4 case .stickers: return 5 - case .phoneNumber: + case .themes: return 6 - case .username: + case .phoneNumber: return 7 - case .askAQuestion: + case .username: return 8 - case .faq: + case .askAQuestion: return 9 - case .debug: + case .language: return 10 - case .logOut: + case .faq: return 11 + case .debug: + return 12 + case .logOut: + return 13 } } static func ==(lhs: SettingsEntry, rhs: SettingsEntry) -> Bool { switch lhs { - case let .userInfo(lhsPeer, lhsCachedData, lhsEditingState, lhsUpdatingImage): - if case let .userInfo(rhsPeer, rhsCachedData, rhsEditingState, rhsUpdatingImage) = rhs { + case let .userInfo(lhsTheme, lhsStrings, lhsPeer, lhsCachedData, lhsEditingState, lhsUpdatingImage): + if case let .userInfo(rhsTheme, rhsStrings, rhsPeer, rhsCachedData, rhsEditingState, rhsUpdatingImage) = rhs { + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false @@ -119,68 +134,80 @@ private enum SettingsEntry: ItemListNodeEntry { } else { return false } - case .setProfilePhoto: - if case .setProfilePhoto = rhs { + case let .setProfilePhoto(lhsTheme, lhsText): + if case let .setProfilePhoto(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case .notificationsAndSounds: - if case .notificationsAndSounds = rhs { + case let .notificationsAndSounds(lhsTheme, lhsText): + if case let .notificationsAndSounds(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case .privacyAndSecurity: - if case .privacyAndSecurity = rhs { + case let .privacyAndSecurity(lhsTheme, lhsText): + if case let .privacyAndSecurity(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case .dataAndStorage: - if case .dataAndStorage = rhs { + case let .dataAndStorage(lhsTheme, lhsText): + if case let .dataAndStorage(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case .stickers: - if case .stickers = rhs { + case let .stickers(lhsTheme, lhsText): + if case let .stickers(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .phoneNumber(number): - if case .phoneNumber(number) = rhs { + case let .themes(lhsTheme, lhsText, lhsWallpapers): + if case let .themes(rhsTheme, rhsText, rhsWallpapers) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsWallpapers == rhsWallpapers { return true } else { return false } - case let .username(address): - if case .username(address) = rhs { + case let .phoneNumber(lhsTheme, lhsText, lhsNumber): + if case let .phoneNumber(rhsTheme, rhsText, rhsNumber) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsNumber == rhsNumber { return true } else { return false } - case let .askAQuestion(loading): - if case .askAQuestion(loading) = rhs { + case let .username(lhsTheme, lhsText, lhsAddress): + if case let .username(rhsTheme, rhsText, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsAddress == rhsAddress { return true } else { return false } - case .faq: - if case .faq = rhs { + case let .language(lhsTheme, lhsText, lhsValue): + if case let .language(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case .debug: - if case .debug = rhs { + case let .askAQuestion(lhsTheme, lhsText, lhsLoading): + if case let .askAQuestion(rhsTheme, rhsText, rhsLoading) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsLoading == rhsLoading { return true } else { return false } - case .logOut: - if case .logOut = rhs { + case let .faq(lhsTheme, lhsText): + if case let .faq(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .debug(lhsTheme, lhsText): + if case let .debug(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .logOut(lhsTheme, lhsText): + if case let .logOut(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -194,54 +221,64 @@ private enum SettingsEntry: ItemListNodeEntry { func item(_ arguments: SettingsItemArguments) -> ListViewItem { switch self { - case let .userInfo(peer, cachedData, state, updatingImage): - return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: TelegramUserPresence(status: .present(until: Int32.max)), cachedData: cachedData, state: state, sectionId: ItemListSectionId(self.section), style: .blocks, editingNameUpdated: { editingName in + case let .userInfo(theme, strings, peer, cachedData, state, updatingImage): + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: TelegramUserPresence(status: .present(until: Int32.max)), cachedData: cachedData, state: state, sectionId: ItemListSectionId(self.section), style: .blocks(withTopInset: false), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { arguments.avatarTapAction() }, context: arguments.avatarAndNameInfoContext, updatingImage: updatingImage) - case .setProfilePhoto: - return ItemListActionItem(title: "Set Profile Photo", kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .setProfilePhoto(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.changeProfilePhoto() }) - case .notificationsAndSounds: - return ItemListDisclosureItem(title: "Notifications and Sounds", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .notificationsAndSounds(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.pushController(notificationsAndSoundsController(account: arguments.account)) }) - case .privacyAndSecurity: - return ItemListDisclosureItem(title: "Privacy and Security", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .privacyAndSecurity(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openPrivacyAndSecurity() }) - case .dataAndStorage: - return ItemListDisclosureItem(title: "Data and Storage", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .dataAndStorage(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openDataAndStorage() }) - case .stickers: - return ItemListDisclosureItem(title: "Stickers", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .stickers(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.pushController(installedStickerPacksController(account: arguments.account, mode: .general)) }) - case let .phoneNumber(number): - return ItemListDisclosureItem(title: "Phone Number", label: number, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .themes(theme, text, wallpapers): + return SettingsThemesItem(account: arguments.account, theme: theme, title: text, sectionId: self.section, action: { + arguments.openThemes() + }, openWallpaper: { wallpaper in + arguments.openTheme(wallpaper) + }, wallpapers: wallpapers) + case let .phoneNumber(theme, text, number): + return ItemListDisclosureItem(theme: theme, title: text, label: number, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.pushController(ChangePhoneNumberIntroController(account: arguments.account, phoneNumber: number)) }) - case let .username(address): - return ItemListDisclosureItem(title: "Username", label: address, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .username(theme, text, address): + return ItemListDisclosureItem(theme: theme, title: text, label: address, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.presentController(usernameSetupController(account: arguments.account)) }) - case let .askAQuestion(askAQuestion): - return ItemListDisclosureItem(title: "Ask a Question", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .language(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + arguments.openLanguage() + }) + case let .askAQuestion(theme, text, loading): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openSupport() }) - case .faq: - return ItemListDisclosureItem(title: "Telegram FAQ", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .faq(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.openFaq() }) - case .debug: - return ItemListDisclosureItem(title: "Debug", label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .debug(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.pushController(debugController(account: arguments.account, accountManager: arguments.accountManager)) }) - case .logOut: - return ItemListActionItem(title: "Log Out", kind: .destructive, alignment: .center, sectionId: ItemListSectionId(self.section), style: .blocks, action: { + case let .logOut(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .center, sectionId: ItemListSectionId(self.section), style: .blocks, action: { arguments.logout() }) } @@ -299,30 +336,32 @@ private struct SettingsState: Equatable { } } -private func settingsEntries(state: SettingsState, view: PeerView) -> [SettingsEntry] { +private func settingsEntries(presentationData: PresentationData, state: SettingsState, view: PeerView, wallpapers: [TelegramWallpaper]) -> [SettingsEntry] { var entries: [SettingsEntry] = [] if let peer = peerViewMainPeer(view) as? TelegramUser { let userInfoState = ItemListAvatarAndNameInfoItemState(editingName: state.editingState?.editingName, updatingName: state.updatingName) - entries.append(.userInfo(peer, view.cachedData, userInfoState, state.updatingAvatar)) - entries.append(.setProfilePhoto) + entries.append(.userInfo(presentationData.theme, presentationData.strings, peer, view.cachedData, userInfoState, state.updatingAvatar)) + entries.append(.setProfilePhoto(presentationData.theme, presentationData.strings.Settings_SetProfilePhoto)) - entries.append(.notificationsAndSounds) - entries.append(.privacyAndSecurity) - entries.append(.dataAndStorage) - entries.append(.stickers) + entries.append(.notificationsAndSounds(presentationData.theme, presentationData.strings.Settings_NotificationsAndSounds)) + entries.append(.privacyAndSecurity(presentationData.theme, presentationData.strings.Settings_PrivacySettings)) + entries.append(.dataAndStorage(presentationData.theme, presentationData.strings.Settings_ChatSettings)) + entries.append(.stickers(presentationData.theme, presentationData.strings.ChatSettings_Stickers)) + entries.append(.themes(presentationData.theme, presentationData.strings.Settings_ChatBackground, wallpapers)) if let phone = peer.phone { - entries.append(.phoneNumber(formatPhoneNumber(phone))) + entries.append(.phoneNumber(presentationData.theme, presentationData.strings.Settings_PhoneNumber, formatPhoneNumber(phone))) } - entries.append(.username(peer.addressName == nil ? "" : ("@" + peer.addressName!))) + entries.append(.username(presentationData.theme, presentationData.strings.Settings_Username, peer.addressName == nil ? "" : ("@" + peer.addressName!))) - entries.append(.askAQuestion(state.loadingSupportPeer)) - entries.append(.faq) - entries.append(.debug) + entries.append(.askAQuestion(presentationData.theme, presentationData.strings.Settings_Support, state.loadingSupportPeer)) + entries.append(.language(presentationData.theme, presentationData.strings.Settings_AppLanguage, presentationData.strings.Localization_LanguageName)) + entries.append(.faq(presentationData.theme, presentationData.strings.Settings_FAQ)) + entries.append(.debug(presentationData.theme, "Debug")) if let _ = state.editingState { - entries.append(.logOut) + entries.append(.logOut(presentationData.theme, presentationData.strings.Settings_Logout)) } } @@ -359,6 +398,9 @@ public func settingsController(account: Account, accountManager: AccountManager) let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext() var updateHiddenAvatarImpl: (() -> Void)? + let wallpapersPromise = Promise<[TelegramWallpaper]>() + wallpapersPromise.set(telegramWallpapers(account: account)) + let arguments = SettingsItemArguments(account: account, accountManager: accountManager, avatarAndNameInfoContext: avatarAndNameInfoContext, avatarTapAction: { var updating = false updateState { @@ -429,6 +471,15 @@ public func settingsController(account: Account, accountManager: AccountManager) pushControllerImpl?(privacyAndSecurityController(account: account, initialSettings: .single(nil) |> then(requestAccountPrivacySettings(account: account) |> map { Optional($0) }))) }, openDataAndStorage: { pushControllerImpl?(dataAndStorageController(account: account)) + }, openThemes: { + pushControllerImpl?(ThemeGridController(account: account)) + }, openTheme: { wallpaper in + let _ = (wallpapersPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { wallpapers in + let controller = ThemeGalleryController(account: account, wallpapers: wallpapers, at: wallpaper) + presentControllerImpl?(controller, ThemePreviewControllerPresentationArguments(transitionArguments: { entry -> GalleryTransitionArguments? in + return nil + })) + }) }, pushController: { controller in pushControllerImpl?(controller) }, presentController: { controller in @@ -460,6 +511,9 @@ public func settingsController(account: Account, accountManager: AccountManager) } }).start()) } + }, openLanguage: { + let controller = LanguageSelectionController(account: account) + presentControllerImpl?(controller, nil) }, openSupport: { var load = false updateState { state in @@ -485,7 +539,7 @@ public func settingsController(account: Account, accountManager: AccountManager) } if let applicationContext = account.applicationContext as? TelegramApplicationContext { - applicationContext.openUrl(faqUrl) + applicationContext.applicationBindings.openUrl(faqUrl) } }, logout: { let alertController = standardTextAlertController(title: NSLocalizedString("Settings.LogoutConfirmationTitle", comment: ""), text: NSLocalizedString("Settings.LogoutConfirmationText", comment: ""), actions: [ @@ -500,16 +554,16 @@ public func settingsController(account: Account, accountManager: AccountManager) let peerView = account.viewTracker.peerView(account.peerId) - let signal = combineLatest(statePromise.get(), peerView) - |> map { state, view -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peerView, wallpapersPromise.get()) + |> map { presentationData, state, view, wallpapers -> (ItemListControllerState, (ItemListNodeState, SettingsEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) let rightNavigationButton: ItemListNavigationButton if let _ = state.editingState { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: true, action: { arguments.saveEditingState() }) } else { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { if let peer = peer as? TelegramUser { updateState { state in return state.withUpdatedEditingState(SettingsEditingState(editingName: ItemListAvatarAndNameInfoItemName(peer.indexName))) @@ -518,19 +572,17 @@ public func settingsController(account: Account, accountManager: AccountManager) }) } - let controllerState = ItemListControllerState(title: .text("Settings"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton) - let listState = ItemListNodeState(entries: settingsEntries(state: state, view: view), style: .blocks) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Settings_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: settingsEntries(presentationData: presentationData, state: state, view: view, wallpapers: wallpapers), style: .blocks) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) - controller.tabBarItem.title = "Settings" - controller.tabBarItem.image = UIImage(bundleImageName: "Chat List/Tabs/IconSettings")?.precomposed() - controller.tabBarItem.selectedImage = UIImage(bundleImageName: "Chat List/Tabs/IconSettingsSelected")?.precomposed() + let controller = ItemListController(account: account, state: signal, tabBarItem: (account.applicationContext as! TelegramApplicationContext).presentationData |> map { presentationData in + return ItemListControllerTabBarItem(title: presentationData.strings.Settings_Title, image: PresentationResourcesRootController.tabSettingsIcon(presentationData.theme), selectedImage: PresentationResourcesRootController.tabSettingsSelectedIcon(presentationData.theme)) + }) pushControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.pushViewController(value) } diff --git a/TelegramUI/SettingsThemeWallpaperNode.swift b/TelegramUI/SettingsThemeWallpaperNode.swift new file mode 100644 index 0000000000..26d89979f1 --- /dev/null +++ b/TelegramUI/SettingsThemeWallpaperNode.swift @@ -0,0 +1,69 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +final class SettingsThemeWallpaperNode: ASDisplayNode { + private var wallpaper: TelegramWallpaper? + + let buttonNode = HighlightTrackingButtonNode() + let backgroundNode = ASDisplayNode() + let imageNode = TransformImageNode() + + var pressed: (() -> Void)? + + override init() { + super.init() + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.imageNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + func setWallpaper(account: Account, wallpaper: TelegramWallpaper, size: CGSize) { + self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) + self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size) + self.imageNode.frame = CGRect(origin: CGPoint(), size: size) + + if self.wallpaper != wallpaper { + self.wallpaper = wallpaper + switch wallpaper { + case .builtin: + self.imageNode.isHidden = false + self.backgroundNode.isHidden = true + self.imageNode.setSignal(account: account, signal: settingsBuiltinWallpaperImage(account: account)) + let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets())) + apply() + case let .color(color): + self.imageNode.isHidden = true + self.backgroundNode.isHidden = false + self.backgroundNode.backgroundColor = UIColor(rgb: UInt32(bitPattern: color)) + case let .image(representations): + self.imageNode.isHidden = false + self.backgroundNode.isHidden = true + self.imageNode.setSignal(account: account, signal: chatAvatarGalleryPhoto(account: account, representations: representations, autoFetchFullSize: true)) + let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: largestImageRepresentation(representations)!.dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets())) + apply() + } + } else if let wallpaper = self.wallpaper { + switch wallpaper { + case .builtin: + let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets())) + apply() + case .color: + break + case let .image(representations): + let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: largestImageRepresentation(representations)!.dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets())) + apply() + } + } + } + + @objc func buttonPressed() { + self.pressed?() + } +} diff --git a/TelegramUI/SettingsThemesItem.swift b/TelegramUI/SettingsThemesItem.swift new file mode 100644 index 0000000000..ec48913ce6 --- /dev/null +++ b/TelegramUI/SettingsThemesItem.swift @@ -0,0 +1,321 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore + + + +class SettingsThemesItem: ListViewItem, ItemListItem { + let account: Account + let theme: PresentationTheme + let title: String + let sectionId: ItemListSectionId + let action: () -> Void + let openWallpaper: (TelegramWallpaper) -> Void + let wallpapers: [TelegramWallpaper] + + init(account: Account, theme: PresentationTheme, title: String, sectionId: ItemListSectionId, action: @escaping () -> Void, openWallpaper: @escaping (TelegramWallpaper) -> Void, wallpapers: [TelegramWallpaper]) { + self.account = account + self.theme = theme + self.title = title + self.sectionId = sectionId + self.action = action + self.openWallpaper = openWallpaper + self.wallpapers = wallpapers + } + + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + async { + let node = SettingsThemesItemNode() + let (layout, apply) = node.asyncLayout()(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply() }) + }) + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? SettingsThemesItemNode { + Queue.mainQueue().async { + let makeLayout = node.asyncLayout() + + async { + let (layout, apply) = makeLayout(self, width, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) + Queue.mainQueue().async { + completion(layout, { + apply() + }) + } + } + } + } + } + + var selectable: Bool = true + + func selected(listView: ListView){ + listView.clearHighlightAnimated(true) + self.action() + } +} + +private let titleFont = Font.regular(17.0) + +class SettingsThemesItemNode: ListViewItemNode { + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + private let titleNode: TextNode + let arrowNode: ASImageNode + + private var item: SettingsThemesItem? + + private var thumbnailNodes: [SettingsThemeWallpaperNode] = [] + + var tag: Any? { + return self.item?.tag + } + + init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.isLayerBacked = true + + self.titleNode = TextNode() + self.titleNode.isLayerBacked = true + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.arrowNode = ASImageNode() + self.arrowNode.displayWithoutProcessing = true + self.arrowNode.displaysAsynchronously = false + self.arrowNode.isLayerBacked = true + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + super.init(layerBacked: false, dynamicBounce: false) + + self.addSubnode(self.titleNode) + self.addSubnode(self.arrowNode) + + for i in 0 ..< 5 { + let imageNode = SettingsThemeWallpaperNode() + self.thumbnailNodes.append(imageNode) + self.addSubnode(imageNode) + let index = i + imageNode.pressed = { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + if index < item.wallpapers.count { + item.openWallpaper(item.wallpapers[index]) + } + } + } + } + } + + func asyncLayout() -> (_ item: SettingsThemesItem, _ width: CGFloat, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + + let currentItem = self.item + + return { item, width, neighbors in + let textColor: UIColor = item.theme.list.itemPrimaryTextColor + + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + + var updateArrowImage: UIImage? + var updatedTheme: PresentationTheme? + + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.theme) + } + + let contentSize: CGSize + let insets: UIEdgeInsets + let separatorHeight = UIScreenPixel + + let referenceImageSize = CGSize(width: 108.0, height: 163.0) + + let leftInset: CGFloat = 16.0 + let padding: CGFloat = 16.0 + let minSpacing: CGFloat = 7.0 + + let imageCount = Int((width - padding * 2.0 + minSpacing) / (referenceImageSize.width + minSpacing)) + + let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((width - padding * 2.0 - max(0.0, CGFloat(imageCount - 1) * minSpacing)) / CGFloat(imageCount)), height: referenceImageSize.height)) + + let spacing = floor((width - padding * 2.0 - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount - 1)) + + contentSize = CGSize(width: width, height: imageSize.height + 58.0) + insets = itemListNeighborsGroupedInsets(neighbors) + + let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = layout.size + + return (layout, { [weak self] in + if let strongSelf = self { + strongSelf.item = item + + if let updateArrowImage = updateArrowImage { + strongSelf.arrowNode.image = updateArrowImage + } + + if let _ = updatedTheme { + strongSelf.topStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + } + + let _ = titleApply() + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 16.0 + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - titleLayout.size.height - 10.0), size: titleLayout.size) + if let arrowImage = strongSelf.arrowNode.image { + strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: width - 15.0 - arrowImage.size.width, y: contentSize.height - 26.0), size: arrowImage.size) + } + + let bounds = CGRect(origin: CGPoint(), size: contentSize) + + for i in 0 ..< strongSelf.thumbnailNodes.count { + + /*if (i >= (int)_imageViews.count) + { + imageView = [[TGRemoteImageView alloc] init]; + imageView.fadeTransition = true; + imageView.fadeTransitionDuration = 0.2; + imageView.clipsToBounds = true; + imageView.contentMode = UIViewContentModeScaleAspectFill; + + imageViewContainer = [[UIButton alloc] init]; + imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [imageViewContainer addSubview:imageView]; + + UIImageView *checkView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ModernWallpaperSelectedIndicator.png"]]; + checkView.frame = CGRectOffset(checkView.frame, imageView.frame.size.width - 5.0f - checkView.frame.size.width, imageView.frame.size.height - 4.0f - checkView.frame.size.height); + checkView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin; + checkView.tag = 100; + [imageView addSubview:checkView]; + + [self addSubview:imageViewContainer]; + [_imageViews addObject:imageViewContainer]; + + [imageViewContainer addTarget:self action:@selector(imageViewTapped:) forControlEvents:UIControlEventTouchUpInside]; + } + else + { + imageViewContainer = _imageViews[i]; + imageView = [imageViewContainer.subviews firstObject]; + } + + imageView.contentHints = _syncLoad ? TGRemoteImageContentHintLoadFromDiskSynchronously : 0; + + imageViewContainer.hidden = false;*/ + + let itemFrame = CGRect(x: (i == imageCount - 1 && item.wallpapers.count >= 3) ? (bounds.size.width - padding - imageSize.width) : (padding + CGFloat(i) * (imageSize.width + spacing)), y: 15.0, width: imageSize.width, height: imageSize.height) + strongSelf.thumbnailNodes[i].frame = itemFrame + + let imageNode = strongSelf.thumbnailNodes[i] + if i >= item.wallpapers.count || i >= imageCount { + imageNode.isHidden = true + } else { + imageNode.isHidden = false + imageNode.setWallpaper(account: item.account, wallpaper: item.wallpapers[i], size: itemFrame.size) + } + } + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) + } + }) + } + } + + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } +} diff --git a/TelegramUI/ShareActionButtonNode.swift b/TelegramUI/ShareActionButtonNode.swift index 1127c93b3a..fe4eccca47 100644 --- a/TelegramUI/ShareActionButtonNode.swift +++ b/TelegramUI/ShareActionButtonNode.swift @@ -2,7 +2,7 @@ import Foundation import AsyncDisplayKit import Display -private let badgeBackgroundImage = generateStretchableFilledCircleImage(diameter: 22.0, color: UIColor(0x007ee5)) +private let badgeBackgroundImage = generateStretchableFilledCircleImage(diameter: 22.0, color: UIColor(rgb: 0x007ee5)) final class ShareActionButtonNode: ASButtonNode { private let badgeLabel: ASTextNode diff --git a/TelegramUI/ShareController.swift b/TelegramUI/ShareController.swift index 1de851e360..57309061a6 100644 --- a/TelegramUI/ShareController.swift +++ b/TelegramUI/ShareController.swift @@ -13,12 +13,7 @@ private func canSendMessagesToPeer(_ peer: Peer) -> Bool { } else if let peer = peer as? TelegramChannel { switch peer.info { case .broadcast: - switch peer.role { - case .creator, .editor, .moderator: - return true - case .member: - return false - } + return peer.hasAdminRights(.canPostMessages) case .group: return true } @@ -53,9 +48,7 @@ public final class ShareController: ViewController { self.shareAction = shareAction self.defaultAction = defaultAction - super.init(navigationBar: NavigationBar()) - - self.navigationBar.isHidden = true + super.init(navigationBarTheme: nil) self.peers.set(account.postbox.tailChatListView(100) |> take(1) |> map { view -> [Peer] in var peers: [Peer] = [] diff --git a/TelegramUI/ShareControllerNode.swift b/TelegramUI/ShareControllerNode.swift index 15ecdbde39..f7717733c8 100644 --- a/TelegramUI/ShareControllerNode.swift +++ b/TelegramUI/ShareControllerNode.swift @@ -7,10 +7,10 @@ import TelegramCore private let defaultBackgroundColor: UIColor = UIColor(white: 1.0, alpha: 1.0) private let highlightedBackgroundColor: UIColor = UIColor(white: 0.9, alpha: 1.0) -private let separatorColor: UIColor = UIColor(0xbcbbc1) +private let separatorColor: UIColor = UIColor(rgb: 0xbcbbc1) private let subtitleFont = Font.regular(12.0) -private let subtitleColor = UIColor(0x7b7b81) +private let subtitleColor = UIColor(rgb: 0x7b7b81) private let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: .white) private let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: highlightedBackgroundColor) @@ -29,7 +29,7 @@ private let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) -final class ShareControllerNode: ASDisplayNode, UIScrollViewDelegate { +final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let account: Account private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -125,9 +125,7 @@ final class ShareControllerNode: ASDisplayNode, UIScrollViewDelegate { self.installActionSeparatorNode.displaysAsynchronously = false self.installActionSeparatorNode.backgroundColor = separatorColor - super.init(viewBlock: { - return UITracingLayerView() - }, didLoad: nil) + super.init() self.controllerInteraction = ShareControllerInteraction(togglePeer: { [weak self] peer in if let strongSelf = self { @@ -142,11 +140,11 @@ final class ShareControllerNode: ASDisplayNode, UIScrollViewDelegate { strongSelf.updateVisibleItemsSelection(animated: true) if strongSelf.selectedPeers.isEmpty { if let defaultAction = strongSelf.defaultAction { - strongSelf.installActionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(0x007ee5), for: .normal) + strongSelf.installActionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) } strongSelf.installActionButtonNode.badge = nil } else { - strongSelf.installActionButtonNode.setTitle("Send", with: Font.medium(20.0), with: UIColor(0x007ee5), for: .normal) + strongSelf.installActionButtonNode.setTitle("Send", with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) strongSelf.installActionButtonNode.badge = "\(strongSelf.selectedPeers.count)" } @@ -177,7 +175,7 @@ final class ShareControllerNode: ASDisplayNode, UIScrollViewDelegate { self.wrappingScrollNode.view.delegate = self self.addSubnode(self.wrappingScrollNode) - self.cancelButtonNode.setTitle("Cancel", with: Font.medium(20.0), with: UIColor(0x007ee5), for: .normal) + self.cancelButtonNode.setTitle("Cancel", with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) /*self.cancelButtonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { @@ -304,7 +302,7 @@ final class ShareControllerNode: ASDisplayNode, UIScrollViewDelegate { transition.updateFrame(node: self.installActionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) transition.updateFrame(node: self.installActionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) - self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0))), transition: transition), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth + 25.0), lineSpacing: 0.0)), transition: transition), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)))) if animateIn { @@ -457,7 +455,7 @@ final class ShareControllerNode: ASDisplayNode, UIScrollViewDelegate { self.installActionSeparatorNode.alpha = 1.0 if let defaultAction = defaultAction { - self.installActionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(0x007ee5), for: .normal) + self.installActionButtonNode.setTitle(defaultAction.title, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) } } @@ -484,6 +482,11 @@ final class ShareControllerNode: ASDisplayNode, UIScrollViewDelegate { if let result = self.installActionButtonNode.hitTest(self.installActionButtonNode.convert(point, from: self), with: event) { return result } + if self.bounds.contains(point) { + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) { + return self.dimNode.view + } + } return super.hitTest(point, with: event) } diff --git a/TelegramUI/ShareControllerPeerGridItem.swift b/TelegramUI/ShareControllerPeerGridItem.swift index a975fd12f4..9117dc10f9 100644 --- a/TelegramUI/ShareControllerPeerGridItem.swift +++ b/TelegramUI/ShareControllerPeerGridItem.swift @@ -16,7 +16,7 @@ final class ShareControllerInteraction { private let selectionBackgroundImage = generateImage(CGSize(width: 60.0 + 4.0, height: 60.0 + 4.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(0x007ee5).cgColor) + context.setFillColor(UIColor(rgb: 0x007ee5).cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.white.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: size.width - 4.0, height: size.height - 4.0))) @@ -103,7 +103,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { func setup(account: Account, peer: Peer) { if self.currentState == nil || self.currentState!.0 !== account || !arePeersEqual(self.currentState!.1, peer) { let text = peer.displayTitle - self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.currentSelected ? UIColor(0x007ee5) : UIColor.black, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: text, font: textFont, textColor: self.currentSelected ? UIColor(rgb: 0x007ee5) : UIColor.black, paragraphAlignment: .center) self.avatarNode.setPeer(account: account, peer: peer) self.currentState = (account, peer) self.setNeedsLayout() @@ -129,7 +129,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { self.currentSelected = selected if let (_, peer) = self.currentState { - self.textNode.attributedText = NSAttributedString(string: peer.displayTitle, font: textFont, textColor: selected ? UIColor(0x007ee5) : UIColor.black, paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: peer.displayTitle, font: textFont, textColor: selected ? UIColor(rgb: 0x007ee5) : UIColor.black, paragraphAlignment: .center) } if selected { diff --git a/TelegramUI/SoftwareVideoLayerFrameManager.swift b/TelegramUI/SoftwareVideoLayerFrameManager.swift index 3241e6d418..3e509114ba 100644 --- a/TelegramUI/SoftwareVideoLayerFrameManager.swift +++ b/TelegramUI/SoftwareVideoLayerFrameManager.swift @@ -11,7 +11,7 @@ private var nextWorker = 0 final class SoftwareVideoLayerFrameManager { private let fetchDisposable: Disposable private var dataDisposable = MetaDisposable() - private var source: SoftwareVideoSource? + private let source = Atomic(value: nil) private var baseTimestamp: Double? private var frames: [MediaTrackFrame] = [] @@ -40,7 +40,7 @@ final class SoftwareVideoLayerFrameManager { func start() { self.dataDisposable.set((self.account.postbox.mediaBox.resourceData(self.resource, option: .complete(waitUntilFetchStatus: false)) |> deliverOn(applyQueue)).start(next: { [weak self] data in if let strongSelf = self, data.complete { - strongSelf.source = SoftwareVideoSource(path: data.path) + let _ = strongSelf.source.swap(SoftwareVideoSource(path: data.path)) } })) } @@ -89,7 +89,7 @@ final class SoftwareVideoLayerFrameManager { return } if let strongSelf = self { - let frameAndLoop = strongSelf.source?.readFrame(maxPts: maxPts) + let frameAndLoop = (strongSelf.source.with { $0 })?.readFrame(maxPts: maxPts) applyQueue.async { if let strongSelf = self { diff --git a/TelegramUI/StickerPackPreviewController.swift b/TelegramUI/StickerPackPreviewController.swift index e2e216962d..84ac2ed605 100644 --- a/TelegramUI/StickerPackPreviewController.swift +++ b/TelegramUI/StickerPackPreviewController.swift @@ -27,9 +27,9 @@ final class StickerPackPreviewController: ViewController { self.account = account self.stickerPack = stickerPack - super.init(navigationBar: NavigationBar()) + super.init(navigationBarTheme: nil) - self.navigationBar.isHidden = true + self.statusBar.statusBarStyle = .Ignore self.stickerPackContents.set(loadedStickerPack(account: account, reference: stickerPack)) } diff --git a/TelegramUI/StickerPackPreviewControllerNode.swift b/TelegramUI/StickerPackPreviewControllerNode.swift index 5562ca11aa..89e90c5433 100644 --- a/TelegramUI/StickerPackPreviewControllerNode.swift +++ b/TelegramUI/StickerPackPreviewControllerNode.swift @@ -7,7 +7,7 @@ import TelegramCore private let defaultBackgroundColor: UIColor = UIColor(white: 1.0, alpha: 1.0) private let highlightedBackgroundColor: UIColor = UIColor(white: 0.9, alpha: 1.0) -private let separatorColor: UIColor = UIColor(0xbcbbc1) +private let separatorColor: UIColor = UIColor(rgb: 0xbcbbc1) private let roundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: .white) private let highlightedRoundedBackground = generateStretchableFilledCircleImage(radius: 16.0, color: highlightedBackgroundColor) @@ -26,7 +26,7 @@ private let highlightedHalfRoundedBackground = generateImage(CGSize(width: 32.0, context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height / 2.0))) })?.stretchableImage(withLeftCapWidth: 16, topCapHeight: 1) -final class StickerPackPreviewControllerNode: ASDisplayNode, UIScrollViewDelegate { +final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private let account: Account private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -115,9 +115,7 @@ final class StickerPackPreviewControllerNode: ASDisplayNode, UIScrollViewDelegat self.installActionSeparatorNode.displaysAsynchronously = false self.installActionSeparatorNode.backgroundColor = separatorColor - super.init(viewBlock: { - return UITracingLayerView() - }, didLoad: nil) + super.init() self.interaction = StickerPackPreviewInteraction(sendSticker: { [weak self] item in if let strongSelf = self { @@ -135,7 +133,7 @@ final class StickerPackPreviewControllerNode: ASDisplayNode, UIScrollViewDelegat self.wrappingScrollNode.view.delegate = self self.addSubnode(self.wrappingScrollNode) - self.cancelButtonNode.setTitle("Cancel", with: Font.medium(20.0), with: UIColor(0x007ee5), for: .normal) + self.cancelButtonNode.setTitle("Cancel", with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) /*self.cancelButtonNode.backgroundColor = defaultBackgroundColor self.cancelButtonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -278,8 +276,10 @@ final class StickerPackPreviewControllerNode: ASDisplayNode, UIScrollViewDelegat transition.updateFrame(node: self.installActionButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight), size: CGSize(width: contentContainerFrame.size.width, height: buttonHeight))) transition.updateFrame(node: self.installActionSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentContainerFrame.size.height - buttonHeight - UIScreenPixel), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) - self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth))), transition: transition), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)))) + let gridSize = CGSize(width: contentFrame.size.width, height: max(32.0, contentFrame.size.height - titleAreaHeight)) + + self.contentGridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: insertItems, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: gridSize, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: bottomGridInset, right: 0.0), preloadSize: 80.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), lineSpacing: 0.0)), transition: transition), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + transition.updateFrame(node: self.contentGridNode, frame: CGRect(origin: CGPoint(x: floor((contentContainerFrame.size.width - contentFrame.size.width) / 2.0), y: titleAreaHeight), size: gridSize)) if animateIn { var durationOffset = 0.0 @@ -426,7 +426,7 @@ final class StickerPackPreviewControllerNode: ASDisplayNode, UIScrollViewDelegat switch stickerPack { case .none, .fetching: self.installActionSeparatorNode.alpha = 0.0 - self.installActionButtonNode.setTitle("", with: Font.medium(20.0), with: UIColor(0x007ee5), for: .normal) + self.installActionButtonNode.setTitle("", with: Font.medium(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) case let .result(info, _, installed): self.installActionSeparatorNode.alpha = 1.0 if installed { @@ -436,7 +436,7 @@ final class StickerPackPreviewControllerNode: ASDisplayNode, UIScrollViewDelegat } else { text = "Remove \(info.count) masks" } - self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: UIColor(0xff3b30), for: .normal) + self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: UIColor(rgb: 0xff3b30), for: .normal) } else { let text: String if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { @@ -444,7 +444,7 @@ final class StickerPackPreviewControllerNode: ASDisplayNode, UIScrollViewDelegat } else { text = "Add \(info.count) masks" } - self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: UIColor(0x007ee5), for: .normal) + self.installActionButtonNode.setTitle(text, with: Font.regular(20.0), with: UIColor(rgb: 0x007ee5), for: .normal) } } } @@ -472,6 +472,11 @@ final class StickerPackPreviewControllerNode: ASDisplayNode, UIScrollViewDelegat if let result = self.installActionButtonNode.hitTest(self.installActionButtonNode.convert(point, from: self), with: event) { return result } + if self.bounds.contains(point) { + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) { + return self.dimNode.view + } + } return super.hitTest(point, with: event) } diff --git a/TelegramUI/StickerPreviewController.swift b/TelegramUI/StickerPreviewController.swift index f8c71c4f00..54d65daeba 100644 --- a/TelegramUI/StickerPreviewController.swift +++ b/TelegramUI/StickerPreviewController.swift @@ -27,9 +27,7 @@ final class StickerPreviewController: ViewController { self.account = account self.item = item - super.init(navigationBar: NavigationBar()) - - self.navigationBar.isHidden = true + super.init(navigationBarTheme: nil) } required init(coder aDecoder: NSCoder) { diff --git a/TelegramUI/StorageUsageController.swift b/TelegramUI/StorageUsageController.swift index 27a1694998..24ebb12505 100644 --- a/TelegramUI/StorageUsageController.swift +++ b/TelegramUI/StorageUsageController.swift @@ -22,12 +22,12 @@ private enum StorageUsageSection: Int32 { } private enum StorageUsageEntry: ItemListNodeEntry { - case keepMedia(String, String) - case keepMediaInfo(String) + case keepMedia(PresentationTheme, String, String) + case keepMediaInfo(PresentationTheme, String) - case collecting(String) - case peersHeader(String) - case peer(Int32, Peer, String) + case collecting(PresentationTheme, String) + case peersHeader(PresentationTheme, String) + case peer(Int32, PresentationTheme, PresentationStrings, Peer, String) var section: ItemListSectionId { switch self { @@ -48,42 +48,48 @@ private enum StorageUsageEntry: ItemListNodeEntry { return 2 case .peersHeader: return 3 - case let .peer(index, _, _): + case let .peer(index, _, _, _, _): return 4 + index } } static func ==(lhs: StorageUsageEntry, rhs: StorageUsageEntry) -> Bool { switch lhs { - case let .keepMedia(text, value): - if case .keepMedia(text, value) = rhs { + case let .keepMedia(lhsTheme, lhsText, lhsValue): + if case let .keepMedia(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .keepMediaInfo(text): - if case .keepMediaInfo(text) = rhs { + case let .keepMediaInfo(lhsTheme, lhsText): + if case let .keepMediaInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .collecting(text): - if case .collecting(text) = rhs { + case let .collecting(lhsTheme, lhsText): + if case let .collecting(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .peersHeader(text): - if case .peersHeader(text) = rhs { + case let .peersHeader(lhsTheme, lhsText): + if case let .peersHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .peer(lhsIndex, lhsPeer, lhsValue): - if case let .peer(rhsIndex, rhsPeer, rhsValue) = rhs { + case let .peer(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsValue): + if case let .peer(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsValue) = rhs { if lhsIndex != rhsIndex { return false } + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if !arePeersEqual(lhsPeer, rhsPeer) { return false } @@ -103,18 +109,18 @@ private enum StorageUsageEntry: ItemListNodeEntry { func item(_ arguments: StorageUsageControllerArguments) -> ListViewItem { switch self { - case let .keepMedia(text, value): - return ItemListDisclosureItem(title: text, label: value, sectionId: self.section, style: .blocks, action: { + case let .keepMedia(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.updateKeepMedia() }) - case let .keepMediaInfo(text): - return ItemListTextItem(text: .markdown(text), sectionId: self.section) - case let .collecting(text): - return ItemListActivityTextItem(displayActivity: true, text: NSAttributedString(string: text, textColor: UIColor(0x6d6d72)), sectionId: self.section) - case let .peersHeader(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .peer(_, peer, value): - return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .none, label: .disclosure(value), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: { + case let .keepMediaInfo(theme, text): + return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + case let .collecting(theme, text): + return ItemListActivityTextItem(displayActivity: true, text: NSAttributedString(string: text, textColor: theme.list.freeTextColor), sectionId: self.section) + case let .peersHeader(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .peer(_, theme, strings, peer, value): + return ItemListPeerItem(theme: theme, strings: strings, account: arguments.account, peer: peer, presence: nil, text: .none, label: .disclosure(value), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: { arguments.openPeerMedia(peer.id) }, setPeerIdWithRevealedOptions: { previousId, id in @@ -135,11 +141,11 @@ private func stringForKeepMediaTimeout(_ timeout: Int32) -> String { } } -private func storageUsageControllerEntries(cacheSettings: CacheStorageSettings, cacheStats: CacheUsageStatsResult?) -> [StorageUsageEntry] { +private func storageUsageControllerEntries(presentationData: PresentationData, cacheSettings: CacheStorageSettings, cacheStats: CacheUsageStatsResult?) -> [StorageUsageEntry] { var entries: [StorageUsageEntry] = [] - entries.append(.keepMedia("Keep Media", stringForKeepMediaTimeout(cacheSettings.defaultCacheStorageTimeout))) - entries.append(.keepMediaInfo("Photos, videos and other files from cloud chats that you have **not accessed** during this period will be removed from this device to save disk space.\n\nAll media will stay in the Telegram cloud and can be re-downloaded if you need it again.")) + entries.append(.keepMedia(presentationData.theme, "Keep Media", stringForKeepMediaTimeout(cacheSettings.defaultCacheStorageTimeout))) + entries.append(.keepMediaInfo(presentationData.theme, "Photos, videos and other files from cloud chats that you have **not accessed** during this period will be removed from this device to save disk space.\n\nAll media will stay in the Telegram cloud and can be re-downloaded if you need it again.")) var addedHeader = false @@ -160,15 +166,15 @@ private func storageUsageControllerEntries(cacheSettings: CacheStorageSettings, if let peer = stats.peers[peerId] { if !addedHeader { addedHeader = true - entries.append(.peersHeader("CHATS")) + entries.append(.peersHeader(presentationData.theme, "CHATS")) } - entries.append(.peer(index, peer, dataSizeString(Int(size)))) + entries.append(.peer(index, presentationData.theme, presentationData.strings, peer, dataSizeString(Int(size)))) index += 1 } } } } else { - entries.append(.collecting("Calculating current cache size...")) + entries.append(.collecting(presentationData.theme, "Calculating current cache size...")) } return entries @@ -355,20 +361,18 @@ func storageUsageController(account: Account) -> ViewController { }) }) - let signal = combineLatest(cacheSettingsPromise.get(), statsPromise.get()) |> deliverOnMainQueue - |> map { cacheSettings, cacheStats -> (ItemListControllerState, (ItemListNodeState, StorageUsageEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, cacheSettingsPromise.get(), statsPromise.get()) |> deliverOnMainQueue + |> map { presentationData, cacheSettings, cacheStats -> (ItemListControllerState, (ItemListNodeState, StorageUsageEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(title: .text("Storage Usage"), leftNavigationButton: nil, rightNavigationButton: nil, animateChanges: false) - let listState = ItemListNodeState(entries: storageUsageControllerEntries(cacheSettings: cacheSettings, cacheStats: cacheStats), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Storage Usage"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: "Back"), animateChanges: false) + let listState = ItemListNodeState(entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, cacheStats: cacheStats), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionDisposables.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) - + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c in controller?.present(c, in: .window, with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) } diff --git a/TelegramUI/StringPluralization.swift b/TelegramUI/StringPluralization.swift new file mode 100644 index 0000000000..790f3991ba --- /dev/null +++ b/TelegramUI/StringPluralization.swift @@ -0,0 +1,44 @@ +import Foundation + +enum PluralizationForm { + case zero + case one + case two + case few + case many + case other + + var name: String { + switch self { + case .zero: + return "zero" + case .one: + return "one" + case .two: + return "two" + case .few: + return "few" + case .many: + return "many" + case .other: + return "other" + } + } +} + +func presentationStringsPluralizationForm(_ lc: UInt32, _ value: Int32) -> PluralizationForm { + switch numberPluralizationForm(lc, value) { + case .zero: + return .zero + case .one: + return .one + case .two: + return .two + case .few: + return .few + case .many: + return .many + case .other: + return .other + } +} diff --git a/TelegramUI/StringWithAppliedEntities.swift b/TelegramUI/StringWithAppliedEntities.swift index 479d8edb41..7af52c5167 100644 --- a/TelegramUI/StringWithAppliedEntities.swift +++ b/TelegramUI/StringWithAppliedEntities.swift @@ -1,9 +1,9 @@ import Foundation import TelegramCore -func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseFont: UIFont, boldFont: UIFont, fixedFont: UIFont) -> NSAttributedString { +func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], baseColor: UIColor, linkColor: UIColor, baseFont: UIFont, boldFont: UIFont, fixedFont: UIFont) -> NSAttributedString { var nsString: NSString? - let string = NSMutableAttributedString(string: text, attributes: [NSFontAttributeName: baseFont, NSForegroundColorAttributeName: UIColor.black]) + let string = NSMutableAttributedString(string: text, attributes: [NSFontAttributeName: baseFont, NSForegroundColorAttributeName: baseColor]) var skipEntity = false let stringLength = string.length for i in 0 ..< entities.count { @@ -22,19 +22,19 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba } switch entity.type { case .Url: - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) + string.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: range) if nsString == nil { nsString = text as NSString } string.addAttribute(TextNode.UrlAttribute, value: nsString!.substring(with: range), range: range) case .Email: - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) + string.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: range) if nsString == nil { nsString = text as NSString } string.addAttribute(TextNode.UrlAttribute, value: "mailto:\(nsString!.substring(with: range))", range: range) case let .TextUrl(url): - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) + string.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: range) if nsString == nil { nsString = text as NSString } @@ -42,14 +42,15 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba case .Bold: string.addAttribute(NSFontAttributeName, value: boldFont, range: range) case .Mention: - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) + string.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: range) if nsString == nil { nsString = text as NSString } string.addAttribute(TextNode.TelegramPeerTextMentionAttribute, value: nsString!.substring(with: range), range: range) case let .TextMention(peerId): - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) - string.addAttribute(TextNode.TelegramPeerMentionAttribute, value: peerId.toInt64() as NSNumber, range: range) + string.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: range) + let mention = nsString!.substring(with: range) + string.addAttribute(TextNode.TelegramPeerMentionAttribute, value: TelegramPeerMention(peerId: peerId, mention: mention), range: range) case .Hashtag: if nsString == nil { nsString = text as NSString @@ -63,17 +64,17 @@ func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], ba skipEntity = true let combinedRange = NSRange(location: range.location, length: nextRange.location + nextRange.length - range.location) - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: combinedRange) + string.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: combinedRange) string.addAttribute(TextNode.TelegramHashtagAttribute, value: TelegramHashtag(peerName: peerName, hashtag: hashtag), range: combinedRange) } } } if !skipEntity { - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) + string.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: range) string.addAttribute(TextNode.TelegramHashtagAttribute, value: TelegramHashtag(peerName: nil, hashtag: hashtag), range: range) } case .BotCommand: - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) + string.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: range) if nsString == nil { nsString = text as NSString } diff --git a/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift b/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift index 843f625c01..7c2292e9c1 100644 --- a/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift +++ b/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift @@ -39,15 +39,19 @@ enum TapLongTapOrDoubleTapGestureRecognizerAction { final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { private var touchLocationAndTimestamp: (CGPoint, Double)? + private var touchCount: Int = 0 private var tapCount: Int = 0 private var timer: Foundation.Timer? private(set) var lastRecognizedGestureAndLocation: (TapLongTapOrDoubleTapGesture, CGPoint)? var tapActionAtPoint: ((CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction)? + var highlight: ((CGPoint?) -> Void)? var hapticFeedback: HapticFeedback? + private var highlightPoint: CGPoint? + override init(target: Any?, action: Selector?) { super.init(target: target, action: action) @@ -66,8 +70,14 @@ final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, UIGestu self.timer = nil self.touchLocationAndTimestamp = nil self.tapCount = 0 + self.touchCount = 0 self.hapticFeedback = nil + if self.highlightPoint != nil { + self.highlightPoint = nil + self.highlight?(nil) + } + super.reset() } @@ -110,14 +120,23 @@ final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, UIGestu super.touchesBegan(touches, with: event) + self.touchCount += touches.count + if let touch = touches.first { + let touchLocation = touch.location(in: self.view) + + if self.highlightPoint != touchLocation { + self.highlightPoint = touchLocation + self.highlight?(touchLocation) + } + if let hitResult = self.view?.hitTest(touch.location(in: self.view), with: event), let _ = hitResult as? UIButton { self.state = .failed return } self.tapCount += 1 - if self.tapCount == 2 { + if self.tapCount == 2 && self.touchCount == 1 { self.timer?.invalidate() self.timer = nil self.lastRecognizedGestureAndLocation = (.doubleTap, self.location(in: self.view)) @@ -177,6 +196,13 @@ final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, UIGestu override func touchesEnded(_ touches: Set, with event: UIEvent) { super.touchesEnded(touches, with: event) + self.touchCount -= touches.count + + if self.highlightPoint != nil { + self.highlightPoint = nil + self.highlight?(nil) + } + self.timer?.invalidate() if let (gesture, location) = self.lastRecognizedGestureAndLocation, case .hold = gesture { @@ -219,6 +245,13 @@ final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, UIGestu override func touchesCancelled(_ touches: Set, with event: UIEvent) { super.touchesCancelled(touches, with: event) + self.touchCount -= touches.count + + if self.highlightPoint != nil { + self.highlightPoint = nil + self.highlight?(nil) + } + self.state = .cancelled } } diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift index 6ceb2dda37..cc18bd54e6 100644 --- a/TelegramUI/TelegramApplicationContext.swift +++ b/TelegramUI/TelegramApplicationContext.swift @@ -1,15 +1,13 @@ import Foundation import SwiftSignalKit import UIKit +import Postbox +import TelegramCore -public final class TelegramApplicationContext { +public final class TelegramApplicationBindings { public let openUrl: (String) -> Void public let getTopWindow: () -> UIWindow? public let displayNotification: (String) -> Void - - let sharedChatMediaInputNode = Atomic(value: nil) - let mediaManager = MediaManager() - public let applicationInForeground: Signal public let applicationIsActive: Signal @@ -21,3 +19,49 @@ public final class TelegramApplicationContext { self.applicationIsActive = applicationIsActive } } + +public final class TelegramApplicationContext { + public let applicationBindings: TelegramApplicationBindings + public let accountManager: AccountManager + public var callManager: PresentationCallManager? + + public let mediaManager = MediaManager() + + public let currentPresentationData: Atomic + private let _presentationData = Promise() + public var presentationData: Signal { + return self._presentationData.get() + } + + private let presentationDataDisposable = MetaDisposable() + + public var navigateToCurrentCall: (() -> Void)? + public var hasOngoingCall: Signal? + + public init(applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, currentPresentationData: PresentationData, presentationData: Signal) { + self.applicationBindings = applicationBindings + self.accountManager = accountManager + self.currentPresentationData = Atomic(value: currentPresentationData) + self._presentationData.set(.single(currentPresentationData) |> then(presentationData)) + + self.presentationDataDisposable.set(self._presentationData.get().start(next: { [weak self] next in + if let strongSelf = self { + let _ = strongSelf.currentPresentationData.swap(next) + } + })) + } + + deinit { + self.presentationDataDisposable.dispose() + } + + public func attachOverlayMediaController(_ controller: OverlayMediaController) { + self.mediaManager.overlayMediaManager.attachOverlayMediaController(controller) + } +} + +extension Account { + var telegramApplicationContext: TelegramApplicationContext { + return self.applicationContext as! TelegramApplicationContext + } +} diff --git a/TelegramUI/TelegramController.swift b/TelegramUI/TelegramController.swift index a8133dee60..16fe9bfba1 100644 --- a/TelegramUI/TelegramController.swift +++ b/TelegramUI/TelegramController.swift @@ -22,7 +22,7 @@ public class TelegramController: ViewController { init(account: Account) { self.account = account - super.init(navigationBar: NavigationBar()) + super.init(navigationBarTheme: NavigationBarTheme(rootControllerTheme: (account.telegramApplicationContext.currentPresentationData.with { $0 }).theme)) if let applicationContext = account.applicationContext as? TelegramApplicationContext { self.mediaStatusDisposable = (applicationContext.mediaManager.playlistPlayerStateAndStatus @@ -48,8 +48,8 @@ public class TelegramController: ViewController { public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - if let playlistStateAndStatus = playlistStateAndStatus { - let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: self.navigationBar.frame.maxY), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - self.navigationBar.frame.maxY - layout.insets(options: [.input]).bottom))) + if let playlistStateAndStatus = self.playlistStateAndStatus { + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: super.navigationHeight), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - super.navigationHeight - layout.insets(options: [.input]).bottom))) if let mediaAccessoryPanel = self.mediaAccessoryPanel { transition.updateFrame(node: mediaAccessoryPanel, frame: panelFrame) mediaAccessoryPanel.updateLayout(size: panelFrame.size, transition: transition) @@ -93,7 +93,11 @@ public class TelegramController: ViewController { } } mediaAccessoryPanel.frame = panelFrame - self.displayNode.insertSubnode(mediaAccessoryPanel, belowSubnode: self.navigationBar) + if let navigationBar = self.navigationBar { + self.displayNode.insertSubnode(mediaAccessoryPanel, belowSubnode: navigationBar) + } else { + self.displayNode.addSubnode(mediaAccessoryPanel) + } self.mediaAccessoryPanel = mediaAccessoryPanel mediaAccessoryPanel.updateLayout(size: panelFrame.size, transition: .immediate) mediaAccessoryPanel.containerNode.headerNode.stateAndStatus = playlistStateAndStatus diff --git a/TelegramUI/TelegramRootController.swift b/TelegramUI/TelegramRootController.swift new file mode 100644 index 0000000000..6eeee03606 --- /dev/null +++ b/TelegramUI/TelegramRootController.swift @@ -0,0 +1,53 @@ +import Foundation +import Display +import Postbox +import TelegramCore +import SwiftSignalKit + +public final class TelegramRootController: NavigationController { + private let account: Account + + public var rootTabController: TabBarController? + public var chatListController: ChatListController? + + private var presentationDataDisposable: Disposable? + private var presentationData: PresentationData + + public init(account: Account) { + self.account = account + + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init() + + 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.rootTabController?.updateTheme(navigationBarTheme: NavigationBarTheme(rootControllerTheme: presentationData.theme), theme: TabBarControllerTheme(rootControllerTheme: presentationData.theme)) + strongSelf.rootTabController?.statusBar.statusBarStyle = presentationData.theme.rootController.statusBar.style.style + } + } + }) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + public func addRootControllers() { + let tabBarController = TabBarController(navigationBarTheme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme), theme: TabBarControllerTheme(rootControllerTheme: self.presentationData.theme)) + let chatListController = ChatListController(account: self.account) + let callListController = CallListController(account: self.account) + tabBarController.controllers = [ContactsController(account: self.account), callListController, chatListController, settingsController(account: self.account, accountManager: self.account.telegramApplicationContext.accountManager)] + self.chatListController = chatListController + self.rootTabController = tabBarController + self.pushViewController(tabBarController, animated: false) + } +} diff --git a/TelegramUI/TelegramUI.h b/TelegramUI/TelegramUI.h index aef0c8dabd..a79c43e4ff 100644 --- a/TelegramUI/TelegramUI.h +++ b/TelegramUI/TelegramUI.h @@ -2,8 +2,8 @@ // TelegramUI.h // TelegramUI // -// Created by Peter on 8/10/16. -// Copyright © 2016 Telegram. All rights reserved. +// Created by Peter on 5/3/17. +// Copyright © 2017 Telegram. All rights reserved. // #import @@ -16,4 +16,6 @@ FOUNDATION_EXPORT const unsigned char TelegramUIVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import - +#import +#import +#import diff --git a/TelegramUI/TextNode.swift b/TelegramUI/TextNode.swift index cfcee8cf24..5793c9c0a9 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -1,6 +1,7 @@ import Foundation import AsyncDisplayKit import Display +import Postbox private let defaultFont = UIFont.systemFont(ofSize: 15.0) @@ -40,9 +41,10 @@ final class TextNodeLayout: NSObject { fileprivate let cutout: TextNodeCutout? fileprivate let insets: UIEdgeInsets let size: CGSize + fileprivate let firstLineOffset: CGFloat fileprivate let lines: [TextNodeLine] - fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, lines: [TextNodeLine], backgroundColor: UIColor?) { + fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, firstLineOffset: CGFloat, lines: [TextNodeLine], backgroundColor: UIColor?) { self.attributedString = attributedString self.maximumNumberOfLines = maximumNumberOfLines self.truncationType = truncationType @@ -51,6 +53,7 @@ final class TextNodeLayout: NSObject { self.cutout = cutout self.insets = insets self.size = size + self.firstLineOffset = firstLineOffset self.lines = lines self.backgroundColor = backgroundColor } @@ -67,30 +70,78 @@ final class TextNodeLayout: NSObject { } } - func attributesAtPoint(_ point: CGPoint) -> [String: Any] { + func attributesAtPoint(_ point: CGPoint) -> (Int, [String: Any])? { if let attributedString = self.attributedString { + let transformedPoint = CGPoint(x: point.x - self.insets.left, y: point.y - self.insets.top) for line in self.lines { - let lineFrame = line.frame.offsetBy(dx: 0.0, dy: -line.frame.size.height) - if lineFrame.contains(point) { - let index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: point.x - lineFrame.minX, y: point.y - lineFrame.minY)) + let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + if lineFrame.contains(transformedPoint) { + var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) + if index == attributedString.length { + index -= 1 + } else if index != 0 { + var glyphStart: CGFloat = 0.0 + CTLineGetOffsetForStringIndex(line.line, index, &glyphStart) + if transformedPoint.x < glyphStart { + index -= 1 + } + } if index >= 0 && index < attributedString.length { - return attributedString.attributes(at: index, effectiveRange: nil) + return (index, attributedString.attributes(at: index, effectiveRange: nil)) } break } } for line in self.lines { - let lineFrame = line.frame.offsetBy(dx: 0.0, dy: -line.frame.size.height) - if lineFrame.offsetBy(dx: 0.0, dy: -lineFrame.size.height).insetBy(dx: -3.0, dy: -3.0).contains(point) { - let index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: point.x - lineFrame.minX, y: point.y - lineFrame.minY)) + let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + if lineFrame.offsetBy(dx: 0.0, dy: -lineFrame.size.height).insetBy(dx: -3.0, dy: -3.0).contains(transformedPoint) { + var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) + if index == attributedString.length { + index -= 1 + } else if index != 0 { + var glyphStart: CGFloat = 0.0 + CTLineGetOffsetForStringIndex(line.line, index, &glyphStart) + if transformedPoint.x < glyphStart { + index -= 1 + } + } if index >= 0 && index < attributedString.length { - return attributedString.attributes(at: index, effectiveRange: nil) + return (index, attributedString.attributes(at: index, effectiveRange: nil)) } break } } } - return [:] + return nil + } + + func attributeRects(name: String, at index: Int) -> [CGRect]? { + if let attributedString = self.attributedString { + var range = NSRange() + let _ = attributedString.attribute(name, at: index, effectiveRange: &range) + if range.length != 0 { + var rects: [CGRect] = [] + for line in self.lines { + let lineRange = NSIntersectionRange(range, line.range) + if lineRange.length != 0 { + var leftOffset: CGFloat = 0.0 + if lineRange.location != line.range.location { + leftOffset = floor(CTLineGetOffsetForStringIndex(line.line, lineRange.location, nil)) + } + var rightOffset: CGFloat = line.frame.width + if lineRange.location + lineRange.length != line.range.length { + rightOffset = ceil(CTLineGetOffsetForStringIndex(line.line, lineRange.location + lineRange.length, nil)) + } + let lineFrame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: line.frame.origin.y - line.frame.size.height + self.firstLineOffset), size: line.frame.size) + rects.append(CGRect(origin: CGPoint(x: lineFrame.minX + leftOffset + self.insets.left, y: lineFrame.minY + self.insets.top), size: CGSize(width: rightOffset - leftOffset, height: lineFrame.size.height))) + } + } + if !rects.isEmpty { + return rects + } + } + } + return nil } } @@ -104,6 +155,16 @@ final class TelegramHashtag { } } +final class TelegramPeerMention { + let peerId: PeerId + let mention: String + + init(peerId: PeerId, mention: String) { + self.peerId = peerId + self.mention = mention + } +} + final class TextNode: ASDisplayNode { static let UrlAttribute = "UrlAttributeT" static let TelegramPeerMentionAttribute = "TelegramPeerMention" @@ -121,11 +182,19 @@ final class TextNode: ASDisplayNode { self.clipsToBounds = false } - func attributesAtPoint(_ point: CGPoint) -> [String: Any] { + func attributesAtPoint(_ point: CGPoint) -> (Int, [String: Any])? { if let cachedLayout = self.cachedLayout { return cachedLayout.attributesAtPoint(point) } else { - return [:] + return nil + } + } + + func attributeRects(name: String, at index: Int) -> [CGRect]? { + if let cachedLayout = self.cachedLayout { + return cachedLayout.attributeRects(name: name, at: index) + } else { + return nil } } @@ -154,7 +223,7 @@ final class TextNode: ASDisplayNode { var maybeTypesetter: CTTypesetter? maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) if maybeTypesetter == nil { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(), lines: [], backgroundColor: backgroundColor) + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor) } let typesetter = maybeTypesetter! @@ -177,6 +246,8 @@ final class TextNode: ASDisplayNode { cutoutEnabled = true } + let firstLineOffset = floorToScreenPixels(fontLineSpacing * 2.0) + var first = true while true { var lineConstrainedWidth = constrainedSize.width @@ -257,9 +328,9 @@ final class TextNode: ASDisplayNode { } } - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), lines: lines, backgroundColor: backgroundColor) + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), firstLineOffset: firstLineOffset, lines: lines, backgroundColor: backgroundColor) } else { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(), lines: [], backgroundColor: backgroundColor) + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor) } } diff --git a/TelegramUI/ThemeGalleryController.swift b/TelegramUI/ThemeGalleryController.swift new file mode 100644 index 0000000000..39ad1d12ce --- /dev/null +++ b/TelegramUI/ThemeGalleryController.swift @@ -0,0 +1,308 @@ +import Foundation +import Display +import QuickLook +import Postbox +import SwiftSignalKit +import AsyncDisplayKit +import TelegramCore + +enum ThemeGalleryEntry: Equatable { + case wallpaper(TelegramWallpaper) + + static func ==(lhs: ThemeGalleryEntry, rhs: ThemeGalleryEntry) -> Bool { + switch lhs { + case let .wallpaper(wallpaper): + if case .wallpaper(wallpaper) = rhs { + return true + } else { + return false + } + } + } +} + +final class ThemePreviewControllerPresentationArguments { + let transitionArguments: (ThemeGalleryEntry) -> GalleryTransitionArguments? + + init(transitionArguments: @escaping (ThemeGalleryEntry) -> GalleryTransitionArguments?) { + self.transitionArguments = transitionArguments + } +} + +class ThemeGalleryController: ViewController { + private var galleryNode: GalleryControllerNode { + return self.displayNode as! GalleryControllerNode + } + + private let account: Account + + private let _ready = Promise() + override var ready: Promise { + return self._ready + } + private var didSetReady = false + + private let disposable = MetaDisposable() + + private var entries: [ThemeGalleryEntry] = [] + private var centralEntryIndex: Int? + + private let centralItemTitle = Promise() + private let centralItemTitleView = Promise() + private let centralItemNavigationStyle = Promise() + private let centralItemFooterContentNode = Promise() + private let centralItemAttributesDisposable = DisposableSet(); + + private var validLayout: (ContainerViewLayout, CGFloat)? + + private var toolbarNode: ThemeGalleryToolbarNode? + + private let _hiddenMedia = Promise(nil) + var hiddenMedia: Signal { + return self._hiddenMedia.get() + } + + init(account: Account, wallpapers: [TelegramWallpaper], at centralWallpaper: TelegramWallpaper) { + self.account = account + + super.init(navigationBarTheme: nil) + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.donePressed)) + + self.statusBar.statusBarStyle = .Hide + + let initialEntries: [ThemeGalleryEntry] = wallpapers.map { ThemeGalleryEntry.wallpaper($0) } + + let entriesSignal: Signal<[ThemeGalleryEntry], NoError> = .single(initialEntries) + + self.disposable.set((entriesSignal |> deliverOnMainQueue).start(next: { [weak self] entries in + if let strongSelf = self { + strongSelf.entries = entries + strongSelf.centralEntryIndex = wallpapers.index(of: centralWallpaper)! + if strongSelf.isViewLoaded { + strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({ ThemeGalleryItem(account: account, entry: $0) }), centralItemIndex: strongSelf.centralEntryIndex, keepFirst: true) + + let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in + strongSelf?.didSetReady = true + } + strongSelf._ready.set(ready |> map { true }) + } + } + })) + + self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in + self?.navigationItem.title = title + })) + + self.centralItemAttributesDisposable.add(self.centralItemTitleView.get().start(next: { [weak self] titleView in + self?.navigationItem.titleView = titleView + })) + + self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in + self?.galleryNode.updatePresentationState({ + $0.withUpdatedFooterContentNode(footerContentNode) + }, transition: .immediate) + })) + + /*self.centralItemAttributesDisposable.add(self.centralItemNavigationStyle.get().start(next: { [weak self] style in + if let strongSelf = self { + switch style { + case .dark: + strongSelf.statusBar.statusBarStyle = .White + strongSelf.navigationBar.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + strongSelf.navigationBar.stripeColor = UIColor.clear + strongSelf.navigationBar.foregroundColor = UIColor.white + strongSelf.navigationBar.accentColor = UIColor.white + strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor.black + strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = true + case .light: + strongSelf.statusBar.statusBarStyle = .Black + strongSelf.navigationBar.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0) + strongSelf.navigationBar.foregroundColor = UIColor.black + strongSelf.navigationBar.accentColor = UIColor(rgb: 0x007ee5) + strongSelf.navigationBar.stripeColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) + strongSelf.galleryNode.backgroundNode.backgroundColor = UIColor(rgb: 0xbdbdc2) + strongSelf.galleryNode.isBackgroundExtendedOverNavigationBar = false + } + } + }))*/ + + self.statusBar.statusBarStyle = .Hide + /*strongSelf.navigationBar.stripeColor = UIColor.clear + strongSelf.navigationBar.foregroundColor = UIColor.white + strongSelf.navigationBar.accentColor = UIColor.white*/ + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable.dispose() + self.centralItemAttributesDisposable.dispose() + } + + @objc func donePressed() { + self.dismiss(forceAway: false) + } + + private func dismiss(forceAway: Bool) { + var animatedOutNode = true + var animatedOutInterface = false + + let completion = { [weak self] in + if animatedOutNode && animatedOutInterface { + self?._hiddenMedia.set(.single(nil)) + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + } + + if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? ThemePreviewControllerPresentationArguments { + if !self.entries.isEmpty { + if centralItemNode.index == 0, let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway { + animatedOutNode = false + centralItemNode.animateOut(to: transitionArguments.transitionNode, completion: { + animatedOutNode = true + completion() + }) + } + } + } + + self.galleryNode.animateOut(animateContent: animatedOutNode, completion: { + animatedOutInterface = true + completion() + }) + } + + override func loadDisplayNode() { + let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in + if let strongSelf = self { + strongSelf.present(controller, in: .window, with: arguments) + } + }, dismissController: { [weak self] in + self?.dismiss(forceAway: true) + }, replaceRootController: { [weak self] controller, ready in + if let strongSelf = self { + //strongSelf.replaceRootController(controller, ready) + } + }) + self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction, pageGap: 0.0) + self.displayNodeDidLoad() + + //self.galleryNode.statusBar = self.statusBar + self.galleryNode.navigationBar = self.navigationBar + + self.galleryNode.transitionNodeForCentralItem = { [weak self] in + if let strongSelf = self { + if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? ThemePreviewControllerPresentationArguments { + if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) { + return transitionArguments.transitionNode + } + } + } + return nil + } + self.galleryNode.dismiss = { [weak self] in + self?._hiddenMedia.set(.single(nil)) + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + + self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in + if let strongSelf = self { + var hiddenItem: ThemeGalleryEntry? + if let index = index { + hiddenItem = strongSelf.entries[index] + + if let node = strongSelf.galleryNode.pager.centralItemNode() { + strongSelf.centralItemTitle.set(node.title()) + strongSelf.centralItemTitleView.set(node.titleView()) + strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) + strongSelf.centralItemFooterContentNode.set(node.footerContent()) + } + } + if strongSelf.didSetReady { + strongSelf._hiddenMedia.set(.single(hiddenItem)) + } + } + } + + self.galleryNode.backgroundNode.backgroundColor = nil + self.galleryNode.backgroundNode.isOpaque = false + self.galleryNode.isBackgroundExtendedOverNavigationBar = true + + let toolbarNode = ThemeGalleryToolbarNode() + self.toolbarNode = toolbarNode + self.galleryNode.addSubnode(toolbarNode) + self.galleryNode.toolbarNode = toolbarNode + toolbarNode.cancel = { [weak self] in + self?.dismiss(forceAway: true) + } + toolbarNode.done = { [weak self] in + if let strongSelf = self { + if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode() { + if !strongSelf.entries.isEmpty { + let wallpaper: TelegramWallpaper + switch strongSelf.entries[centralItemNode.index] { + case let .wallpaper(value): + wallpaper = value + } + let _ = (updatePresentationThemeSettingsInteractively(postbox: strongSelf.account.postbox, { current in + if case .color(0x121212) = wallpaper { + return PresentationThemeSettings(chatWallpaper: wallpaper, theme: .builtin(.dark)) + } + + return PresentationThemeSettings(chatWallpaper: wallpaper, theme: .builtin(.light)) + }) |> deliverOnMainQueue).start(completed: { + self?.dismiss(forceAway: true) + }) + } + } + } + } + + let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in + self?.didSetReady = true + } + self._ready.set(ready |> map { true }) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + var nodeAnimatesItself = false + + if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? ThemePreviewControllerPresentationArguments { + self.centralItemTitle.set(centralItemNode.title()) + self.centralItemTitleView.set(centralItemNode.titleView()) + self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) + self.centralItemFooterContentNode.set(centralItemNode.footerContent()) + + if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]) { + nodeAnimatesItself = true + centralItemNode.animateIn(from: transitionArguments.transitionNode) + + self._hiddenMedia.set(.single(self.entries[centralItemNode.index])) + } + } + + self.galleryNode.animateIn(animateContent: !nodeAnimatesItself) + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + + transition.updateFrame(node: self.toolbarNode!, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - 49.0), size: CGSize(width: layout.size.width, height: 49.0))) + self.toolbarNode!.updateLayout(size: CGSize(width: layout.size.width, height: 49.0), transition: transition) + + let replace = self.validLayout == nil + self.validLayout = (layout, 0.0) + + if replace { + self.galleryNode.pager.replaceItems(self.entries.map({ ThemeGalleryItem(account: self.account, entry: $0) }), centralItemIndex: self.centralEntryIndex) + } + } +} diff --git a/TelegramUI/ThemeGalleryItem.swift b/TelegramUI/ThemeGalleryItem.swift new file mode 100644 index 0000000000..fcf5be9f2b --- /dev/null +++ b/TelegramUI/ThemeGalleryItem.swift @@ -0,0 +1,218 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore + +class ThemeGalleryItem: GalleryItem { + let account: Account + let entry: ThemeGalleryEntry + + init(account: Account, entry: ThemeGalleryEntry) { + self.account = account + self.entry = entry + } + + func node() -> GalleryItemNode { + let node = ThemeGalleryItemNode(account: self.account) + + node.setEntry(self.entry) + + return node + } + + func updateNode(node: GalleryItemNode) { + if let node = node as? ThemeGalleryItemNode { + node.setEntry(self.entry) + } + } +} + +final class ThemeGalleryItemNode: ZoomableContentGalleryItemNode { + private let account: Account + + private var entry: ThemeGalleryEntry? + + private let imageNode: TransformImageNode + fileprivate let _ready = Promise() + fileprivate let _title = Promise() + //private let footerContentNode: ChatItemGalleryFooterContentNode + + private var fetchDisposable = MetaDisposable() + + init(account: Account) { + self.account = account + + self.imageNode = TransformImageNode() + //self.footerContentNode = ChatItemGalleryFooterContentNode(account: account) + + super.init() + + self.backgroundColor = .white + + self.imageNode.imageUpdated = { [weak self] in + self?._ready.set(.single(Void())) + } + + self.imageNode.view.contentMode = .scaleAspectFill + self.imageNode.clipsToBounds = true + } + + deinit { + self.fetchDisposable.dispose() + } + + override func ready() -> Signal { + return self._ready.get() + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + + fileprivate func setEntry(_ entry: ThemeGalleryEntry) { + if self.entry != entry { + self.entry = entry + + switch entry { + case let .wallpaper(wallpaper): + switch wallpaper { + case .builtin: + let displaySize = CGSize(width: 640.0, height: 1136.0) + self.imageNode.alphaTransitionOnFirstUpdate = false + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.setSignal(account: account, signal: settingsBuiltinWallpaperImage(account: self.account), dispatchOnDisplayLink: false) + self.zoomableContent = (displaySize, self.imageNode) + case let .color(color): + self.imageNode.isHidden = true + self.backgroundColor = UIColor(rgb: UInt32(bitPattern: color)) + case let .image(representations): + if let largestSize = largestImageRepresentation(representations) { + let displaySize = largestSize.dimensions.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor + self.imageNode.alphaTransitionOnFirstUpdate = false + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))() + self.imageNode.setSignal(account: account, signal: chatAvatarGalleryPhoto(account: account, representations: representations), dispatchOnDisplayLink: false) + self.zoomableContent = (largestSize.dimensions, self.imageNode) + self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(largestSize.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .generic)).start()) + } else { + self._ready.set(.single(Void())) + } + } + } + } + } + + override func animateIn(from node: ASDisplayNode) { + var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) + let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + let transformedCopyViewFinalFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) + + let copyView = node.view.snapshotContentTree()! + + self.view.insertSubview(copyView, belowSubview: self.scrollView) + copyView.frame = transformedSelfFrame + + copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in + copyView?.removeFromSuperview() + }) + + copyView.layer.animatePosition(from: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedCopyViewFinalFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewFinalFrame.size.height / transformedSelfFrame.size.height) + copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false) + + self.imageNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.imageNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.07) + + transformedFrame.origin = CGPoint() + //self.imageNode.layer.animateBounds(from: transformedFrame, to: self.imageNode.layer.bounds, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + let transform = CATransform3DScale(self.imageNode.layer.transform, transformedFrame.size.width / self.imageNode.layer.bounds.size.width, transformedFrame.size.height / self.imageNode.layer.bounds.size.height, 1.0) + self.imageNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: self.imageNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + + self.imageNode.clipsToBounds = true + self.imageNode.layer.animate(from: (self.imageNode.frame.width / 2.0) as NSNumber, to: 0.0 as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionDefault, duration: 0.18, removeOnCompletion: false, completion: { [weak self] value in + if value { + self?.imageNode.clipsToBounds = false + } + }) + } + + override func animateOut(to node: ASDisplayNode, completion: @escaping () -> Void) { + var transformedFrame = node.view.convert(node.view.bounds, to: self.imageNode.view) + let transformedSuperFrame = node.view.convert(node.view.bounds, to: self.imageNode.view.superview) + let transformedSelfFrame = node.view.convert(node.view.bounds, to: self.view) + let transformedCopyViewInitialFrame = self.imageNode.view.convert(self.imageNode.view.bounds, to: self.view) + + var positionCompleted = false + var boundsCompleted = false + var copyCompleted = false + + let copyView = node.view.snapshotContentTree()! + + self.view.insertSubview(copyView, belowSubview: self.scrollView) + copyView.frame = transformedSelfFrame + + let intermediateCompletion = { [weak copyView] in + if positionCompleted && boundsCompleted && copyCompleted { + copyView?.removeFromSuperview() + completion() + } + } + + let durationFactor = 1.0 + + copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1 * durationFactor, removeOnCompletion: false) + + copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25 * durationFactor, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height) + copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25 * durationFactor, removeOnCompletion: false, completion: { _ in + copyCompleted = true + intermediateCompletion() + }) + + self.imageNode.layer.animatePosition(from: self.imageNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25 * durationFactor, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + positionCompleted = true + intermediateCompletion() + }) + + self.imageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25 * durationFactor, removeOnCompletion: false) + + transformedFrame.origin = CGPoint() + + /*self.imageNode.layer.animateBounds(from: self.imageNode.layer.bounds, to: transformedFrame, duration: 0.25 * durationFactor, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + boundsCompleted = true + intermediateCompletion() + })*/ + + let transform = CATransform3DScale(self.imageNode.layer.transform, transformedFrame.size.width / self.imageNode.layer.bounds.size.width, transformedFrame.size.height / self.imageNode.layer.bounds.size.height, 1.0) + self.imageNode.layer.animate(from: NSValue(caTransform3D: self.imageNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25 * durationFactor, removeOnCompletion: false, completion: { _ in + boundsCompleted = true + intermediateCompletion() + }) + + self.imageNode.clipsToBounds = true + self.imageNode.layer.animate(from: 0.0 as NSNumber, to: (self.imageNode.frame.width / 2.0) as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionDefault, duration: 0.18 * durationFactor, removeOnCompletion: false) + } + + override func visibilityUpdated(isVisible: Bool) { + super.visibilityUpdated(isVisible: isVisible) + + /*if let (account, media) = self.accountAndEntry, let file = media as? TelegramMediaFile { + if isVisible { + self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource).start()) + } else { + self.fetchDisposable.set(nil) + } + }*/ + } + + override func title() -> Signal { + return self._title.get() + } + + /*override func footerContent() -> Signal { + return .single(self.footerContentNode) + }*/ +} diff --git a/TelegramUI/ThemeGalleryToolbarNode.swift b/TelegramUI/ThemeGalleryToolbarNode.swift new file mode 100644 index 0000000000..b7d7dcf589 --- /dev/null +++ b/TelegramUI/ThemeGalleryToolbarNode.swift @@ -0,0 +1,71 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class ThemeGalleryToolbarNode: ASDisplayNode { + private let cancelButton = HighlightableButtonNode() + private let doneButton = HighlightableButtonNode() + private let separatorNode = ASDisplayNode() + private let topSeparatorNode = ASDisplayNode() + + var cancel: (() -> Void)? + var done: (() -> Void)? + + override init() { + super.init() + + self.addSubnode(self.cancelButton) + self.addSubnode(self.doneButton) + self.addSubnode(self.separatorNode) + self.addSubnode(self.topSeparatorNode) + + self.backgroundColor = UIColor(rgb: 0xeaebeb) + self.separatorNode.backgroundColor = .black + self.topSeparatorNode.backgroundColor = .black + + self.cancelButton.setTitle("Cancel", with: Font.regular(17.0), with: .black, for: []) + self.doneButton.setTitle("Set", with: Font.regular(17.0), with: .black, for: []) + + self.cancelButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.cancelButton.backgroundColor = UIColor(rgb: 0xd4d4d4) + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.cancelButton.backgroundColor = .clear + }) + } + } + } + + self.doneButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.doneButton.backgroundColor = UIColor(rgb: 0xd4d4d4) + } else { + UIView.animate(withDuration: 0.3, animations: { + strongSelf.doneButton.backgroundColor = .clear + }) + } + } + } + + self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) + self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.cancelButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: floor(size.width / 2.0), height: size.height)) + self.doneButton.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: size.width - floor(size.width / 2.0), height: size.height)) + self.separatorNode.frame = CGRect(origin: CGPoint(x: floor(size.width / 2.0), y: 0.0), size: CGSize(width: UIScreenPixel, height: size.height)) + self.topSeparatorNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: UIScreenPixel)) + } + + @objc func cancelPressed() { + self.cancel?() + } + + @objc func donePressed() { + self.done?() + } +} diff --git a/TelegramUI/ThemeGridController.swift b/TelegramUI/ThemeGridController.swift new file mode 100644 index 0000000000..0810a0cf0b --- /dev/null +++ b/TelegramUI/ThemeGridController.swift @@ -0,0 +1,80 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +final class ThemeGridController: TelegramController { + private var controllerNode: ThemeGridControllerNode { + return self.displayNode as! ThemeGridControllerNode + } + + private let _ready = Promise() + override var ready: Promise { + return self._ready + } + + private let account: Account + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + override init(account: Account) { + self.account = account + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + super.init(account: account) + + self.title = self.presentationData.strings.Wallpaper_Title + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + + 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() + } + } + }) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.presentationDataDisposable?.dispose() + } + + private func updateThemeAndStrings() { + self.title = self.presentationData.strings.Wallpaper_Title + + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationBar?.updateTheme(NavigationBarTheme(rootControllerTheme: self.presentationData.theme)) + + if self.isNodeLoaded { + self.controllerNode.updatePresentationData(self.presentationData) + } + } + + override func loadDisplayNode() { + self.displayNode = ThemeGridControllerNode(account: self.account, presentationData: self.presentationData, present: { [weak self] controller, arguments in + self?.present(controller, in: .window, with: arguments) + }) + self._ready.set(self.controllerNode.ready.get()) + + self.displayNodeDidLoad() + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) + } +} diff --git a/TelegramUI/ThemeGridControllerItem.swift b/TelegramUI/ThemeGridControllerItem.swift new file mode 100644 index 0000000000..18927d3012 --- /dev/null +++ b/TelegramUI/ThemeGridControllerItem.swift @@ -0,0 +1,84 @@ +import Foundation +import Display +import TelegramCore +import SwiftSignalKit +import AsyncDisplayKit +import Postbox + +final class ThemeGridControllerItem: GridItem { + let account: Account + let wallpaper: TelegramWallpaper + let interaction: ThemeGridControllerInteraction + + let section: GridSection? = nil + + init(account: Account, wallpaper: TelegramWallpaper, interaction: ThemeGridControllerInteraction) { + self.account = account + self.wallpaper = wallpaper + self.interaction = interaction + } + + func node(layout: GridNodeLayout) -> GridItemNode { + let node = ThemeGridControllerItemNode() + node.setup(account: self.account, wallpaper: self.wallpaper, interaction: self.interaction) + return node + } + + func update(node: GridItemNode) { + guard let node = node as? ThemeGridControllerItemNode else { + assertionFailure() + return + } + node.setup(account: self.account, wallpaper: self.wallpaper, interaction: self.interaction) + } +} + +private let avatarFont = Font.medium(18.0) +private let textFont = Font.regular(11.0) + +final class ThemeGridControllerItemNode: GridItemNode { + private let wallpaperNode: SettingsThemeWallpaperNode + + private var currentState: (Account, TelegramWallpaper)? + private var interaction: ThemeGridControllerInteraction? + + override init() { + self.wallpaperNode = SettingsThemeWallpaperNode() + + super.init() + + self.addSubnode(self.wallpaperNode) + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func setup(account: Account, wallpaper: TelegramWallpaper, interaction: ThemeGridControllerInteraction) { + self.interaction = interaction + + if self.currentState == nil || self.currentState!.0 !== account || wallpaper != self.currentState!.1 { + self.currentState = (account, wallpaper) + self.setNeedsLayout() + } + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if let (_, wallpaper) = self.currentState { + self.interaction?.openWallpaper(wallpaper) + } + } + } + + override func layout() { + super.layout() + + let bounds = self.bounds + if let (account, wallpaper) = self.currentState { + self.wallpaperNode.setWallpaper(account: account, wallpaper: wallpaper, size: bounds.size) + } + } +} diff --git a/TelegramUI/ThemeGridControllerNode.swift b/TelegramUI/ThemeGridControllerNode.swift new file mode 100644 index 0000000000..18b937ea04 --- /dev/null +++ b/TelegramUI/ThemeGridControllerNode.swift @@ -0,0 +1,177 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +final class ThemeGridControllerInteraction { + let openWallpaper: (TelegramWallpaper) -> Void + + init(openWallpaper: @escaping (TelegramWallpaper) -> Void) { + self.openWallpaper = openWallpaper + } +} + +private struct ThemeGridControllerEntry: Comparable, Identifiable { + let index: Int + let wallpaper: TelegramWallpaper + + static func ==(lhs: ThemeGridControllerEntry, rhs: ThemeGridControllerEntry) -> Bool { + return lhs.index == rhs.index && lhs.wallpaper == rhs.wallpaper + } + + static func <(lhs: ThemeGridControllerEntry, rhs: ThemeGridControllerEntry) -> Bool { + return lhs.index < rhs.index + } + + var stableId: Int { + return self.index + } + + func item(account: Account, interaction: ThemeGridControllerInteraction) -> ThemeGridControllerItem { + return ThemeGridControllerItem(account: account, wallpaper: self.wallpaper, interaction: interaction) + } +} + +private struct ThemeGridEntryTransition { + let deletions: [Int] + let insertions: [GridNodeInsertItem] + let updates: [GridNodeUpdateItem] + let updateFirstIndexInSectionOffset: Int? + let stationaryItems: GridNodeStationaryItems + let scrollToItem: GridNodeScrollToItem? +} + +private func preparedThemeGridEntryTransition(account: Account, from fromEntries: [ThemeGridControllerEntry], to toEntries: [ThemeGridControllerEntry], interaction: ThemeGridControllerInteraction) -> ThemeGridEntryTransition { + let stationaryItems: GridNodeStationaryItems = .none + let scrollToItem: GridNodeScrollToItem? = nil + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices + let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction), previousIndex: $0.2) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, item: $0.1.item(account: account, interaction: interaction)) } + + return ThemeGridEntryTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: nil, stationaryItems: stationaryItems, scrollToItem: scrollToItem) +} + +final class ThemeGridControllerNode: ASDisplayNode { + private let account: Account + private var presentationData: PresentationData + + private let present: (ViewController, Any?) -> Void + + let ready = ValuePromise() + + private let gridNode: GridNode + private var queuedTransitions: [ThemeGridEntryTransition] = [] + private var validLayout: (ContainerViewLayout, CGFloat)? + + private var disposable: Disposable? + + init(account: Account, presentationData: PresentationData, present: @escaping (ViewController, Any?) -> Void) { + self.account = account + self.presentationData = presentationData + self.present = present + + self.gridNode = GridNode() + self.gridNode.showVerticalScrollIndicator = true + + super.init(viewBlock: { + return UITracingLayerView() + }, didLoad: nil) + + self.backgroundColor = presentationData.theme.list.itemBackgroundColor + + self.addSubnode(self.gridNode) + + let previousEntries = Atomic<[ThemeGridControllerEntry]?>(value: nil) + + let interaction = ThemeGridControllerInteraction(openWallpaper: { [weak self] wallpaper in + if let strongSelf = self { + let entries = previousEntries.with { $0 } + if let entries = entries, !entries.isEmpty { + let wallpapers = entries.map { $0.wallpaper } + let controller = ThemeGalleryController(account: account, wallpapers: wallpapers, at: wallpaper) + strongSelf.present(controller, ThemePreviewControllerPresentationArguments(transitionArguments: { entry -> GalleryTransitionArguments? in + return nil + })) + } + } + }) + + let transition = telegramWallpapers(account: account) + |> map { wallpapers -> (ThemeGridEntryTransition, Bool) in + var entries: [ThemeGridControllerEntry] = [] + var index = 0 + for item in wallpapers { + entries.append(ThemeGridControllerEntry(index: index, wallpaper: item)) + index += 1 + } + let previous = previousEntries.swap(entries) + return (preparedThemeGridEntryTransition(account: account, from: previous ?? [], to: entries, interaction: interaction), previous == nil) + } + self.disposable = (transition |> deliverOnMainQueue).start(next: { [weak self] (transition, _) in + if let strongSelf = self { + strongSelf.enqueueTransition(transition) + } + }) + } + + deinit { + self.disposable?.dispose() + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + self.backgroundColor = presentationData.theme.list.itemBackgroundColor + } + + private func enqueueTransition(_ transition: ThemeGridEntryTransition) { + self.queuedTransitions.append(transition) + if self.validLayout != nil { + self.dequeueTransitions() + } + } + + private func dequeueTransitions() { + while !self.queuedTransitions.isEmpty { + let transition = self.queuedTransitions.removeFirst() + self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.ready.set(true) + } + }) + } + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + var insets = layout.insets(options: [.input]) + insets.top += navigationBarHeight + let scrollIndicatorInsets = insets + + let referenceImageSize = CGSize(width: 108.0, height: 163.0) + + let minSpacing: CGFloat = 10.0 + + let imageCount = Int((layout.size.width - minSpacing * 2.0) / (referenceImageSize.width + minSpacing)) + + let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((layout.size.width - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) + + let spacing = floor((layout.size.width - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount + 1)) + + insets.top += spacing + + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, preloadSize: 300.0, type: .fixed(itemSize: imageSize, lineSpacing: spacing)), transition: .immediate), stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + + self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + + let dequeue = self.validLayout == nil + self.validLayout = (layout, navigationBarHeight) + if dequeue { + self.dequeueTransitions() + } + } +} diff --git a/TelegramUI/TwoStepVerificationPasswordEntryController.swift b/TelegramUI/TwoStepVerificationPasswordEntryController.swift index 7da201586e..ae3472c4c0 100644 --- a/TelegramUI/TwoStepVerificationPasswordEntryController.swift +++ b/TelegramUI/TwoStepVerificationPasswordEntryController.swift @@ -38,14 +38,14 @@ private enum TwoStepVerificationPasswordEntryTag: ItemListItemTag { } private enum TwoStepVerificationPasswordEntryEntry: ItemListNodeEntry { - case passwordEntryTitle(String) - case passwordEntry(String) + case passwordEntryTitle(PresentationTheme, String) + case passwordEntry(PresentationTheme, String) - case hintTitle(String) - case hintEntry(String) + case hintTitle(PresentationTheme, String) + case hintEntry(PresentationTheme, String) - case emailEntry(String) - case emailInfo(String) + case emailEntry(PresentationTheme, String) + case emailInfo(PresentationTheme, String) var section: ItemListSectionId { return TwoStepVerificationPasswordEntrySection.password.rawValue @@ -70,38 +70,38 @@ private enum TwoStepVerificationPasswordEntryEntry: ItemListNodeEntry { static func ==(lhs: TwoStepVerificationPasswordEntryEntry, rhs: TwoStepVerificationPasswordEntryEntry) -> Bool { switch lhs { - case let .passwordEntryTitle(text): - if case .passwordEntryTitle(text) = rhs { + case let .passwordEntryTitle(lhsTheme, lhsText): + if case let .passwordEntryTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .passwordEntry(text): - if case .passwordEntry(text) = rhs { + case let .passwordEntry(lhsTheme, lhsText): + if case let .passwordEntry(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .hintTitle(text): - if case .hintTitle(text) = rhs { + case let .hintTitle(lhsTheme, lhsText): + if case let .hintTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .hintEntry(text): - if case .hintEntry(text) = rhs { + case let .hintEntry(lhsTheme, lhsText): + if case let .hintEntry(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .emailEntry(text): - if case .emailEntry(text) = rhs { + case let .emailEntry(lhsTheme, lhsText): + if case let .emailEntry(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .emailInfo(text): - if case .emailInfo(text) = rhs { + case let .emailInfo(lhsTheme, lhsText): + if case let .emailInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -115,30 +115,30 @@ private enum TwoStepVerificationPasswordEntryEntry: ItemListNodeEntry { func item(_ arguments: TwoStepVerificationPasswordEntryControllerArguments) -> ListViewItem { switch self { - case let .passwordEntryTitle(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .passwordEntry(text): - return ItemListSingleLineInputItem(title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: "", type: .password, spacing: 0.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in + case let .passwordEntryTitle(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .passwordEntry(theme, text): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: "", type: .password, spacing: 0.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) - case let .hintTitle(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) - case let .hintEntry(text): - return ItemListSingleLineInputItem(title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: "", type: .password, spacing: 0.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in + case let .hintTitle(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .hintEntry(theme, text): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: "", type: .password, spacing: 0.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) - case let .emailEntry(text): - return ItemListSingleLineInputItem(title: NSAttributedString(string: "E-Mail", textColor: .black), text: text, placeholder: "", type: .email, spacing: 10.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in + case let .emailEntry(theme, text): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: "E-Mail", textColor: .black), text: text, placeholder: "", type: .email, spacing: 10.0, tag: TwoStepVerificationPasswordEntryTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) - case let .emailInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) + case let .emailInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } @@ -221,22 +221,22 @@ private struct TwoStepVerificationPasswordEntryControllerState: Equatable { } } -private func twoStepVerificationPasswordEntryControllerEntries(state: TwoStepVerificationPasswordEntryControllerState, mode: TwoStepVerificationPasswordEntryMode) -> [TwoStepVerificationPasswordEntryEntry] { +private func twoStepVerificationPasswordEntryControllerEntries(presentationData: PresentationData, state: TwoStepVerificationPasswordEntryControllerState, mode: TwoStepVerificationPasswordEntryMode) -> [TwoStepVerificationPasswordEntryEntry] { var entries: [TwoStepVerificationPasswordEntryEntry] = [] switch state.stage { case let .entry(text): - entries.append(.passwordEntryTitle("Enter a password")) - entries.append(.passwordEntry(text)) + entries.append(.passwordEntryTitle(presentationData.theme, presentationData.strings.TwoStepAuth_SetupPasswordEnterPasswordNew)) + entries.append(.passwordEntry(presentationData.theme, text)) case let .reentry(_, text): - entries.append(.passwordEntryTitle("Please re-enter your password")) - entries.append(.passwordEntry(text)) + entries.append(.passwordEntryTitle(presentationData.theme, presentationData.strings.TwoStepAuth_SetupPasswordEnterPasswordChange)) + entries.append(.passwordEntry(presentationData.theme, text)) case let .hint(_, text): - entries.append(.hintTitle("Please create a hint for your password")) - entries.append(.hintEntry(text)) + entries.append(.hintTitle(presentationData.theme, presentationData.strings.TwoStepAuth_SetupHint)) + entries.append(.hintEntry(presentationData.theme, text)) case let .email(_, _, text): - entries.append(.emailEntry(text)) - entries.append(.emailInfo("Please add your valid e-mail. It is the only way to recover a forgotten password.")) + entries.append(.emailEntry(presentationData.theme, text)) + entries.append(.emailInfo(presentationData.theme, presentationData.strings.TwoStepAuth_EmailHelp)) } return entries @@ -384,10 +384,10 @@ func twoStepVerificationPasswordEntryController(account: Account, mode: TwoStepV checkPassword() }) - let signal = statePromise.get() |> deliverOnMainQueue - |> map { state -> (ItemListControllerState, (ItemListNodeState, TwoStepVerificationPasswordEntryEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> deliverOnMainQueue + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, TwoStepVerificationPasswordEntryEntry.ItemGenerationArguments)) in - let leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { dismissImpl?() }) @@ -408,22 +408,20 @@ func twoStepVerificationPasswordEntryController(account: Account, mode: TwoStepV case .hint, .email: break } - rightNavigationButton = ItemListNavigationButton(title: "Next", style: .bold, enabled: nextEnabled, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: nextEnabled, action: { checkPassword() }) } - let controllerState = ItemListControllerState(title: .text("Password"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, animateChanges: false) - let listState = ItemListNodeState(entries: twoStepVerificationPasswordEntryControllerEntries(state: state, mode: mode), style: .blocks, focusItemTag: TwoStepVerificationPasswordEntryTag.input, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.TwoStepAuth_EnterPasswordTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: twoStepVerificationPasswordEntryControllerEntries(presentationData: presentationData, state: state, mode: mode), style: .blocks, focusItemTag: TwoStepVerificationPasswordEntryTag.input, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) - + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/TwoStepVerificationResetController.swift b/TelegramUI/TwoStepVerificationResetController.swift index af6cf4fcb8..71d53b1d3d 100644 --- a/TelegramUI/TwoStepVerificationResetController.swift +++ b/TelegramUI/TwoStepVerificationResetController.swift @@ -40,8 +40,8 @@ private enum TwoStepVerificationResetTag: ItemListItemTag { } private enum TwoStepVerificationResetEntry: ItemListNodeEntry { - case codeEntry(String) - case codeInfo(String) + case codeEntry(PresentationTheme, String) + case codeInfo(PresentationTheme, String) var section: ItemListSectionId { return TwoStepVerificationResetSection.password.rawValue @@ -58,14 +58,14 @@ private enum TwoStepVerificationResetEntry: ItemListNodeEntry { static func ==(lhs: TwoStepVerificationResetEntry, rhs: TwoStepVerificationResetEntry) -> Bool { switch lhs { - case let .codeEntry(text): - if case .codeEntry(text) = rhs { + case let .codeEntry(lhsTheme, lhsText): + if case let .codeEntry(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .codeInfo(text): - if case .codeInfo(text) = rhs { + case let .codeInfo(lhsTheme, lhsText): + if case let .codeInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -79,14 +79,14 @@ private enum TwoStepVerificationResetEntry: ItemListNodeEntry { func item(_ arguments: TwoStepVerificationResetControllerArguments) -> ListViewItem { switch self { - case let .codeEntry(text): - return ItemListSingleLineInputItem(title: NSAttributedString(string: "Code", textColor: .black), text: text, placeholder: "", type: .password, spacing: 10.0, tag: TwoStepVerificationResetTag.input, sectionId: self.section, textUpdated: { updatedText in + case let .codeEntry(theme, text): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: "Code", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .password, spacing: 10.0, tag: TwoStepVerificationResetTag.input, sectionId: self.section, textUpdated: { updatedText in arguments.updateEntryText(updatedText) }, action: { arguments.next() }) - case let .codeInfo(text): - return ItemListSectionHeaderItem(text: text, sectionId: self.section) + case let .codeInfo(theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) } } } @@ -120,11 +120,11 @@ private struct TwoStepVerificationResetControllerState: Equatable { } } -private func twoStepVerificationResetControllerEntries(state: TwoStepVerificationResetControllerState, emailPattern: String) -> [TwoStepVerificationResetEntry] { +private func twoStepVerificationResetControllerEntries(presentationData: PresentationData, state: TwoStepVerificationResetControllerState, emailPattern: String) -> [TwoStepVerificationResetEntry] { var entries: [TwoStepVerificationResetEntry] = [] - entries.append(.codeEntry(state.codeText)) - entries.append(.codeInfo("Please check your e-mail and enter the 6-digit code we've sent there to deactivate your cloud password.\n\n[Having trouble accessing your e-mail \(escapedPlaintextForMarkdown(emailPattern))?]()")) + entries.append(.codeEntry(presentationData.theme, state.codeText)) + entries.append(.codeInfo(presentationData.theme, "Please check your e-mail and enter the 6-digit code we've sent there to deactivate your cloud password.\n\n[Having trouble accessing your e-mail \(escapedPlaintextForMarkdown(emailPattern))?]()")) return entries } @@ -192,10 +192,10 @@ func twoStepVerificationResetController(account: Account, emailPattern: String, presentControllerImpl?(standardTextAlertController(title: nil, text: "Your remaining options are either to remember your password or to reset your account.", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) - let signal = statePromise.get() |> deliverOnMainQueue - |> map { state -> (ItemListControllerState, (ItemListNodeState, TwoStepVerificationResetEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get()) |> deliverOnMainQueue + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, TwoStepVerificationResetEntry.ItemGenerationArguments)) in - let leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { dismissImpl?() }) @@ -207,22 +207,20 @@ func twoStepVerificationResetController(account: Account, emailPattern: String, if state.codeText.isEmpty { nextEnabled = false } - rightNavigationButton = ItemListNavigationButton(title: "Next", style: .bold, enabled: nextEnabled, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: nextEnabled, action: { checkCode() }) } - let controllerState = ItemListControllerState(title: .text("E-Mail Code"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, animateChanges: false) - let listState = ItemListNodeState(entries: twoStepVerificationResetControllerEntries(state: state, emailPattern: emailPattern), style: .blocks, focusItemTag: TwoStepVerificationResetTag.input, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.TwoStepAuth_RecoveryTitle), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: twoStepVerificationResetControllerEntries(presentationData: presentationData, state: state, emailPattern: emailPattern), style: .blocks, focusItemTag: TwoStepVerificationResetTag.input, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) - + let controller = ItemListController(account: account, state: signal) presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window, with: p) diff --git a/TelegramUI/TwoStepVerificationUnlockController.swift b/TelegramUI/TwoStepVerificationUnlockController.swift index 10ba9aad4f..c552c81513 100644 --- a/TelegramUI/TwoStepVerificationUnlockController.swift +++ b/TelegramUI/TwoStepVerificationUnlockController.swift @@ -46,18 +46,18 @@ private enum TwoStepVerificationUnlockSettingsEntryTag: ItemListItemTag { } private enum TwoStepVerificationUnlockSettingsEntry: ItemListNodeEntry { - case passwordEntry(String) - case passwordEntryInfo(String) + case passwordEntry(PresentationTheme, String, String) + case passwordEntryInfo(PresentationTheme, String) - case passwordSetup - case passwordSetupInfo(String) + case passwordSetup(PresentationTheme, String) + case passwordSetupInfo(PresentationTheme, String) - case changePassword - case turnPasswordOff - case setupRecoveryEmail(Bool) - case passwordInfo(String) + case changePassword(PresentationTheme, String) + case turnPasswordOff(PresentationTheme, String) + case setupRecoveryEmail(PresentationTheme, String) + case passwordInfo(PresentationTheme, String) - case pendingEmailInfo(String) + case pendingEmailInfo(PresentationTheme, String) var section: ItemListSectionId { return TwoStepVerificationUnlockSettingsSection.password.rawValue @@ -88,44 +88,60 @@ private enum TwoStepVerificationUnlockSettingsEntry: ItemListNodeEntry { static func ==(lhs: TwoStepVerificationUnlockSettingsEntry, rhs: TwoStepVerificationUnlockSettingsEntry) -> Bool { switch lhs { - case let .passwordEntry(text): - if case .passwordEntry(text) = rhs { + case let .passwordEntry(lhsTheme, lhsText, lhsValue): + if case let .passwordEntry(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .passwordEntryInfo(text): - if case .passwordEntryInfo(text) = rhs { + case let .passwordEntryInfo(lhsTheme, lhsText): + if case let .passwordEntryInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .passwordSetupInfo(text): - if case .passwordSetupInfo(text) = rhs { + case let .passwordSetupInfo(lhsTheme, lhsText): + if case let .passwordSetupInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .setupRecoveryEmail(exists): - if case .setupRecoveryEmail(exists) = rhs { + case let .setupRecoveryEmail(lhsTheme, lhsText): + if case let .setupRecoveryEmail(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .passwordInfo(text): - if case .passwordInfo(text) = rhs { + case let .passwordInfo(lhsTheme, lhsText): + if case let .passwordInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .pendingEmailInfo(text): - if case .pendingEmailInfo(text) = rhs { + case let .pendingEmailInfo(lhsTheme, lhsText): + if case let .pendingEmailInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .passwordSetup(lhsTheme, lhsText): + if case let .passwordSetup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .changePassword(lhsTheme, lhsText): + if case let .changePassword(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .turnPasswordOff(lhsTheme, lhsText): + if case let .turnPasswordOff(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case .passwordSetup, .changePassword, .turnPasswordOff: - return lhs.stableId == rhs.stableId } } @@ -135,49 +151,43 @@ private enum TwoStepVerificationUnlockSettingsEntry: ItemListNodeEntry { func item(_ arguments: TwoStepVerificationUnlockSettingsControllerArguments) -> ListViewItem { switch self { - case let .passwordEntry(text): - return ItemListSingleLineInputItem(title: NSAttributedString(string: "Password", textColor: .black), text: text, placeholder: "", type: .password, spacing: 10.0, tag: TwoStepVerificationUnlockSettingsEntryTag.password, sectionId: self.section, textUpdated: { updatedText in + case let .passwordEntry(theme, text, value): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: text, textColor: theme.list.itemPrimaryTextColor), text: value, placeholder: "", type: .password, spacing: 10.0, tag: TwoStepVerificationUnlockSettingsEntryTag.password, sectionId: self.section, textUpdated: { updatedText in arguments.updatePasswordText(updatedText) }, action: { }) - case let .passwordEntryInfo(text): - return ItemListTextItem(text: .markdown(text), sectionId: self.section, linkAction: { action in + case let .passwordEntryInfo(theme, text): + return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { action in switch action { case .tap: arguments.openForgotPassword() } }) - case .passwordSetup: - return ItemListActionItem(title: "Set Additional Password", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .passwordSetup(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openSetupPassword() }) - case let .passwordSetupInfo(text): - return ItemListTextItem(text: .markdown(text), sectionId: self.section) - case .changePassword: - return ItemListActionItem(title: "Change Password", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .passwordSetupInfo(theme, text): + return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) + case let .changePassword(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openSetupPassword() }) - case .turnPasswordOff: - return ItemListActionItem(title: "Turn Password Off", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .turnPasswordOff(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openDisablePassword() }) - case let .setupRecoveryEmail(exists): - let title: String - if exists { - title = "Change Recovery E-Mail" - } else { - title = "Set Recovery E-Mail" - } - return ItemListActionItem(title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + case let .setupRecoveryEmail(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openSetupEmail() }) - case let .passwordInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .pendingEmailInfo(text): - return ItemListTextItem(text: .markdown(text), sectionId: self.section, linkAction: { action in + case let .passwordInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .pendingEmailInfo(theme, text): + return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section, linkAction: { action in switch action { - case .tap: - arguments.openResetPendingEmail() + case .tap: + arguments.openResetPendingEmail() } }) } @@ -213,37 +223,37 @@ private struct TwoStepVerificationUnlockSettingsControllerState: Equatable { } } -private func twoStepVerificationUnlockSettingsControllerEntries(state: TwoStepVerificationUnlockSettingsControllerState,data: TwoStepVerificationUnlockSettingsControllerData) -> [TwoStepVerificationUnlockSettingsEntry] { +private func twoStepVerificationUnlockSettingsControllerEntries(presentationData: PresentationData, state: TwoStepVerificationUnlockSettingsControllerState,data: TwoStepVerificationUnlockSettingsControllerData) -> [TwoStepVerificationUnlockSettingsEntry] { var entries: [TwoStepVerificationUnlockSettingsEntry] = [] switch data { case let .access(configuration): if let configuration = configuration { switch configuration { - case let .notSet(pendingEmailPattern): - if pendingEmailPattern.isEmpty { - entries.append(.passwordSetup) - entries.append(.passwordSetupInfo("You can set a password that will be required when you log in on a new device in addition to the code you cat in the SMS.")) - } else { - entries.append(.pendingEmailInfo("Please check your e-mail and click on the validation link to complete Two-Step verification setup. Be sure to check the spam folder as well.\n\n\(pendingEmailPattern)\n\n[Abort Two-Step Verification Setup]()")) - } - case let .set(hint, _, _): - entries.append(.passwordEntry(state.passwordText)) - if hint.isEmpty { - entries.append(.passwordEntryInfo("You have enabled Two-Step verification, so your account is protected with an additional password.\n\n[Forgot password?](forgot)")) - } else { - entries.append(.passwordEntryInfo("hint: \(escapedPlaintextForMarkdown(hint))\n\nYou have enabled Two-Step verification, so your account is protected with an additional password.\n\n[Forgot password?](forgot)")) - } + case let .notSet(pendingEmailPattern): + if pendingEmailPattern.isEmpty { + entries.append(.passwordSetup(presentationData.theme, presentationData.strings.TwoStepAuth_SetPassword)) + entries.append(.passwordSetupInfo(presentationData.theme, presentationData.strings.TwoStepAuth_SetPasswordHelp)) + } else { + entries.append(.pendingEmailInfo(presentationData.theme, presentationData.strings.TwoStepAuth_ConfirmationText + "\n\n\(pendingEmailPattern)\n\n[" + presentationData.strings.TwoStepAuth_ConfirmationAbort + "]()")) + } + case let .set(hint, _, _): + entries.append(.passwordEntry(presentationData.theme, presentationData.strings.TwoStepAuth_EnterPasswordPassword, state.passwordText)) + if hint.isEmpty { + entries.append(.passwordEntryInfo(presentationData.theme, presentationData.strings.TwoStepAuth_EnterPasswordHelp + "\n\n[" + presentationData.strings.TwoStepAuth_EnterPasswordForgot + "](forgot)")) + } else { + entries.append(.passwordEntryInfo(presentationData.theme, presentationData.strings.TwoStepAuth_EnterPasswordHint(escapedPlaintextForMarkdown(hint)).0 + "\n\n" + presentationData.strings.TwoStepAuth_EnterPasswordHelp + "\n\n[" + presentationData.strings.TwoStepAuth_EnterPasswordForgot + "](forgot)")) + } } } case let .manage(_, emailSet, pendingEmailPattern): - entries.append(.changePassword) - entries.append(.turnPasswordOff) - entries.append(.setupRecoveryEmail(emailSet)) + entries.append(.changePassword(presentationData.theme, presentationData.strings.TwoStepAuth_ChangePassword)) + entries.append(.turnPasswordOff(presentationData.theme, presentationData.strings.TwoStepAuth_RemovePassword)) + entries.append(.setupRecoveryEmail(presentationData.theme, emailSet ? presentationData.strings.TwoStepAuth_ChangeEmail : presentationData.strings.TwoStepAuth_SetupEmail)) if pendingEmailPattern.isEmpty { - entries.append(.passwordInfo("You have enabled Two-Step verification.\nYou'll need the password you set up here to log in to your Telegram account.")) + entries.append(.passwordInfo(presentationData.theme, presentationData.strings.TwoStepAuth_EnterPasswordHelp)) } else { - entries.append(.passwordInfo("Your recovery e-mail \(pendingEmailPattern) is not yet active and pending confirmation.")) + entries.append(.passwordInfo(presentationData.theme, presentationData.strings.TwoStepAuth_PendingEmailHelp(pendingEmailPattern).0)) } } @@ -442,15 +452,15 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep })) }) - let signal = combineLatest(statePromise.get(), dataPromise.get() |> deliverOnMainQueue) |> deliverOnMainQueue - |> map { state, data -> (ItemListControllerState, (ItemListNodeState, TwoStepVerificationUnlockSettingsEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), dataPromise.get() |> deliverOnMainQueue) |> deliverOnMainQueue + |> map { presentationData, state, data -> (ItemListControllerState, (ItemListNodeState, TwoStepVerificationUnlockSettingsEntry.ItemGenerationArguments)) in var rightNavigationButton: ItemListNavigationButton? var emptyStateItem: ItemListControllerEmptyStateItem? let title: String switch data { case let .access(configuration): - title = "Password" + title = presentationData.strings.TwoStepAuth_Title if let configuration = configuration { if state.checking { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) @@ -459,7 +469,7 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep case .notSet: break case .set: - rightNavigationButton = ItemListNavigationButton(title: "Next", style: .bold, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: true, action: { var wasChecking = false var password: String? updateState { state in @@ -500,22 +510,21 @@ func twoStepVerificationUnlockSettingsController(account: Account, mode: TwoStep emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() } case .manage: - title = "Two-Step Verification" + title = presentationData.strings.PrivacySettings_TwoStepAuth if state.checking { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) } } - let controllerState = ItemListControllerState(title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: false) - let listState = ItemListNodeState(entries: twoStepVerificationUnlockSettingsControllerEntries(state: state, data: data), style: .blocks, focusItemTag: TwoStepVerificationUnlockSettingsEntryTag.password, emptyStateItem: emptyStateItem, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: twoStepVerificationUnlockSettingsControllerEntries(presentationData: presentationData, state: state, data: data), style: .blocks, focusItemTag: TwoStepVerificationUnlockSettingsEntryTag.password, emptyStateItem: emptyStateItem, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + let controller = ItemListController(account: account, state: signal) replaceControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.replaceTopController(c, animated: true) } diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 2a6e18ed32..8002910ac4 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -16,8 +16,9 @@ private final class UserInfoControllerArguments { let updatePeerBlocked: (Bool) -> Void let deleteContact: () -> Void let displayUsernameContextMenu: (String) -> Void + let call: () -> Void - init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void) { + init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, tapAvatarAction: @escaping () -> Void, openChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void, call: @escaping () -> Void) { self.account = account self.avatarAndNameInfoContext = avatarAndNameInfoContext self.updateEditingName = updateEditingName @@ -29,6 +30,7 @@ private final class UserInfoControllerArguments { self.updatePeerBlocked = updatePeerBlocked self.deleteContact = deleteContact self.displayUsernameContextMenu = displayUsernameContextMenu + self.call = call } } @@ -44,19 +46,19 @@ private enum UserInfoEntryTag { } private enum UserInfoEntry: ItemListNodeEntry { - case info(peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState) - case about(text: String) - case phoneNumber(index: Int, value: PhoneNumberWithLabel) - case userName(value: String) - case sendMessage - case shareContact - case startSecretChat - case sharedMedia - case notifications(settings: PeerNotificationSettings?) - case notificationSound(settings: PeerNotificationSettings?) - case groupsInCommon(Int32) - case secretEncryptionKey(SecretChatKeyFingerprint) - case block(action: DestructiveUserInfoAction) + case info(PresentationTheme, PresentationStrings, peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, displayCall: Bool) + case about(PresentationTheme, String, String) + case phoneNumber(PresentationTheme, Int, PhoneNumberWithLabel) + case userName(PresentationTheme, String, String) + case sendMessage(PresentationTheme, String) + case shareContact(PresentationTheme, String) + case startSecretChat(PresentationTheme, String) + case sharedMedia(PresentationTheme, String) + case notifications(PresentationTheme, String, String) + case notificationSound(PresentationTheme, String, String) + case groupsInCommon(PresentationTheme, String, Int32) + case secretEncryptionKey(PresentationTheme, String, SecretChatKeyFingerprint) + case block(PresentationTheme, String, DestructiveUserInfoAction) var section: ItemListSectionId { switch self { @@ -77,9 +79,15 @@ private enum UserInfoEntry: ItemListNodeEntry { static func ==(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool { switch lhs { - case let .info(lhsPeer, lhsPresence, lhsCachedData, lhsState): + case let .info(lhsTheme, lhsStrings, lhsPeer, lhsPresence, lhsCachedData, lhsState, lhsDisplayCall): switch rhs { - case let .info(rhsPeer, rhsPresence, rhsCachedData, rhsState): + case let .info(rhsTheme, rhsStrings, rhsPeer, rhsPresence, rhsCachedData, rhsState, rhsDisplayCall): + if lhsTheme !== rhsTheme { + return false + } + if lhsStrings !== rhsStrings { + return false + } if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false @@ -104,101 +112,84 @@ private enum UserInfoEntry: ItemListNodeEntry { if lhsState != rhsState { return false } - return true - default: - return false - } - case let .about(lhsText): - switch rhs { - case .about(lhsText): - return true - default: - return false - } - case let .phoneNumber(lhsIndex, lhsValue): - switch rhs { - case let .phoneNumber(rhsIndex, rhsValue) where lhsIndex == rhsIndex && lhsValue == rhsValue: - return true - default: - return false - } - case let .userName(value): - switch rhs { - case .userName(value): - return true - default: - return false - } - case .sendMessage: - switch rhs { - case .sendMessage: - return true - default: - return false - } - case .shareContact: - switch rhs { - case .shareContact: - return true - default: - return false - } - case .startSecretChat: - switch rhs { - case .startSecretChat: - return true - default: - return false - } - case .sharedMedia: - switch rhs { - case .sharedMedia: - return true - default: - return false - } - case let .notifications(lhsSettings): - switch rhs { - case let .notifications(rhsSettings): - if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { - return lhsSettings.isEqual(to: rhsSettings) - } else if (lhsSettings != nil) != (rhsSettings != nil) { + if lhsDisplayCall != rhsDisplayCall { return false } return true default: return false } - case let .notificationSound(lhsSettings): - switch rhs { - case let .notificationSound(rhsSettings): - if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { - return lhsSettings.isEqual(to: rhsSettings) - } else if (lhsSettings != nil) != (rhsSettings != nil) { - return false - } - return true - default: - return false - } - case let .groupsInCommon(count): - if case .groupsInCommon(count) = rhs { + case let .about(lhsTheme, lhsText, lhsValue): + if case let .about(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .secretEncryptionKey(fingerprint): - if case .secretEncryptionKey(fingerprint) = rhs { + case let .phoneNumber(lhsTheme, lhsIndex, lhsValue): + if case let .phoneNumber(rhsTheme, rhsIndex, rhsValue) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsValue == rhsValue { return true } else { return false } - case let .block(action): - switch rhs { - case .block(action): - return true - default: - return false + case let .userName(lhsTheme, lhsText, lhsValue): + if case let .userName(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .sendMessage(lhsTheme, lhsText): + if case let .sendMessage(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .shareContact(lhsTheme, lhsText): + if case let .shareContact(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .startSecretChat(lhsTheme, lhsText): + if case let .startSecretChat(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .sharedMedia(lhsTheme, lhsText): + if case let .sharedMedia(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .notifications(lhsTheme, lhsText, lhsValue): + if case let .notifications(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .notificationSound(lhsTheme, lhsText, lhsValue): + if case let .notificationSound(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .groupsInCommon(lhsTheme, lhsText, lhsValue): + if case let .groupsInCommon(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .secretEncryptionKey(lhsTheme, lhsText, lhsValue): + if case let .secretEncryptionKey(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .block(lhsTheme, lhsText, lhsAction): + if case let .block(rhsTheme, rhsText, rhsAction) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsAction == rhsAction { + return true + } else { + return false } } } @@ -209,7 +200,7 @@ private enum UserInfoEntry: ItemListNodeEntry { return 0 case .about: return 1 - case let .phoneNumber(index, _): + case let .phoneNumber(_, index, _): return 2 + index case .userName: return 1000 @@ -240,71 +231,56 @@ private enum UserInfoEntry: ItemListNodeEntry { func item(_ arguments: UserInfoControllerArguments) -> ListViewItem { switch self { - case let .info(peer, presence, cachedData, state): - return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: presence, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in + case let .info(theme, strings, peer, presence, cachedData, state, displayCall): + return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, peer: peer, presence: presence, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in arguments.updateEditingName(editingName) }, avatarTapped: { arguments.tapAvatarAction() - }, context: arguments.avatarAndNameInfoContext) - case let .about(text): - return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section, action: nil) - case let .phoneNumber(_, value): - return ItemListTextWithLabelItem(label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section, action: { + }, context: arguments.avatarAndNameInfoContext, call: displayCall ? { + arguments.call() + } : nil) + case let .about(theme, text, value): + return ItemListTextWithLabelItem(theme: theme, label: text, text: value, multiline: true, sectionId: self.section, action: nil) + case let .phoneNumber(theme, _, value): + return ItemListTextWithLabelItem(theme: theme, label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section, action: { }) - case let .userName(value): - return ItemListTextWithLabelItem(label: "username", text: "@\(value)", multiline: false, sectionId: self.section, action: { + case let .userName(theme, text, value): + return ItemListTextWithLabelItem(theme: theme, label: text, text: "@\(value)", multiline: false, sectionId: self.section, action: { arguments.displayUsernameContextMenu("@" + value) }, tag: UserInfoEntryTag.username) - case .sendMessage: - return ItemListActionItem(title: "Send Message", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + case let .sendMessage(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.openChat() }) - case .shareContact: - return ItemListActionItem(title: "Share Contact", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + case let .shareContact(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { }) - case .startSecretChat: - return ItemListActionItem(title: "Start Secret Chat", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { + case let .startSecretChat(theme, text): + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { }) - case .sharedMedia: - return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .plain, action: { + case let .sharedMedia(theme, text): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: { arguments.openSharedMedia() }) - case let .notifications(settings): - let label: String - if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState { - label = "Disabled" - } else { - label = "Enabled" - } - return ItemListDisclosureItem(title: "Notifications", label: label, sectionId: self.section, style: .plain, action: { + case let .notifications(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { arguments.changeNotificationMuteSettings() }) - case let .notificationSound(settings): - let label: String - label = "Default" - return ItemListDisclosureItem(title: "Sound", label: label, sectionId: self.section, style: .plain, action: { + case let .notificationSound(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: { }) - case let .groupsInCommon(count): - return ItemListDisclosureItem(title: "Groups in Common", label: "\(count)", sectionId: self.section, style: .plain, action: { + case let .groupsInCommon(theme, text, value): + return ItemListDisclosureItem(theme: theme, title: text, label: "\(value)", sectionId: self.section, style: .plain, action: { arguments.openGroupsInCommon() }) - case let .secretEncryptionKey(fingerprint): - return ItemListDisclosureItem(title: "Encryption Key", label: "", sectionId: self.section, style: .plain, action: { + case let .secretEncryptionKey(theme, text, fingerprint): + return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: { }) - case let .block(action): - let title: String - switch action { - case .block: - title = "Block User" - case .unblock: - title = "Unblock User" - case .removeContact: - title = "Remove Contact" - } - return ItemListActionItem(title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { + case let .block(theme, text, action): + return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: { switch action { case .block: arguments.updatePeerBlocked(true) @@ -368,7 +344,18 @@ private struct UserInfoState: Equatable { } } -private func userInfoEntries(account: Account, view: PeerView, state: UserInfoState, peerChatState: Coding?) -> [UserInfoEntry] { +private func stringForBlockAction(strings: PresentationStrings, action: DestructiveUserInfoAction) -> String { + switch action { + case .block: + return strings.Conversation_BlockUser + case .unblock: + return strings.Conversation_UnblockUser + case .removeContact: + return strings.UserInfo_DeleteContact + } +} + +private func userInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, state: UserInfoState, peerChatState: Coding?) -> [UserInfoEntry] { var entries: [UserInfoEntry] = [] guard let peer = view.peers[view.peerId], let user = peerViewMainPeer(view) as? TelegramUser else { @@ -386,51 +373,57 @@ private func userInfoEntries(account: Account, view: PeerView, state: UserInfoSt } } - entries.append(UserInfoEntry.info(peer: user, presence: view.peerPresences[user.id], cachedData: view.cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil))) + entries.append(UserInfoEntry.info(presentationData.theme, presentationData.strings, peer: user, presence: view.peerPresences[user.id], cachedData: view.cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), displayCall: true)) if let cachedUserData = view.cachedData as? CachedUserData { if let about = cachedUserData.about, !about.isEmpty { - entries.append(UserInfoEntry.about(text: about)) + entries.append(UserInfoEntry.about(presentationData.theme, presentationData.strings.Profile_About, about)) } } if let phoneNumber = user.phone, !phoneNumber.isEmpty { - entries.append(UserInfoEntry.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber))) + entries.append(UserInfoEntry.phoneNumber(presentationData.theme, 0, PhoneNumberWithLabel(label: "home", number: phoneNumber))) } if !isEditing { if let username = user.username, !username.isEmpty { - entries.append(UserInfoEntry.userName(value: username)) + entries.append(UserInfoEntry.userName(presentationData.theme, presentationData.strings.Profile_Username, username)) } if !(peer is TelegramSecretChat) { - entries.append(UserInfoEntry.sendMessage) + entries.append(UserInfoEntry.sendMessage(presentationData.theme, presentationData.strings.UserInfo_SendMessage)) if view.peerIsContact { - entries.append(UserInfoEntry.shareContact) + entries.append(UserInfoEntry.shareContact(presentationData.theme, presentationData.strings.UserInfo_ShareContact)) } - entries.append(UserInfoEntry.startSecretChat) + entries.append(UserInfoEntry.startSecretChat(presentationData.theme, presentationData.strings.UserInfo_StartSecretChat)) } - entries.append(UserInfoEntry.sharedMedia) + entries.append(UserInfoEntry.sharedMedia(presentationData.theme, presentationData.strings.GroupInfo_SharedMedia)) } - entries.append(UserInfoEntry.notifications(settings: view.notificationSettings)) + let notificationsLabel: String + if let settings = view.notificationSettings as? TelegramPeerNotificationSettings, case .muted = settings.muteState { + notificationsLabel = presentationData.strings.UserInfo_NotificationsDisabled + } else { + notificationsLabel = presentationData.strings.UserInfo_NotificationsEnabled + } + entries.append(UserInfoEntry.notifications(presentationData.theme, presentationData.strings.GroupInfo_Notifications, notificationsLabel)) if let groupsInCommon = (view.cachedData as? CachedUserData)?.commonGroupCount, groupsInCommon != 0 && !isEditing { - entries.append(UserInfoEntry.groupsInCommon(groupsInCommon)) + entries.append(UserInfoEntry.groupsInCommon(presentationData.theme, presentationData.strings.UserInfo_GroupsInCommon, groupsInCommon)) } if peer is TelegramSecretChat, let peerChatState = peerChatState as? SecretChatKeyState, let keyFingerprint = peerChatState.keyFingerprint { - entries.append(UserInfoEntry.secretEncryptionKey(keyFingerprint)) + entries.append(UserInfoEntry.secretEncryptionKey(presentationData.theme, presentationData.strings.Profile_EncryptionKey, keyFingerprint)) } if isEditing { - entries.append(UserInfoEntry.notificationSound(settings: view.notificationSettings)) + entries.append(UserInfoEntry.notificationSound(presentationData.theme, presentationData.strings.GroupInfo_Sound, "Default")) if view.peerIsContact { - entries.append(UserInfoEntry.block(action: .removeContact)) + entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .removeContact), .removeContact)) } } else { if let cachedData = view.cachedData as? CachedUserData { if cachedData.isBlocked { - entries.append(UserInfoEntry.block(action: .unblock)) + entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .unblock), .unblock)) } else { - entries.append(UserInfoEntry.block(action: .block)) + entries.append(UserInfoEntry.block(presentationData.theme, stringForBlockAction(strings: presentationData.strings, action: .block), .block)) } } } @@ -549,15 +542,33 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }, displayUsernameContextMenu: { text in displayUsernameContextMenuImpl?(text) + }, call: { + let callResult = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: false) + if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { + if currentPeerId == peerId { + account.telegramApplicationContext.navigateToCurrentCall?() + } else { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let _ = (account.postbox.modify { modifier -> (Peer?, Peer?) in + return (modifier.getPeer(peerId), modifier.getPeer(currentPeerId)) + } |> deliverOnMainQueue).start(next: { peer, current in + if let peer = peer, let current = current { + presentControllerImpl?(standardTextAlertController(title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: { + let _ = account.telegramApplicationContext.callManager?.requestCall(peerId: peerId, endCurrentIfAny: true) + })]), nil) + } + }) + } + } }) - let signal = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [.peerChatState(peerId: peerId)])) - |> map { state, view, chatState -> (ItemListControllerState, (ItemListNodeState, UserInfoEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [.peerChatState(peerId: peerId)])) + |> map { presentationData, state, view, chatState -> (ItemListControllerState, (ItemListNodeState, UserInfoEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) var leftNavigationButton: ItemListNavigationButton? let rightNavigationButton: ItemListNavigationButton if let editingState = state.editingState { - leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { updateState { $0.withUpdatedEditingState(nil) } @@ -571,7 +582,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll if state.savingData { rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: doneEnabled, action: {}) } else { - rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: .bold, enabled: doneEnabled, action: { var updateName: ItemListAvatarAndNameInfoItemName? updateState { state in if let editingState = state.editingState, let editingName = editingState.editingName { @@ -602,7 +613,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }) } } else { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Edit, style: .regular, enabled: true, action: { if let user = peer { updateState { state in return state.withUpdatedEditingState(UserInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(user.indexName))) @@ -611,15 +622,15 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll }) } - let controllerState = ItemListControllerState(title: .text("Info"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton) - let listState = ItemListNodeState(entries: userInfoEntries(account: account, view: view, state: state, peerChatState: (chatState.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState), style: .plain) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil) + let listState = ItemListNodeState(entries: userInfoEntries(account: account, presentationData: presentationData, view: view, state: state, peerChatState: (chatState.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState), style: .plain) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) + let controller = ItemListController(account: account, state: signal) pushControllerImpl = { [weak controller] value in (controller?.navigationController as? NavigationController)?.pushViewController(value) diff --git a/TelegramUI/UsernameSetupController.swift b/TelegramUI/UsernameSetupController.swift index b3191cf5d5..9d3b85f5d7 100644 --- a/TelegramUI/UsernameSetupController.swift +++ b/TelegramUI/UsernameSetupController.swift @@ -20,9 +20,9 @@ private enum UsernameSetupSection: Int32 { } private enum UsernameSetupEntry: ItemListNodeEntry { - case editablePublicLink(String?, String) - case publicLinkStatus(String, AddressNameValidationStatus) - case publicLinkInfo(String) + case editablePublicLink(PresentationTheme, String?, String) + case publicLinkStatus(PresentationTheme, String, AddressNameValidationStatus) + case publicLinkInfo(PresentationTheme, String) var section: ItemListSectionId { switch self { @@ -44,20 +44,20 @@ private enum UsernameSetupEntry: ItemListNodeEntry { static func ==(lhs: UsernameSetupEntry, rhs: UsernameSetupEntry) -> Bool { switch lhs { - case let .editablePublicLink(lhsCurrentText, lhsText): - if case let .editablePublicLink(rhsCurrentText, rhsText) = rhs, lhsCurrentText == rhsCurrentText, lhsText == rhsText { + case let .editablePublicLink(lhsTheme, lhsCurrentText, lhsText): + if case let .editablePublicLink(rhsTheme, rhsCurrentText, rhsText) = rhs, lhsTheme === rhsTheme, lhsCurrentText == rhsCurrentText, lhsText == rhsText { return true } else { return false } - case let .publicLinkInfo(text): - if case .publicLinkInfo(text) = rhs { + case let .publicLinkInfo(lhsTheme, lhsText): + if case let .publicLinkInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } - case let .publicLinkStatus(addressName, status): - if case .publicLinkStatus(addressName, status) = rhs { + case let .publicLinkStatus(lhsTheme, lhsAddressName, lhsStatus): + if case let .publicLinkStatus(rhsTheme, rhsAddressName, rhsStatus) = rhs, lhsTheme === rhsTheme, lhsAddressName == rhsAddressName, lhsStatus == rhsStatus { return true } else { return false @@ -71,42 +71,42 @@ private enum UsernameSetupEntry: ItemListNodeEntry { func item(_ arguments: UsernameSetupControllerArguments) -> ListViewItem { switch self { - case let .editablePublicLink(currentText, text): - return ItemListSingleLineInputItem(title: NSAttributedString(string: "t.me/", textColor: .black), text: text, placeholder: "", sectionId: self.section, textUpdated: { updatedText in + case let .editablePublicLink(theme, currentText, text): + return ItemListSingleLineInputItem(theme: theme, title: NSAttributedString(string: "t.me/", textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", sectionId: self.section, textUpdated: { updatedText in arguments.updatePublicLinkText(currentText, updatedText) }, action: { }) - case let .publicLinkInfo(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) - case let .publicLinkStatus(addressName, status): + case let .publicLinkInfo(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) + case let .publicLinkStatus(theme, addressName, status): var displayActivity = false let text: NSAttributedString switch status { case let .invalidFormat(error): switch error { case .startsWithDigit: - text = NSAttributedString(string: "Names can't start with a digit.", textColor: UIColor(0xcf3030)) + text = NSAttributedString(string: "Names can't start with a digit.", textColor: UIColor(rgb: 0xcf3030)) case .startsWithUnderscore: - text = NSAttributedString(string: "Names can't start with an underscore.", textColor: UIColor(0xcf3030)) + text = NSAttributedString(string: "Names can't start with an underscore.", textColor: UIColor(rgb: 0xcf3030)) case .endsWithUnderscore: - text = NSAttributedString(string: "Names can't end with an underscore.", textColor: UIColor(0xcf3030)) + text = NSAttributedString(string: "Names can't end with an underscore.", textColor: UIColor(rgb: 0xcf3030)) case .tooShort: - text = NSAttributedString(string: "Names must have at least 5 characters.", textColor: UIColor(0xcf3030)) + text = NSAttributedString(string: "Names must have at least 5 characters.", textColor: UIColor(rgb: 0xcf3030)) case .invalidCharacters: - text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030)) + text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(rgb: 0xcf3030)) } case let .availability(availability): switch availability { case .available: - text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(0x26972c)) + text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(rgb: 0x26972c)) case .invalid: - text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030)) + text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(rgb: 0xcf3030)) case .taken: - text = NSAttributedString(string: "\(addressName) is already taken.", textColor: UIColor(0xcf3030)) + text = NSAttributedString(string: "\(addressName) is already taken.", textColor: UIColor(rgb: 0xcf3030)) } case .checking: - text = NSAttributedString(string: "Checking name...", textColor: UIColor(0x6d6d72)) + text = NSAttributedString(string: "Checking name...", textColor: UIColor(rgb: 0x6d6d72)) displayActivity = true } return ItemListActivityTextItem(displayActivity: displayActivity, text: text, sectionId: self.section) @@ -158,7 +158,7 @@ private struct UsernameSetupControllerState: Equatable { } } -private func usernameSetupControllerEntries(view: PeerView, state: UsernameSetupControllerState) -> [UsernameSetupEntry] { +private func usernameSetupControllerEntries(presentationData: PresentationData, view: PeerView, state: UsernameSetupControllerState) -> [UsernameSetupEntry] { var entries: [UsernameSetupEntry] = [] if let peer = view.peers[view.peerId] as? TelegramUser { @@ -173,11 +173,11 @@ private func usernameSetupControllerEntries(view: PeerView, state: UsernameSetup } } - entries.append(.editablePublicLink(peer.addressName, currentAddressName)) + entries.append(.editablePublicLink(presentationData.theme, peer.addressName, currentAddressName)) if let status = state.addressNameValidationStatus { - entries.append(.publicLinkStatus(currentAddressName, status)) + entries.append(.publicLinkStatus(presentationData.theme, currentAddressName, status)) } - entries.append(.publicLinkInfo("You can shoose a username on Telegram. If you do, other people will be able to find you by this username and contact you without knowing your phone number.\n\nYou can user a-z, 0-9 and underscores. Minimum length is 5 characters.")) + entries.append(.publicLinkInfo(presentationData.theme, presentationData.strings.Username_Help)) } return entries @@ -228,8 +228,8 @@ public func usernameSetupController(account: Account) -> ViewController { let peerView = account.viewTracker.peerView(account.peerId) |> deliverOnMainQueue - let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, peerView) - |> map { state, view -> (ItemListControllerState, (ItemListNodeState, UsernameSetupEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> deliverOnMainQueue, peerView) + |> map { presentationData, state, view -> (ItemListControllerState, (ItemListNodeState, UsernameSetupEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) var rightNavigationButton: ItemListNavigationButton? @@ -245,7 +245,7 @@ public func usernameSetupController(account: Account) -> ViewController { } } - rightNavigationButton = ItemListNavigationButton(title: "Done", style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { + rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Done, style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: { var updatedAddressNameValue: String? updateState { state in if state.editingPublicLinkText != peer.addressName { @@ -278,20 +278,19 @@ public func usernameSetupController(account: Account) -> ViewController { }) } - let leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Cancel, style: .regular, enabled: true, action: { dismissImpl?() }) - let controllerState = ItemListControllerState(title: .text("Username"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, animateChanges: false) - let listState = ItemListNodeState(entries: usernameSetupControllerEntries(view: view, state: state), style: .blocks, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Username_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: usernameSetupControllerEntries(presentationData: presentationData, view: view, state: state), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + let controller = ItemListController(account: account, state: signal) dismissImpl = { [weak controller] in controller?.dismiss() } diff --git a/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift b/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift index f760c78bfc..aff6c15c11 100644 --- a/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift +++ b/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift @@ -76,11 +76,11 @@ final class VerticalListContextResultsChatInputPanelButtonItemNode: ListViewItem self.buttonNode = HighlightTrackingButtonNode() self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = UIColor(0xC9CDD1) + self.topSeparatorNode.backgroundColor = UIColor(rgb: 0xC9CDD1) self.topSeparatorNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xD6D6DA) + self.separatorNode.backgroundColor = UIColor(rgb: 0xD6D6DA) self.separatorNode.isLayerBacked = true self.titleNode = TextNode() @@ -124,7 +124,7 @@ final class VerticalListContextResultsChatInputPanelButtonItemNode: ListViewItem let makeTitleLayout = TextNode.asyncLayout(self.titleNode) return { [weak self] item, width, mergedTop, mergedBottom in - let titleString = NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x007ee5)) + let titleString = NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(rgb: 0x007ee5)) let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .end, CGSize(width: width - 16.0, height: 100.0), .natural, nil, UIEdgeInsets()) diff --git a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift index 05d19c6945..03d588289c 100644 --- a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift @@ -71,7 +71,7 @@ final class VerticalListContextResultsChatInputPanelItem: ListViewItem { private let titleFont = Font.medium(16.0) private let textFont = Font.regular(15.0) private let iconFont = Font.medium(25.0) -private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 2.0, color: UIColor(0xdfdfdf)) +private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 2.0, color: UIColor(rgb: 0xdfdfdf)) final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { static let itemHeight: CGFloat = 75.0 @@ -92,15 +92,15 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { self.textNode = TextNode() self.topSeparatorNode = ASDisplayNode() - self.topSeparatorNode.backgroundColor = UIColor(0xC9CDD1) + self.topSeparatorNode.backgroundColor = UIColor(rgb: 0xC9CDD1) self.topSeparatorNode.isLayerBacked = true self.separatorNode = ASDisplayNode() - self.separatorNode.backgroundColor = UIColor(0xD6D6DA) + self.separatorNode.backgroundColor = UIColor(rgb: 0xD6D6DA) self.separatorNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() - self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) + self.highlightedBackgroundNode.backgroundColor = UIColor(rgb: 0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true self.iconTextBackgroundNode = ASImageNode() @@ -163,7 +163,7 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { } if let text = item.result.description { - textString = NSAttributedString(string: text, font: textFont, textColor: UIColor(0x8e8e93)) + textString = NSAttributedString(string: text, font: textFont, textColor: UIColor(rgb: 0x8e8e93)) } var imageResource: TelegramMediaResource? diff --git a/TelegramUI/VideoOverlayMediaItem.swift b/TelegramUI/VideoOverlayMediaItem.swift new file mode 100644 index 0000000000..504084e018 --- /dev/null +++ b/TelegramUI/VideoOverlayMediaItem.swift @@ -0,0 +1,27 @@ +import Foundation +import Display +import AsyncDisplayKit + +final class VideoOverlayMediaItem: OverlayMediaItem { + fileprivate weak var player: MediaPlayer? + + init(player: MediaPlayer) { + self.player = player + } + + func node() -> OverlayMediaItemNode { + return VideoOverlayMediaItemNode(item: self) + } +} + +final class VideoOverlayMediaItemNode: OverlayMediaItemNode { + private let item: VideoOverlayMediaItem + + init(item: VideoOverlayMediaItem) { + self.item = item + + super.init() + + self.backgroundColor = .green + } +} diff --git a/TelegramUI/VoiceCallDataSavingController.swift b/TelegramUI/VoiceCallDataSavingController.swift index 3c7955b149..6d62defa61 100644 --- a/TelegramUI/VoiceCallDataSavingController.swift +++ b/TelegramUI/VoiceCallDataSavingController.swift @@ -17,10 +17,10 @@ private enum VoiceCallDataSavingSection: Int32 { } private enum VoiceCallDataSavingEntry: ItemListNodeEntry { - case never(String, Bool) - case cellular(String, Bool) - case always(String, Bool) - case info(String) + case never(PresentationTheme, String, Bool) + case cellular(PresentationTheme, String, Bool) + case always(PresentationTheme, String, Bool) + case info(PresentationTheme, String) var section: ItemListSectionId { return VoiceCallDataSavingSection.dataSaving.rawValue @@ -41,26 +41,26 @@ private enum VoiceCallDataSavingEntry: ItemListNodeEntry { static func ==(lhs: VoiceCallDataSavingEntry, rhs: VoiceCallDataSavingEntry) -> Bool { switch lhs { - case let .never(text, value): - if case .never(text, value) = rhs { + case let .never(lhsTheme, lhsText, lhsValue): + if case let .never(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .cellular(text, value): - if case .cellular(text, value) = rhs { + case let .cellular(lhsTheme, lhsText, lhsValue): + if case let .cellular(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .always(text, value): - if case .always(text, value) = rhs { + case let .always(lhsTheme, lhsText, lhsValue): + if case let .always(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } - case let .info(text): - if case .info(text) = rhs { + case let .info(lhsTheme, lhsText): + if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false @@ -74,20 +74,20 @@ private enum VoiceCallDataSavingEntry: ItemListNodeEntry { func item(_ arguments: VoiceCallDataSavingControllerArguments) -> ListViewItem { switch self { - case let .never(text, value): - return ItemListCheckboxItem(title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + case let .never(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateSelection(.never) }) - case let .cellular(text, value): - return ItemListCheckboxItem(title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + case let .cellular(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateSelection(.cellular) }) - case let .always(text, value): - return ItemListCheckboxItem(title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { + case let .always(theme, text, value): + return ItemListCheckboxItem(theme: theme, title: text, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateSelection(.always) }) - case let .info(text): - return ItemListTextItem(text: .plain(text), sectionId: self.section) + case let .info(theme, text): + return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) } } } @@ -103,13 +103,13 @@ private func stringForDataSavingOption(_ option: VoiceCallDataSaving) -> String } } -private func voiceCallDataSavingControllerEntries(settings: VoiceCallSettings) -> [VoiceCallDataSavingEntry] { +private func voiceCallDataSavingControllerEntries(presentationData: PresentationData, settings: VoiceCallSettings) -> [VoiceCallDataSavingEntry] { var entries: [VoiceCallDataSavingEntry] = [] - entries.append(.never(stringForDataSavingOption(.never), settings.dataSaving == .never)) - entries.append(.cellular(stringForDataSavingOption(.cellular), settings.dataSaving == .cellular)) - entries.append(.always(stringForDataSavingOption(.always), settings.dataSaving == .always)) - entries.append(.info("Using less data may improve your experience on bad networks, but will slightly decrease audio quality.")) + entries.append(.never(presentationData.theme, stringForDataSavingOption(.never), settings.dataSaving == .never)) + entries.append(.cellular(presentationData.theme, stringForDataSavingOption(.cellular), settings.dataSaving == .cellular)) + entries.append(.always(presentationData.theme, stringForDataSavingOption(.always), settings.dataSaving == .always)) + entries.append(.info(presentationData.theme, "Using less data may improve your experience on bad networks, but will slightly decrease audio quality.")) return entries } @@ -134,17 +134,15 @@ func voiceCallDataSavingController(account: Account) -> ViewController { }).start() }) - let signal = voiceCallSettingsPromise.get() |> deliverOnMainQueue - |> map { data -> (ItemListControllerState, (ItemListNodeState, VoiceCallDataSavingEntry.ItemGenerationArguments)) in + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, voiceCallSettingsPromise.get()) |> deliverOnMainQueue + |> map { presentationData, data -> (ItemListControllerState, (ItemListNodeState, VoiceCallDataSavingEntry.ItemGenerationArguments)) in - let controllerState = ItemListControllerState(title: .text("Use Less Data"), leftNavigationButton: nil, rightNavigationButton: nil, animateChanges: false) - let listState = ItemListNodeState(entries: voiceCallDataSavingControllerEntries(settings: data), style: .blocks, emptyStateItem: nil, animateChanges: false) + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Use Less Data"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let listState = ItemListNodeState(entries: voiceCallDataSavingControllerEntries(presentationData: presentationData, settings: data), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } - let controller = ItemListController(signal) - controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) - + let controller = ItemListController(account: account, state: signal) return controller } diff --git a/TelegramUI/VoiceCallSettings.swift b/TelegramUI/VoiceCallSettings.swift index 3e57b02724..6733407f59 100644 --- a/TelegramUI/VoiceCallSettings.swift +++ b/TelegramUI/VoiceCallSettings.swift @@ -20,7 +20,7 @@ public struct VoiceCallSettings: PreferencesEntry, Equatable { } public init(decoder: Decoder) { - self.dataSaving = VoiceCallDataSaving(rawValue: (decoder.decodeInt32ForKey("ds") as Int32))! + self.dataSaving = VoiceCallDataSaving(rawValue: decoder.decodeInt32ForKey("ds", orElse: 0))! } public func encode(_ encoder: Encoder) { diff --git a/TelegramUI/Wallpapers.swift b/TelegramUI/Wallpapers.swift new file mode 100644 index 0000000000..5edebc8e06 --- /dev/null +++ b/TelegramUI/Wallpapers.swift @@ -0,0 +1,105 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit + +public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { + case builtin + case color(Int32) + case image([TelegramMediaImageRepresentation]) + + public init(decoder: Decoder) { + switch decoder.decodeInt32ForKey("v", orElse: 0) { + case 0: + self = .builtin + case 1: + self = .color(decoder.decodeInt32ForKey("c", orElse: 0)) + case 2: + self = .image(decoder.decodeObjectArrayWithDecoderForKey("i")) + default: + assertionFailure() + self = .builtin + } + } + + public func encode(_ encoder: Encoder) { + switch self { + case .builtin: + encoder.encodeInt32(0, forKey: "v") + case let .color(color): + encoder.encodeInt32(1, forKey: "v") + encoder.encodeInt32(color, forKey: "c") + case let .image(representations): + encoder.encodeInt32(2, forKey: "v") + encoder.encodeObjectArray(representations, forKey: "i") + } + } + + public static func ==(lhs: TelegramWallpaper, rhs: TelegramWallpaper) -> Bool { + switch lhs { + case .builtin: + if case .builtin = rhs { + return true + } else { + return false + } + case let .color(color): + if case .color(color) = rhs { + return true + } else { + return false + } + case let .image(lhsRepresentations): + if case let .image(rhsRepresentations) = rhs, lhsRepresentations == rhsRepresentations { + return true + } else { + return false + } + } + } +} + +func telegramWallpapers(account: Account) -> Signal<[TelegramWallpaper], NoError> { + return account.postbox.modify { modifier -> [TelegramWallpaper] in + let items = modifier.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudWallpapers) + if items.count == 0 { + return [.builtin, .color(0x121212)] + } else { + return items.map { $0.contents as! TelegramWallpaper } + } + } |> mapToSignal { list -> Signal<[TelegramWallpaper], NoError> in + let remote = account.network.request(Api.functions.account.getWallPapers()) + |> retryRequest + |> mapToSignal { result -> Signal<[TelegramWallpaper], NoError> in + var items: [TelegramWallpaper] = [] + for item in result { + switch item { + case let .wallPaper(_, _, sizes, color): + items.append(.image(telegramMediaImageRepresentationsFromApiSizes(sizes))) + case let .wallPaperSolid(_, _, bgColor, color): + items.append(.color(bgColor)) + } + } + items.removeFirst() + items.insert(.builtin, at: 0) + items.insert(.color(0x121212), at: 1) + + if items == list { + return .complete() + } else { + return account.postbox.modify { modifier -> [TelegramWallpaper] in + var entries: [OrderedItemListEntry] = [] + for item in items { + var intValue = Int32(entries.count) + let id = MemoryBuffer(data: Data(bytes: &intValue, count: 4)) + entries.append(OrderedItemListEntry(id: id, contents: item)) + } + modifier.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudWallpapers, items: entries) + + return items + } + } + } + return .single(list) |> then(remote) + } +} diff --git a/TelegramUI/WebpagePreviewAccessoryPanelNode.swift b/TelegramUI/WebpagePreviewAccessoryPanelNode.swift index 611dcac446..315eddf3ea 100644 --- a/TelegramUI/WebpagePreviewAccessoryPanelNode.swift +++ b/TelegramUI/WebpagePreviewAccessoryPanelNode.swift @@ -5,20 +5,6 @@ import Postbox import SwiftSignalKit import Display -private let lineImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(0x007ee5)) -private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor(0x9099A2).cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - context.move(to: CGPoint(x: 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) - context.strokePath() - context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) - context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) - context.strokePath() -}) - final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { private let webpageDisposable = MetaDisposable() @@ -29,18 +15,23 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { let titleNode: ASTextNode let textNode: ASTextNode - init(account: Account, webpage: TelegramMediaWebpage) { + var theme: PresentationTheme + var strings: PresentationStrings + + init(account: Account, webpage: TelegramMediaWebpage, theme: PresentationTheme, strings: PresentationStrings) { self.webpage = webpage + self.theme = theme + self.strings = strings self.closeButton = ASButtonNode() - self.closeButton.setImage(closeButtonImage, for: []) + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.closeButton.displaysAsynchronously = false self.lineNode = ASImageNode() self.lineNode.displayWithoutProcessing = true self.lineNode.displaysAsynchronously = false - self.lineNode.image = lineImage + self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) self.titleNode = ASTextNode() self.titleNode.truncationMode = .byTruncatingTail @@ -68,6 +59,31 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { self.webpageDisposable.dispose() } + override func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + if self.theme !== theme || self.strings !== strings { + self.strings = strings + + if self.theme !== theme { + self.theme = theme + + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) + self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(theme) + } + + if let text = self.titleNode.attributedText?.string { + self.titleNode.attributedText = NSAttributedString(string: text, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + } + + if let text = self.textNode.attributedText?.string { + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) + } + + self.updateWebpage() + + self.setNeedsLayout() + } + } + func replaceWebpage(_ webpage: TelegramMediaWebpage) { if !self.webpage.isEqual(webpage) { self.webpage = webpage @@ -80,7 +96,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { var text = "" switch self.webpage.content { case .Pending: - authorName = "Loading..." + authorName = self.strings.Channel_NotificationLoading case let .Loaded(content): if let title = content.title { authorName = title @@ -92,8 +108,8 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { text = content.text ?? "" } - self.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: UIColor(0x007ee5)) - self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: UIColor.black) + self.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: self.theme.chat.inputPanel.primaryTextColor) self.setNeedsLayout() } diff --git a/submodules/libtgvoip b/submodules/libtgvoip new file mode 160000 index 0000000000..c7047d0efe --- /dev/null +++ b/submodules/libtgvoip @@ -0,0 +1 @@ +Subproject commit c7047d0efe43fb87fd97cef11b7e48501db10702