From c8b190cd37bb34184e8efeb474b8089fb9a7f51d Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 1 Feb 2024 20:58:34 +0400 Subject: [PATCH 01/50] boosts --- Telegram-Mac/AccountContext.swift | 43 ++++- Telegram-Mac/Appearance.swift | 3 + .../Contents.json | 22 +++ .../customemoji_24.png | Bin 0 -> 527 bytes .../customemoji_24@2x.png | Bin 0 -> 924 bytes .../Contents.json | 22 +++ .../voicetotext_24.png | Bin 0 -> 408 bytes .../voicetotext_24@2x.png | Bin 0 -> 749 bytes .../Contents.json | 22 +++ .../lastseen_subtract_24.png | Bin 0 -> 415 bytes .../lastseen_subtract_24@2x.png | Bin 0 -> 773 bytes .../Contents.json | 22 +++ .../messageprivacy_subtract_24.png | Bin 0 -> 438 bytes .../messageprivacy_subtract_24@2x.png | Bin 0 -> 800 bytes .../Icon_Profile_Boost.imageset/Contents.json | 22 +++ .../ic_pf_boost.png | Bin 0 -> 865 bytes .../ic_pf_boost@2x.png | Bin 0 -> 1566 bytes .../menu_boost.imageset/Contents.json | 22 +++ .../menu_boost.imageset/menu_boost (1).png | Bin 0 -> 304 bytes .../menu_boost.imageset/menu_boost@2x (1).png | Bin 0 -> 553 bytes .../BoostChannelModalController.swift | 92 +++++++--- Telegram-Mac/BoostFeatureRowItem.swift | 13 +- .../ChannelBoostStatsController.swift | 24 ++- .../ChannelPermissionsController.swift | 90 +++++++++- Telegram-Mac/ChatController.swift | 2 +- Telegram-Mac/ChatInterfaceInteraction.swift | 2 +- .../ChatPresentationInterfaceState.swift | 8 + Telegram-Mac/ChatReactionsView.swift | 12 +- Telegram-Mac/ChatRowItem.swift | 2 +- .../EmptyGroupstickerSearchRowItem.swift | 14 +- Telegram-Mac/FastSettings.swift | 7 +- Telegram-Mac/GroupEmojiPackController.swift | 134 ++++++++++++++- Telegram-Mac/GroupInfoEntries.swift | 20 +++ Telegram-Mac/ImageUtils.swift | 30 ++-- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/LottieLocalAnimations.swift | 3 +- Telegram-Mac/MessagesPrivacyController.swift | 4 +- Telegram-Mac/PeerInfoHeadItem.swift | 12 +- Telegram-Mac/PremiumBoardingController.swift | 26 ++- .../PremiumBoardingFeaturesController.swift | 20 +++ Telegram-Mac/PremiumLimitConfig.swift | 9 + .../PremiumShowStatusController.swift | 2 +- Telegram-Mac/ReactionsWindowController.swift | 2 +- .../SelectivePrivacySettingsController.swift | 2 +- Telegram-Mac/en.lproj/Localizable.strings | Bin 813512 -> 816536 bytes Telegram-Mac/tgs/menu/menu_boost.tgs | Bin 0 -> 724 bytes Telegram.xcodeproj/project.pbxproj | 4 + TelegramShare/Info.plist | 2 +- .../Sources/ColorPalette/ColorPalette.swift | 147 ---------------- .../Sources/ColorPalette/PeerNamesColor.swift | 160 ++++++++++++++++++ .../Sources/Localization/Localizable.swift | 40 ++++- .../Sources/TelegramIconsTheme.swift | 48 ++++++ submodules/telegram-ios | 2 +- tools/generate-images.swift | 5 +- 54 files changed, 871 insertions(+), 247 deletions(-) create mode 100644 Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_EmojiPack.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_EmojiPack.imageset/customemoji_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_EmojiPack.imageset/customemoji_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_VoiceToText.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_VoiceToText.imageset/voicetotext_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_VoiceToText.imageset/voicetotext_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_LastSeen.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_LastSeen.imageset/lastseen_subtract_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_LastSeen.imageset/lastseen_subtract_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_MessagePrivacy.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_MessagePrivacy.imageset/messageprivacy_subtract_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_MessagePrivacy.imageset/messageprivacy_subtract_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Profile_Boost.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Profile_Boost.imageset/ic_pf_boost.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Profile_Boost.imageset/ic_pf_boost@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/menu_boost.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/menu_boost.imageset/menu_boost (1).png create mode 100644 Telegram-Mac/Assets.xcassets/menu_boost.imageset/menu_boost@2x (1).png create mode 100644 Telegram-Mac/tgs/menu/menu_boost.tgs create mode 100644 packages/ColorPalette/Sources/ColorPalette/PeerNamesColor.swift diff --git a/Telegram-Mac/AccountContext.swift b/Telegram-Mac/AccountContext.swift index 11f405cb8..e68fe48b1 100644 --- a/Telegram-Mac/AccountContext.swift +++ b/Telegram-Mac/AccountContext.swift @@ -22,7 +22,7 @@ import InAppPurchaseManager import ApiCredentials let clown: String = "🤡" -let tagsGloballyEnabled = true + public struct PremiumConfiguration { @@ -41,7 +41,13 @@ public struct PremiumConfiguration { minChannelProfileIconLevel: 7, minChannelEmojiStatusLevel: 8, minChannelWallpaperLevel: 9, - minChannelCustomWallpaperLevel: 10 + minChannelCustomWallpaperLevel: 10, + minGroupProfileIconLevel: 7, + minGroupEmojiStatusLevel: 8, + minGroupWallpaperLevel: 9, + minGroupCustomWallpaperLevel: 9, + minGroupEmojiPackLevel: 9, + minGroupAudioTranscriptionLevel: 9 ) } @@ -60,6 +66,13 @@ public struct PremiumConfiguration { public let minChannelWallpaperLevel: Int32 public let minChannelCustomWallpaperLevel: Int32 + public let minGroupProfileIconLevel: Int32 + public let minGroupEmojiStatusLevel: Int32 + public let minGroupWallpaperLevel: Int32 + public let minGroupCustomWallpaperLevel: Int32 + public let minGroupEmojiPackLevel: Int32 + public let minGroupAudioTranscriptionLevel: Int32 + fileprivate init( isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, @@ -74,8 +87,13 @@ public struct PremiumConfiguration { minChannelProfileIconLevel: Int32, minChannelEmojiStatusLevel: Int32, minChannelWallpaperLevel: Int32, - minChannelCustomWallpaperLevel: Int32 - + minChannelCustomWallpaperLevel: Int32, + minGroupProfileIconLevel: Int32, + minGroupEmojiStatusLevel: Int32, + minGroupWallpaperLevel: Int32, + minGroupCustomWallpaperLevel: Int32, + minGroupEmojiPackLevel: Int32, + minGroupAudioTranscriptionLevel: Int32 ) { self.isPremiumDisabled = isPremiumDisabled self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu @@ -91,6 +109,12 @@ public struct PremiumConfiguration { self.minChannelEmojiStatusLevel = minChannelEmojiStatusLevel self.minChannelWallpaperLevel = minChannelWallpaperLevel self.minChannelCustomWallpaperLevel = minChannelCustomWallpaperLevel + self.minGroupProfileIconLevel = minGroupProfileIconLevel + self.minGroupEmojiStatusLevel = minGroupEmojiStatusLevel + self.minGroupWallpaperLevel = minGroupWallpaperLevel + self.minGroupCustomWallpaperLevel = minGroupCustomWallpaperLevel + self.minGroupEmojiPackLevel = minGroupEmojiPackLevel + self.minGroupAudioTranscriptionLevel = minGroupAudioTranscriptionLevel } public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration { @@ -113,7 +137,13 @@ public struct PremiumConfiguration { minChannelProfileIconLevel: get(data["channel_profile_bg_icon_level_min"]) ?? defaultValue.minChannelProfileIconLevel, minChannelEmojiStatusLevel: get(data["channel_emoji_status_level_min"]) ?? defaultValue.minChannelEmojiStatusLevel, minChannelWallpaperLevel: get(data["channel_wallpaper_level_min"]) ?? defaultValue.minChannelWallpaperLevel, - minChannelCustomWallpaperLevel: get(data["channel_custom_wallpaper_level_min"]) ?? defaultValue.minChannelCustomWallpaperLevel + minChannelCustomWallpaperLevel: get(data["channel_custom_wallpaper_level_min"]) ?? defaultValue.minChannelCustomWallpaperLevel, + minGroupProfileIconLevel: get(data["group_profile_bg_icon_level_min "]) ?? defaultValue.minGroupProfileIconLevel, + minGroupEmojiStatusLevel: get(data["group_emoji_status_level_min"]) ?? defaultValue.minGroupEmojiStatusLevel, + minGroupWallpaperLevel: get(data["group_wallpaper_level_min"]) ?? defaultValue.minGroupWallpaperLevel, + minGroupCustomWallpaperLevel: get(data["group_custom_wallpaper_level_min"]) ?? defaultValue.minGroupCustomWallpaperLevel, + minGroupEmojiPackLevel: get(data["group_emoji_stickers_level_min"]) ?? defaultValue.minGroupEmojiPackLevel, + minGroupAudioTranscriptionLevel: get(data["group_transcribe_level_min"]) ?? defaultValue.minGroupAudioTranscriptionLevel ) } else { return defaultValue @@ -124,6 +154,7 @@ public struct PremiumConfiguration { + extension AppConfiguration { func getGeneralValue(_ key: String, orElse defaultValue: Int32) -> Int32 { if let value = self.data?[key] as? Double { @@ -462,7 +493,7 @@ final class AccountContext { if let _peerNameColors = _peerNameColors { return _peerNameColors } - return .init(colors: [:], darkColors: [:], displayOrder: [], profileColors: [:], profileDarkColors: [:], profilePaletteColors: [:], profilePaletteDarkColors: [:], profileStoryColors: [:], profileStoryDarkColors: [:], profileDisplayOrder: [], nameColorsChannelMinRequiredBoostLevel: [:]) + return .init(colors: [:], darkColors: [:], displayOrder: [], profileColors: [:], profileDarkColors: [:], profilePaletteColors: [:], profilePaletteDarkColors: [:], profileStoryColors: [:], profileStoryDarkColors: [:], profileDisplayOrder: [], nameColorsChannelMinRequiredBoostLevel: [:], nameColorsGroupMinRequiredBoostLevel: [:]) } diff --git a/Telegram-Mac/Appearance.swift b/Telegram-Mac/Appearance.swift index a477bb1b6..e778c9b81 100644 --- a/Telegram-Mac/Appearance.swift +++ b/Telegram-Mac/Appearance.swift @@ -2610,6 +2610,7 @@ private func generateIcons(from palette: ColorPalette, bubbled: Bool) -> Telegra profile_unblock: { generateProfileIcon(NSImage(named: "Icon_Profile_Unblock")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, profile_translate: { generateProfileIcon(NSImage(named: "Icon_Profile_Translate")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, profile_join_channel: { generateProfileIcon(NSImage(named: "Icon_Profile_JoinChannel")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_boost: { generateProfileIcon(NSImage(named: "Icon_Profile_Boost")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, chat_quiz_explanation: { NSImage(named: "Icon_QuizExplanation")!.precomposed(palette.accentIcon) }, chat_quiz_explanation_bubble_incoming: { NSImage(named: "Icon_QuizExplanation")!.precomposed(palette.accentIconBubble_incoming) }, chat_quiz_explanation_bubble_outgoing: { NSImage(named: "Icon_QuizExplanation")!.precomposed(palette.accentIconBubble_outgoing) }, @@ -2888,6 +2889,8 @@ private func generateIcons(from palette: ColorPalette, bubbled: Bool) -> Telegra channel_feature_reaction: { NSImage(named: "Icon_ChannelFeature_Reaction")!.precomposed(palette.accent) }, channel_feature_status: { NSImage(named: "Icon_ChannelFeature_Status")!.precomposed(palette.accent) }, channel_feature_stories: { NSImage(named: "Icon_ChannelFeature_Stories")!.precomposed(palette.accent) }, + channel_feature_emoji_pack: { NSImage(named: "Icon_ChannelFeature_EmojiPack")!.precomposed(palette.accent) }, + channel_feature_voice_to_text: { NSImage(named: "Icon_ChannelFeature_VoiceToText")!.precomposed(palette.accent) }, chat_hidden_author: { NSImage(named: "Icon_AuthorHidden")!.precomposed(.white) }, chat_my_notes: { NSImage(named: "Icon_MyNotes")!.precomposed(.white) }, premium_required_forward: { NSImage(named: "Icon_PremiumRequired_Forward")!.precomposed() } diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_EmojiPack.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_EmojiPack.imageset/Contents.json new file mode 100644 index 000000000..f08f936e1 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_EmojiPack.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "customemoji_24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "customemoji_24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_EmojiPack.imageset/customemoji_24.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_EmojiPack.imageset/customemoji_24.png new file mode 100644 index 0000000000000000000000000000000000000000..429e5d435da583789db9ca544fe5c9fc7ca58f4f GIT binary patch literal 527 zcmV+q0`UEbP)73MX*cCM|l^JPN164kD1!`GN}*#KCS8#0Y!X9tIH!W&FlwUWe|$q9uc6z`53B zShK6P5I)qj`n!h)WWqtfz9cixmU?zD^TM+g2-HK1dKUF6xTx+laomvoGFbp^!!gWjKJnFIYgXCC$wk_up z&z842Z@LHCA(z5qF4jw!!JWO1wXj;FFX1`Y{6frMrtvaAp>0lw+Uq5D8sGZ6ZOVGd RJ4*ln002ovPDHLkV1jcW;R^r& literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_EmojiPack.imageset/customemoji_24@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelFeature_EmojiPack.imageset/customemoji_24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e40bbadff6e8e95767053b81a569c980ec24560b GIT binary patch literal 924 zcmV;N17rM&P)IC)%ZYQuifxSW83EWQL3P<8ZQ4~c5homCT%bf2M!9T$Li0E$7-cMZQn6W~858Bu^!hlYtEFgcA+YT7O*=t|I zq)pdJc8`Rh76?ZtwNo(%OSGdJf;`<412#2<{z&@H_RcdgN8Gk{bO~`dh$-u^!87`E z`C6=D)`QRr&ST&qQM!elmtR4DIoISXe86qvZej#eR1U7fBZk4Vqmci7fFWzUYYn|1 z@q>VDAp1a@Mze-XZB`hD1Y&M&xHUiYo+0~^@*5sHjI%)=?UUE05U)=1+2mNsZ?Em6 zwao#dh>b6m?7Vv?`|J}EXQ68&Bh-=72uCng+!@r)eLsaHBZB3$YZ3p1$vK`*chMZJ zko~(uz0f4G3U##MJ^R<%J9u`LqvJ3gT zXW`f*uT3M8K7B0FeWWPS{wPIAr$GI}K9`&_j=tz3tdn&w<P0T2?LSMOO5m~33428N9GY-W!sllH2M!}lJlYBkPYrYsDzG1pnzZGdFSU9Mlkh9K5aWg&ONT>vLCp0UFv_~>3)Zae9|fNQYe@OoKjL* yr$nZ2ty5HK~#7F)s(?e z!!Qs<|AYqA0b7EP98-czPzR&~)B$whbO0Ts1WIr^2uE(P8orS1krK&H4tXz;cpg-Z`2ElU{YAs+lj| zaV+RZ^}%=Vsu!AWQGqFBBh)@gKo~pndO!uj2_+oj*9}yfn88YeF1hb9E#>@qkwIzm z|Jh{53@V%p$(R}g|JiS7XTY>%MT0NZBkzyQDz{mJ%cnZayaV>C*G!!6_aS+%>Y9M> zVV4+=T+DZd<$HgagfSLVvx{`hNRly&zmw`0+975usOY8~yNZe7x8bl5i&&m5;>PwI zJ27Szf>`(Prwn^ky{WZ0G?rt78*_7mC0xDktM~;a|3jrfr%K@f0000+x$=>~2mNH(xCLA*gio`@5rP!uGQR&vq1 z`zMc@=OqB)BNGCJ$hEc3T^-jUT*>Ou0)|kMYE+k9}#%B}1SF!Zo%Q=%ROw4QN6LKxl4dx7C=fh1+S$UR14o)BJmR^B~L;O)`9 z3nS>$^TFJrJ9}g_Pah~y$Clk8p3`e~ccWp{qeFrF_7y8>u(;I0=^o7!i_q(saD~%{ zBjWx}XzsFn$x#b+w3XS(GWvqn-{q~ebz;QVidbAEhf$BJYPIZPj|_X=KDOu&+CJ=z z_(8qJmGOi2>^eY-cDCbo)WsFMf?CDY(`iDLIr<&2+rW=0W%)6PVKPYkF^kbj0;Lo$ub~Xn4EG-&!f2NhWG3AxNci?{@*7m{oLyLpO{muzR zvRaAqDszQ5$0Ya`=_SPld-A<@&-q!u=R?H#uPX2Ua=WaT>xn<4{;>Ch-w*G{VzPbT zS_FUD$p%|jyt!HP-E)I%cF~S$4bwDM7pyqGO?5-C(9Q!awl&@l7Eek!GE2U-M)_JO zYn70{#%+oB&fX5%e-%!AjVTWYokcnK`Tpvkd=$~^q);N1SsiIHGe}g; z(pKN$N#49B)dCN{!@?)lzdf1w@kxIjU;ESjy2>|ZKVVAT(3yR{Yw|TD z+%OP*$D{z$fl|R;Du~hnCAb4f1$GA^6_^e}Do{E&Qh`YYTn9vp<-ubl?dl{r-J5x5 zhLhI&R;&MQ5JzL#T5c_sBNorncw9UmEdN^mS^QrNWoay*E8&2Ipj}%&0Vj8QP4el6 zbH1yP0Yk;khG@v~jZ-q>JaKtpKQzu4EeY76P8={z zW`{_bEeY77F5Fvwv%IwY;+{VPWfgr;{@2hc$q$V%_JKOZ1dN$=72?n#0l7}oq|&&T zD9ipGoQHddZ>~Dp`CGg?zQ%2W9Su45r3CcMi$c*_b3hsl3JBE~vnxO{l9-W+|Uk*f=MBXaWx1BaW1nYqo%8 zO8s(Tb}pKs+;71nM?{u-2_@^N_E`I$Aj5Y|rvu9Jt8=Om);%!aD@4vaC6MI358Cn$ zd`D~%cb==ykuA_84u$xb9ScDAd2GsBD1=AEZ%{O*4nKExS{@ijX&Oc`BCc{^>Nm)Y zVg^S^3vi>z6;2FlN=~Vc)E{wt1aoPq1^$5tO;Wym?eKm#mJ-k{=F7rLly8{QMd7t$ zKA)H^U2x2YQ0Iux^B)GMk7g=|r4ofRzvLrG`$7#2?x*{ToD8uC1K)P9mb8(aWV}FO zT4~neyg^{T*??!^Ox@`S?t}VqB00000NkvXXu0mjf D6ZJ>N literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_MessagePrivacy.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_MessagePrivacy.imageset/Contents.json new file mode 100644 index 000000000..dd86c865b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_MessagePrivacy.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "messageprivacy_subtract_24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "messageprivacy_subtract_24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_MessagePrivacy.imageset/messageprivacy_subtract_24.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_MessagePrivacy.imageset/messageprivacy_subtract_24.png new file mode 100644 index 0000000000000000000000000000000000000000..1f9b7e94e0c677ad1ffdbf709b1ab41ff022815b GIT binary patch literal 438 zcmV;n0ZIOeP) zy^WM$iIMxDP9`;uz#_4$Et@OAY^tYCv6dmiuWya%7gCgE`e7(7YSW&VS3;Y+_q g!0DeY3nY#I1$|yW?L`8{Jpcdz07*qoM6N<$f(0S4X8-^I literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_MessagePrivacy.imageset/messageprivacy_subtract_24@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Boarding_MessagePrivacy.imageset/messageprivacy_subtract_24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2a4b4719c4d407da1eb33eafcf8243a85896776c GIT binary patch literal 800 zcmV+*1K<3KP)Vq!Clzq1AV~$#f!)DH8^naepj}Cv;oi(U z7_+S2N{bfB?BGXcnOk02k|PGkrO^(Kzb*e*9tM9`gDh*y<3?B^pwsRwpMcdxkI9>OW=1Yw(L<4Y$R22gES>>O(rzH^Z@xJQ?Y^jj*DsHk5Ln2138$ z<#bsN0+XiF`v6@kqhFAbJKM@*Q#+dyGHRtZm5~W((3c{8ihYL+qtW(8;i!dNoQ8N- zTzd-{2b@;(NoWH2f%Twjdx`X6XmeKkOaZzy5)DYvPyxO8o`u3MDg4L=0&;|l4;kIF z2T&tS^pLf}e{uj)(K#mYN${OXJ~Xos;e3!fM|=s?1Yocp775@6>p|7FCRcq!ow6O{ zdj?43Lo))5!gEb5_a6L2T$?A*_LiBs-vsy=lV*HUfDg4{<7qID5I?uR$Pln9+qn}h zI9)zr`i+y&z30F8yZE=~&kLS*<~pz7q*dDqLVM0P5-H%Ge~i!*mAeg=-C-ld?+-=a zXpj!>gaYwXakIxdFqR+DJ;+HT$pN$o&*>J^%S_(yH0U1G)-fCK%3UWcgO-0D5I%HM zv&5GOKdI!|ENBmgS&oDobW6nJ3e33<@X$){yTQ6|s~`H48te363aeox zgbV*CA0zZxSd+8+wB?3G&as35j_zVVHRwTzs}JX5jhW)NgB*xKx47;Yx4%)u=N70000S{x#J|Y^oz!^e}MO1fCJ%UW^KfVyDE4YBbye`qh1g~YEh>Z;_b_fx*5I17J z@233(NAkNft5q|YVj>3Bm&>4C`isc*a2zbr!h3T0s(8UGy?u>8MaqgQZM+60W z9?ic1Hi*f!MM!6{w8M)efGIfY-8$>(btgREkKepfhN=w8)yx?#oCWYY;KF9BMEK%R`E4lK<$b&^*sYZ%zWsVo5B!KSAz%wF^9K9K zF%An>p0vE|%}W_&n9($9XNyEtXjVl3M8Q~)2oS2WA2b=7SZt=UR!O0 r4m~9R6VCq)!6cT{zf%j&@NbA06n^`h4c8SG00000NkvXXu0mjfs@{Rj literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Boost.imageset/ic_pf_boost@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Boost.imageset/ic_pf_boost@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ab615937fd91b4bb1373e3333ec3814d24ccd882 GIT binary patch literal 1566 zcmV+(2I2XMP)XBX6XbXCx~Xt~Qw-HT0-@lOxnOTefJ0PNs+)1yDGPgm=aa9y;G z!q|%WQC*>B+7pmR+k+MDcQ2ylqvCcjCd@j*rkt8SiFdn2;xUKhW^SJ?W+Up4 zK=yTyWUhvM-8Q3ejt}Y>HK`{co9qMrp+^VU4W?w$M|DiPx`0l+8^{e5b<~2smLD|oA3iZzldk)+C0C2d;y*5 z9^wTD=lKPBbI}Mab)Es>Whg~Xr*_`DA@B2|^0I&fBXK>C7ej~k(Fg0$HT=UO6L3B{T>9xOcvjT4W!h)v0vh}RNz80Azg!@XYzISFX7f?#1Z2l( zLJy*u9B5syF(e`vHpxI);yqKkj=cyV4JJ*Kx!7yd?B zl+EHg$G#v8(+)Xx?J*l+6T2ioSj>r;Q0a2LAmjF=+#nPb5IGmVgCwd<>K_B|Niooz z%1KL))_XnNY7u^iz+GfbSP};RmHv)v00>`#jWCI5A~S(3%12;m(@p4L8au`c1b334JoX0DS@($0w#Rt1dIfk@tGE6}(<6}mpb;?&^XH^4>JA}?ang1&UyX^VHW8lX5g0n#$ivKI##}(oK|`ByUIdmh zWtpSXhgmGH2b#d6FdKnoWE)u)>eq_`zQTFXQJ9XvQnHOjxB+02{z0Zk+6I$Qqfjq= zC(m@5zq709%S-w^3PW|l|7o1L@{~nZzao=c2+QB<^|D7UkKgh-D2n&WotT>~JBg!B zu1LJ2yeEpQa!;hVjO-*Vt_)kaJE~Yf=Pb)x&UPK%?z*B3ySgsywpB^UP(o4KIaXd+ z=k6&bY}wxrQxdOq5??ML&kdiz4pd`*;0g25I={I7u_u2}W)SVku_xTS^Qp;WPZD-t z#7g2Z)dXaASH#NVG1a@X_yT;P-3lzzI^r?aM_`(P;c1SyZqSyvurrOCC;x6HAj5gH z+QJh)T!t~)wB2rV0U75P`Gg~{-J!MNGp9LKZJ^P!837Ys4xw#`ff>^MuaF|7J4XJ( zu5~ti%6U5iGR~}f2st8K6mEVAAMkk$Pvjq!9hL3kM4Q+$ZPjkqT9@+)*LZn%yc6>+ zn*1;V?(cD*?y!R&Mo@$k^uX>IRd$0+4A%x6=S+*Vy?3QgzWr+-dloFqH+OFMqqvJ& Qga7~l07*qoM6N<$g1Qsh!~g&Q literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/menu_boost.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/menu_boost.imageset/Contents.json new file mode 100644 index 000000000..3d328566b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/menu_boost.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "menu_boost (1).png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "menu_boost@2x (1).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/menu_boost.imageset/menu_boost (1).png b/Telegram-Mac/Assets.xcassets/menu_boost.imageset/menu_boost (1).png new file mode 100644 index 0000000000000000000000000000000000000000..31148ee92cb87acb74f2d48da97b38ae4955c013 GIT binary patch literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|&H|6fVg?3oVGw3ym^DWND9BhG zFbAx$cY3 zy%n@yPQT#$BcxN^yPw}?yD8_mOdLj`J&wLiNR6yvQO#PK=7i$91XaGmB1Tcx(X ve+Q??8MGA3ozczt`&a(jaRZ)wORo5+<3Gw~<@|L9dWgZ()z4*}Q$iB}s-0>d literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/menu_boost.imageset/menu_boost@2x (1).png b/Telegram-Mac/Assets.xcassets/menu_boost.imageset/menu_boost@2x (1).png new file mode 100644 index 0000000000000000000000000000000000000000..f94c00604fe35a371f2da9b49f7f619e3456f022 GIT binary patch literal 553 zcmV+^0@nSBP)9pSUhGM;edrBXh>Vn?z>m6ls5H>DM16Rn_s% zLZy9-!?k%bx&e1Sb!8*VS3h@sCC;CD;Lh1rY|{3`{ZBL$oGk}@;gSZRR(C_Gqz=aY zUcFqgL-pVJ6j#RvhP}c1hAvCRI;JBl!}ro#<45K)iIDRl^Qh1z@hCh z*>_97qK{N#1ls!Fa?NYWl6sJGW9<5GU2c$l_f1MbIm%i)MJlw3za%SM Int32 { + +func requiredBoostSubjectLevel(subject: BoostSubject, group: Bool, context: AccountContext, configuration: PremiumConfiguration) -> Int32 { switch subject { case .stories: return 1 case let .channelReactions(reactionCount): return reactionCount case let .nameColors(colors): - if let value = context.peerNameColors.nameColorsChannelMinRequiredBoostLevel[colors.rawValue] { - return value + if group { + if let value = context.peerNameColors.nameColorsGroupMinRequiredBoostLevel[colors.rawValue] { + return value + } } else { - return 1 + if let value = context.peerNameColors.nameColorsChannelMinRequiredBoostLevel[colors.rawValue] { + return value + } } + return 1 case .nameIcon: return configuration.minChannelNameIconLevel case .profileColors: return configuration.minChannelProfileColorLevel case .profileIcon: - return configuration.minChannelProfileIconLevel + return group ? configuration.minGroupProfileIconLevel : configuration.minChannelProfileIconLevel case .emojiStatus: - return configuration.minChannelEmojiStatusLevel + return group ? configuration.minGroupEmojiStatusLevel : configuration.minChannelEmojiStatusLevel case .wallpaper: - return configuration.minChannelWallpaperLevel + return group ? configuration.minGroupWallpaperLevel : configuration.minChannelWallpaperLevel case .customWallpaper: - return configuration.minChannelCustomWallpaperLevel + return group ? configuration.minGroupCustomWallpaperLevel : configuration.minChannelCustomWallpaperLevel + case .audioTranscription: + return configuration.minGroupAudioTranscriptionLevel + case .emojiPack: + return configuration.minGroupEmojiPackLevel } } @@ -54,9 +64,12 @@ enum BoostSubject: Equatable { case emojiStatus case wallpaper case customWallpaper - - func requiredLevel(context: AccountContext, configuration: PremiumConfiguration) -> Int32 { - return requiredBoostSubjectLevel(subject: self, context: context, configuration: configuration) + case audioTranscription + case emojiPack + + + func requiredLevel(context: AccountContext, group: Bool, configuration: PremiumConfiguration) -> Int32 { + return requiredBoostSubjectLevel(subject: self, group: group, context: context, configuration: configuration) } } @@ -188,14 +201,16 @@ private func generateBadgePath(rectSize: CGSize, tailPosition: CGFloat = 0.5) -> private final class Arguments { let context: AccountContext let presentation: TelegramPresentationTheme + let isGroup: Bool let boost:()->Void let openChannel:()->Void let shareLink:(String)->Void let copyLink:(String)->Void let openGiveaway:()->Void - init(context: AccountContext, presentation: TelegramPresentationTheme, boost:@escaping()->Void, openChannel:@escaping()->Void, shareLink: @escaping(String)->Void, copyLink: @escaping(String)->Void, openGiveaway:@escaping()->Void) { + init(context: AccountContext, presentation: TelegramPresentationTheme, isGroup: Bool, boost:@escaping()->Void, openChannel:@escaping()->Void, shareLink: @escaping(String)->Void, copyLink: @escaping(String)->Void, openGiveaway:@escaping()->Void) { self.context = context self.presentation = presentation + self.isGroup = isGroup self.boost = boost self.copyLink = copyLink self.shareLink = shareLink @@ -244,6 +259,10 @@ private struct State : Equatable { } } + var isGroup: Bool { + return self.peer.peer.isGroup || self.peer.peer.isSupergroup + } + var currentLevelBoosts: Int { return status.boosts - status.currentLevelBoosts } @@ -275,10 +294,10 @@ private struct State : Equatable { case .wallpaper: title = strings().channelBoostEnableWallpapers default: - if level == 0 { - title = strings().channelBoostEnableStories + if self.isGroup { + title = strings().channelBoostTitleGroup } else { - title = strings().channelBoostIncreaseLimit + title = strings().channelBoostTitleChannel } } } else { @@ -287,9 +306,17 @@ private struct State : Equatable { } else { if let _ = remaining { if level == 0 { - title = samePeer ? strings().channelBoostEnableStoriesForChannel : strings().channelBoostEnableStoriesForOtherChannel + if isGroup { + title = strings().channelBoostTitleGroup + } else { + title = strings().channelBoostTitleChannel + } } else { - title = strings().channelBoostHelpUpgradeChannel + if isGroup { + title = strings().channelBoostHelpUpgradeGroup + } else { + title = strings().channelBoostHelpUpgradeChannel + } } } else { title = strings().channelBoostMaxLevelReached @@ -851,7 +878,11 @@ private final class AcceptRowView : TableRowView { if state.isAdmin { title = strings().modalCopyLink } else { - title = strings().channelBoostBoostChannel + if state.isGroup { + title = strings().channelBoostBoostGroup + } else { + title = strings().channelBoostBoostChannel + } gradient = true } } @@ -995,23 +1026,31 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { nameColorsAtLevel.append((key, value)) } + let isGroup = arguments.isGroup if let nextLevels = nextLevels { for level in nextLevels { var perks: [BoostChannelPerk] = [] perks.append(.story(level)) - perks.append(.reaction(level)) - + + if !isGroup { + perks.append(.reaction(level)) + } + var nameColorsCount: Int32 = 0 for (colorLevel, count) in nameColorsAtLevel { if level >= colorLevel && colorLevel == 1 { nameColorsCount = count } } - if nameColorsCount > 0 { + if !isGroup && nameColorsCount > 0 { perks.append(.nameColor(nameColorsCount)) } + if isGroup && level >= premiumConfiguration.minGroupAudioTranscriptionLevel { + perks.append(.audioTranscription) + } + if level >= premiumConfiguration.minChannelProfileColorLevel { let delta = min(level - premiumConfiguration.minChannelProfileColorLevel + 1, 2) perks.append(.profileColor(8 * delta)) @@ -1020,17 +1059,21 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { perks.append(.profileIcon) } + if isGroup && level >= premiumConfiguration.minGroupAudioTranscriptionLevel { + perks.append(.emojiPack) + } + var linkColorsCount: Int32 = 0 for (colorLevel, count) in nameColorsAtLevel { if level >= colorLevel { linkColorsCount += count } } - if linkColorsCount > 0 { + if !isGroup && linkColorsCount > 0 { perks.append(.linkColor(linkColorsCount)) } - if level >= premiumConfiguration.minChannelNameIconLevel { + if !isGroup && level >= premiumConfiguration.minChannelNameIconLevel { perks.append(.linkIcon) } if level >= premiumConfiguration.minChannelEmojiStatusLevel { @@ -1042,6 +1085,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { if level >= premiumConfiguration.minChannelCustomWallpaperLevel { perks.append(.customWallpaper) } + // header entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_perk_level(level), equatable: .init(level), comparable: nil, item: { initialSize, stableId in @@ -1096,7 +1140,7 @@ func BoostChannelModalController(context: AccountContext, peer: Peer, boosts: Ch var close:(()->Void)? = nil - let arguments = Arguments(context: context, presentation: presentation, boost: { + let arguments = Arguments(context: context, presentation: presentation, isGroup: peer.isGroup || peer.isSupergroup, boost: { let myStatus = stateValue.with { $0.myStatus } let nextLevelBoosts = stateValue.with { $0.status.nextLevelBoosts } diff --git a/Telegram-Mac/BoostFeatureRowItem.swift b/Telegram-Mac/BoostFeatureRowItem.swift index 5e1bf34d9..f1e924042 100644 --- a/Telegram-Mac/BoostFeatureRowItem.swift +++ b/Telegram-Mac/BoostFeatureRowItem.swift @@ -20,7 +20,8 @@ enum BoostChannelPerk: Equatable { case emojiStatus case wallpaper(Int32) case customWallpaper - + case audioTranscription + case emojiPack func title() -> String { switch self { case let .story(value): @@ -43,6 +44,11 @@ enum BoostChannelPerk: Equatable { return strings().channelBoostTableWallpaperCountable(Int(value)) case .customWallpaper: return strings().channelBoostTableCustomWallpaper + case .audioTranscription: + return strings().channelBoostTableAudioTranscription + case .emojiPack: + return strings().channelBoostTableEmojiPack + } } @@ -69,6 +75,11 @@ enum BoostChannelPerk: Equatable { return theme.icons.channel_feature_background case .customWallpaper: return theme.icons.channel_feature_background_photo + case .audioTranscription: + return theme.icons.channel_feature_voice_to_text + case .emojiPack: + return theme.icons.channel_feature_emoji_pack + } } } diff --git a/Telegram-Mac/ChannelBoostStatsController.swift b/Telegram-Mac/ChannelBoostStatsController.swift index 7529b0bba..00a2a23fa 100644 --- a/Telegram-Mac/ChannelBoostStatsController.swift +++ b/Telegram-Mac/ChannelBoostStatsController.swift @@ -237,7 +237,11 @@ private final class BoostRowItem : TableRowItem { override var height: CGFloat { - return 100 + if state.isGroup { + return 100 + 100 + } else { + return 100 + } } override func viewClass() -> AnyClass { @@ -473,10 +477,11 @@ private final class BoostRowItemView : TableRowView { return } - transition.updateFrame(view: lineView, frame: lineView.centerFrameX(y: frame.height - lineView.frame.height)) + transition.updateFrame(view: lineView, frame: lineView.centerFrameX(y: top.frame.height + 10)) let topPoint = NSMakePoint(max(min(lineView.frame.minX + lineView.frame.width * item.state.percentToNext - top.frame.width / 2, size.width - 20 - top.frame.width), lineView.frame.minX), lineView.frame.minY - top.frame.height - 10) + transition.updateFrame(view: top, frame: CGRect(origin: topPoint, size: top.frame.size)) } @@ -508,6 +513,7 @@ private struct State : Equatable { var booster: ChannelBoostersContext.State? var revealed: Bool = false + var isGroup: Bool var link: String { if let peer = peer { @@ -683,11 +689,11 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { })) } - entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().statsBoostsBoostersInfo), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(state.isGroup ? strings().statsBoostsBoostersInfoGroup : strings().statsBoostsBoostersInfo), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) index += 1 } else { entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_empty_boosters, equatable: nil, comparable: nil, item: { initialSize, stableId in - return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .singleItem, text: strings().statsBoostsNoBoostersYet, font: .normal(.text), color: theme.colors.grayText) + return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .singleItem, text: state.isGroup ? strings().statsBoostsNoBoostersYetGroup : strings().statsBoostsNoBoostersYet, font: .normal(.text), color: theme.colors.grayText) })) } @@ -712,7 +718,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { }, share: arguments.shareLink, copyLink: arguments.copyLink) })) - entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().statsBoostsLinkInfo), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(state.isGroup ? strings().statsBoostsLinkInfoGroup : strings().statsBoostsLinkInfo), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) index += 1 // entries @@ -728,7 +734,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { arguments.giveaway(nil) }))) - entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().channelBoostsStatsGetBoostsViaGiftsInfo), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(state.isGroup ? strings().channelBoostsStatsGetBoostsViaGiftsInfoGroup : strings().channelBoostsStatsGetBoostsViaGiftsInfo), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) index += 1 } @@ -746,12 +752,12 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { return entries } -func ChannelBoostStatsController(context: AccountContext, peerId: PeerId) -> InputDataController { +func ChannelBoostStatsController(context: AccountContext, peerId: PeerId, isGroup: Bool = false) -> InputDataController { let actionsDisposable = DisposableSet() var getController:(()->InputDataController?)? = nil - let initialState = State() + let initialState = State(isGroup: isGroup) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -793,7 +799,7 @@ func ChannelBoostStatsController(context: AccountContext, peerId: PeerId) -> Inp return InputDataSignalValue(entries: entries(state, arguments: arguments)) } - let controller = InputDataController(dataSignal: signal, title: " ") + let controller = InputDataController(dataSignal: signal, title: strings().statsBoosts) controller.contextObject = boostersContext diff --git a/Telegram-Mac/ChannelPermissionsController.swift b/Telegram-Mac/ChannelPermissionsController.swift index a3e26bfe0..584881a0a 100644 --- a/Telegram-Mac/ChannelPermissionsController.swift +++ b/Telegram-Mac/ChannelPermissionsController.swift @@ -29,7 +29,8 @@ private final class Arguments { let updateSlowMode:(Int32)->Void let convert:()->Void let toggleReveal:(TelegramChatBannedRightsFlags)->Void - init(context: AccountContext, updatePermission: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (ChannelParticipant) -> Void, openPeerInfo: @escaping (Peer) -> Void, openKicked: @escaping () -> Void, presentRestrictedPublicGroupPermissionsAlert: @escaping() -> Void, updateSlowMode:@escaping(Int32)->Void, convert: @escaping()->Void, toggleReveal:@escaping(TelegramChatBannedRightsFlags)->Void) { + let updateBoostNeed:(Int32?)->Void + init(context: AccountContext, updatePermission: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (ChannelParticipant) -> Void, openPeerInfo: @escaping (Peer) -> Void, openKicked: @escaping () -> Void, presentRestrictedPublicGroupPermissionsAlert: @escaping() -> Void, updateSlowMode:@escaping(Int32)->Void, convert: @escaping()->Void, toggleReveal:@escaping(TelegramChatBannedRightsFlags)->Void, updateBoostNeed:@escaping(Int32?)->Void) { self.context = context self.updatePermission = updatePermission self.addPeer = addPeer @@ -42,6 +43,7 @@ private final class Arguments { self.updateSlowMode = updateSlowMode self.convert = convert self.toggleReveal = toggleReveal + self.updateBoostNeed = updateBoostNeed } } @@ -54,6 +56,7 @@ private struct State: Equatable { var revealed: [TelegramChatBannedRightsFlags: Bool] = [:] var peer: PeerEquatable? var cachedData: CachedDataEquatable? + var restrictBoosters: Int32? = nil } func stringForGroupPermission(right: TelegramChatBannedRightsFlags, channel: TelegramChannel?) -> String { @@ -184,6 +187,9 @@ private let _id_convert_to_giga = InputDataIdentifier("_id_convert_to_giga") private let _id_slow_mode = InputDataIdentifier("_id_slow_mode") private let _id_kicked = InputDataIdentifier("_id_kicked") private let _id_add_peer = InputDataIdentifier("_id_add_peer") +private let _id_do_not_boosters = InputDataIdentifier("_id_do_not_boosters") +private let _id_boost_count = InputDataIdentifier("_id_boost_count") + private func _id_peer(_ peerId: PeerId) -> InputDataIdentifier { return InputDataIdentifier("_id_peer_\(peerId.toInt64())") } @@ -288,6 +294,44 @@ private func entries(state: State, arguments: Arguments) -> [InputDataEntry] { } return index } + + let insertBoost:(TelegramChatBannedRightsFlags)->Void = { rigths in + //TODO LANG + if rigths.contains(.banSendMedia) || rigths.contains(.banSendText) { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_do_not_boosters, data: .init(name: "Do Not Restrict Boosters", color: theme.colors.text, type: .switchable(state.restrictBoosters != nil), viewType: state.restrictBoosters != nil ? .firstItem : .singleItem, action: { + if state.restrictBoosters != nil { + arguments.updateBoostNeed(nil) + } else { + arguments.updateBoostNeed(1) + } + }))) + + let infoText: String + if let boost = state.restrictBoosters { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_boost_count, equatable: .init(boost), comparable: nil, item: { initialSize, stableId in + let list:[Int32] = [1, 2, 3, 4, 5] + let titles: [String] = ["1", + "2", + "3", + "4", + "5"] + return SelectSizeRowItem(initialSize, stableId: stableId, current: boost, sizes: list, hasMarkers: false, titles: titles, viewType: .lastItem, selectAction: { index in + arguments.updateBoostNeed(list[index]) + }) + })) + infoText = "Choose how many boosts a user must give to the group to bypass restrictions on sending messages." + } else { + infoText = "Turn this on to always allow users who boosted your group to send messages and media." + } + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(infoText), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + + } entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 @@ -346,7 +390,6 @@ private func entries(state: State, arguments: Arguments) -> [InputDataEntry] { index += 1 entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_convert_to_giga, data: .init(name: strings().groupInfoPermissionsBroadcastConvert, color: theme.colors.text, type: .next, viewType: .singleItem, action: arguments.convert))) - index += 1 entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().groupInfoPermissionsBroadcastConvertInfo(Formatter.withSeparator.string(from: .init(value: arguments.context.limitConfiguration.maxSupergroupMemberCount))!)), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) index += 1 @@ -356,6 +399,8 @@ private func entries(state: State, arguments: Arguments) -> [InputDataEntry] { } } + + insertBoost(effectiveRightsFlags) if !channel.flags.contains(.isGigagroup) { insertSlowMode(cachedData.slowModeTimeout) @@ -365,7 +410,6 @@ private func entries(state: State, arguments: Arguments) -> [InputDataEntry] { } entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_kicked, data: .init(name: strings().groupInfoPermissionsRemoved, color: theme.colors.text, type: .nextContext(cachedData.participantsSummary.kickedCount.flatMap({ "\($0 > 0 ? "\($0)" : "")" }) ?? ""), viewType: .singleItem, action: arguments.openKicked))) - index += 1 if !channel.flags.contains(.isGigagroup) { @@ -379,7 +423,6 @@ private func entries(state: State, arguments: Arguments) -> [InputDataEntry] { entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_add_peer, equatable: .init(viewType), comparable: nil, item: { initialSize, stableId in return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: strings().groupInfoPermissionsAddException, nameStyle: blueActionButton, type: .none, viewType: viewType, action: arguments.addPeer, thumb: GeneralThumbAdditional(thumb: theme.icons.peerInfoAddMember, textInset: 52, thumbInset: 5)) })) - index += 1 @@ -437,7 +480,6 @@ private func entries(state: State, arguments: Arguments) -> [InputDataEntry] { } }) })) - index += 1 } } @@ -479,11 +521,14 @@ private func entries(state: State, arguments: Arguments) -> [InputDataEntry] { } items.append(.init(string: string, flags: rights.0, selected: isSelected, enabled: enabled, viewType: bestGeneralViewType(list, for: i), reveable: rights.0 == .banSendMedia, subItems: subItems)) } + index = insertPermissions(items) entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 + insertBoost(effectiveRightsFlags) + insertSlowMode(nil) entries.append(.sectionId(sectionId, type: .normal)) @@ -496,7 +541,6 @@ private func entries(state: State, arguments: Arguments) -> [InputDataEntry] { entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_add_peer, equatable: .init(viewType), comparable: nil, item: { initialSize, stableId in return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: strings().groupInfoPermissionsAddException, nameStyle: blueActionButton, type: .none, viewType: viewType, action: arguments.addPeer, thumb: GeneralThumbAdditional(thumb: theme.icons.peerInfoAddMember, textInset: 52, thumbInset: 5)) })) - index += 1 } @@ -529,7 +573,7 @@ final class ChannelPermissionsController : TableViewController { let peerId = self.peerId let context = self.context - let statePromise = ValuePromise(State(), ignoreRepeated: true) + let statePromise = ValuePromise(ignoreRepeated: true) let stateValue = Atomic(value: State()) let updateState: ((State) -> State) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) @@ -708,7 +752,31 @@ final class ChannelPermissionsController : TableViewController { } else { effectiveRightsFlags = TelegramChatBannedRightsFlags() } - if value { + + if rights == .banSendMedia { + if value { + effectiveRightsFlags.remove(rights) + for item in banSendMediaSubList() { + effectiveRightsFlags.remove(item.0) + } + } else { + effectiveRightsFlags.insert(rights) + for (right, _) in allGroupPermissionList(peer: group) { + if groupPermissionDependencies(right).contains(rights) { + effectiveRightsFlags.insert(right) + } + } + + for item in banSendMediaSubList() { + effectiveRightsFlags.insert(item.0) + for (right, _) in allGroupPermissionList(peer: group) { + if groupPermissionDependencies(right).contains(item.0) { + effectiveRightsFlags.insert(right) + } + } + } + } + } else if value { effectiveRightsFlags.remove(rights) effectiveRightsFlags = effectiveRightsFlags.subtracting(groupPermissionDependencies(rights)) } else { @@ -849,6 +917,12 @@ final class ChannelPermissionsController : TableViewController { } return current } + }, updateBoostNeed: { value in + updateState { current in + var current = current + current.restrictBoosters = value + return current + } }) let previous = Atomic<[AppearanceWrapperEntry]>(value: []) diff --git a/Telegram-Mac/ChatController.swift b/Telegram-Mac/ChatController.swift index 9c1a607be..aadfad3dc 100644 --- a/Telegram-Mac/ChatController.swift +++ b/Telegram-Mac/ChatController.swift @@ -5395,7 +5395,7 @@ class ChatController: EditableViewController, Notifable, Tab let isFirst = Atomic(value: true) let tagsAndFiles: Signal<([SavedMessageTags.Tag], [Int64 : TelegramMediaFile]), NoError> - if peerId == context.peerId, tagsGloballyEnabled { + if peerId == context.peerId { tagsAndFiles = combineLatest(context.engine.data.subscribe( TelegramEngine.EngineData.Item.Messages.SavedMessageTagStats(peerId: context.account.peerId, threadId: threadId64) ) diff --git a/Telegram-Mac/ChatInterfaceInteraction.swift b/Telegram-Mac/ChatInterfaceInteraction.swift index aac70d721..6b1c4861a 100644 --- a/Telegram-Mac/ChatInterfaceInteraction.swift +++ b/Telegram-Mac/ChatInterfaceInteraction.swift @@ -897,7 +897,7 @@ final class ChatInteraction : InterfaceObserver { let updatedOpaqueData = try? EngineEncoder.encode(interfaceState) - var s:Signal = context.engine.peers.setOpaqueChatInterfaceState(peerId: peerId, threadId: mode.threadId64, state: .init(opaqueData: updatedOpaqueData, historyScrollMessageIndex: interfaceState.historyScrollMessageIndex, synchronizeableInputState: interfaceState.synchronizeableInputState)) + var s:Signal = context.engine.peers.setOpaqueChatInterfaceState(peerId: peerId, threadId: mode.threadId64, state: .init(opaqueData: updatedOpaqueData, historyScrollMessageIndex: interfaceState.historyScrollMessageIndex, mediaDraftState: nil, synchronizeableInputState: interfaceState.synchronizeableInputState)) if !force && !interfaceState.inputState.inputText.isEmpty { s = s |> delay(10, queue: Queue.mainQueue()) diff --git a/Telegram-Mac/ChatPresentationInterfaceState.swift b/Telegram-Mac/ChatPresentationInterfaceState.swift index 2d889fa1b..62b36e43d 100644 --- a/Telegram-Mac/ChatPresentationInterfaceState.swift +++ b/Telegram-Mac/ChatPresentationInterfaceState.swift @@ -603,6 +603,14 @@ class ChatPresentationInterfaceState: Equatable { return .block("") } +// #if DEBUG +// if let peer = peer as? TelegramChannel, peer.adminRights == nil { +// return .action(strings().boostGroupChatInputAction, { chatInteraction in +// +// }, nil) +// } +// #endif + if searchMode.tag != nil { return .action(!searchMode.showAll ? strings().chatStateShowOtherMessages : strings().chatStateHideOtherMessages, { chatInteraction in chatInteraction.update { current in diff --git a/Telegram-Mac/ChatReactionsView.swift b/Telegram-Mac/ChatReactionsView.swift index 3113354d5..7df3edab3 100644 --- a/Telegram-Mac/ChatReactionsView.swift +++ b/Telegram-Mac/ChatReactionsView.swift @@ -245,7 +245,7 @@ final class ChatReactionsLayout { lhs.rect == rhs.rect && lhs.message.id == rhs.message.id && lhs.canViewList == rhs.canViewList && - lhs.message.effectiveReactions(isTags: lhs.context.peerId == lhs.message.id.peerId && tagsGloballyEnabled) == rhs.message.effectiveReactions(isTags: rhs.context.peerId == rhs.message.id.peerId && tagsGloballyEnabled) + lhs.message.effectiveReactions(isTags: lhs.context.peerId == lhs.message.id.peerId) == rhs.message.effectiveReactions(isTags: rhs.context.peerId == rhs.message.id.peerId) } static func <(lhs: Reaction, rhs: Reaction) -> Bool { return lhs.index < rhs.index @@ -347,7 +347,7 @@ final class ChatReactionsLayout { let context = self.context let tag = self.value.value let label = self.text?.attributedString.string - if message.id.peerId == context.peerId, tagsGloballyEnabled { + if message.id.peerId == context.peerId { if let menu = menu { return menu } else { @@ -491,7 +491,7 @@ final class ChatReactionsLayout { init(context: AccountContext, message: Message, available: AvailableReactions?, peerAllowed: PeerAllowedReactions?, savedMessageTags: SavedMessageTags?, engine:Reactions, theme: TelegramPresentationTheme, renderType: ChatItemRenderType, currentTag: MessageReaction.Reaction?, isIncoming: Bool, isOutOfBounds: Bool, hasWallpaper: Bool, stateOverlayTextColor: NSColor, openInfo:@escaping(PeerId)->Void, runEffect: @escaping(MessageReaction.Reaction)->Void, tagAction:@escaping(MessageReaction.Reaction)->Void) { var mode: Mode = .full - if message.id.peerId == context.peerId, tagsGloballyEnabled { + if message.id.peerId == context.peerId { mode = .tag } self.message = message @@ -510,7 +510,7 @@ final class ChatReactionsLayout { return index } - let reactions = message.effectiveReactions(context.peerId, isTags: context.peerId == message.id.peerId && tagsGloballyEnabled)! + let reactions = message.effectiveReactions(context.peerId, isTags: context.peerId == message.id.peerId)! var indexes:[MessageReaction.Reaction: Int] = [:] if let available = available { @@ -580,7 +580,7 @@ final class ChatReactionsLayout { if message.id.peerId == context.peerId, !isFilterTag, mode == .tag { tagAction(value) } else { - engine.react(message.id, values: message.newReactions(with: value.toUpdate(source.file), isTags: context.peerId == message.id.peerId && tagsGloballyEnabled)) + engine.react(message.id, values: message.newReactions(with: value.toUpdate(source.file), isTags: context.peerId == message.id.peerId)) } }, openInfo: openInfo, runEffect: runEffect) } else { @@ -1501,7 +1501,7 @@ final class ChatReactionsView : View { guard let currentLayout = currentLayout else { return } - let layout = currentLayout.message.effectiveReactions(currentLayout.context.peerId, isTags: currentLayout.context.peerId == currentLayout.message.id.peerId && tagsGloballyEnabled) + let layout = currentLayout.message.effectiveReactions(currentLayout.context.peerId, isTags: currentLayout.context.peerId == currentLayout.message.id.peerId) let peer = layout?.recentPeers.first(where: { $0.isUnseen || !checkUnseen }) if let peer = peer, let reaction = reactions.first(where: { $0.value.value == peer.value }) { reaction.runEffect(reaction.value.value) diff --git a/Telegram-Mac/ChatRowItem.swift b/Telegram-Mac/ChatRowItem.swift index 5d6f8b964..9d37badb0 100644 --- a/Telegram-Mac/ChatRowItem.swift +++ b/Telegram-Mac/ChatRowItem.swift @@ -3082,7 +3082,7 @@ class ChatRowItem: TableRowItem { } |> take(1) - let isTags = context.peerId == peerId && tagsGloballyEnabled + let isTags = context.peerId == peerId diff --git a/Telegram-Mac/EmptyGroupstickerSearchRowItem.swift b/Telegram-Mac/EmptyGroupstickerSearchRowItem.swift index d2ffed577..e317c7a35 100644 --- a/Telegram-Mac/EmptyGroupstickerSearchRowItem.swift +++ b/Telegram-Mac/EmptyGroupstickerSearchRowItem.swift @@ -10,8 +10,14 @@ import Cocoa import TGUIKit class EmptyGroupstickerSearchRowItem: GeneralRowItem { - - init(_ initialSize: NSSize, height: CGFloat, stableId: AnyHashable, viewType: GeneralViewType = .legacy) { + + enum PackType { + case stickers + case emojies + } + let packType: PackType + init(_ initialSize: NSSize, height: CGFloat, stableId: AnyHashable, viewType: GeneralViewType = .legacy, type: PackType = .stickers) { + self.packType = type super.init(initialSize, height: height, stableId: stableId, viewType: viewType) } @@ -77,8 +83,8 @@ private class EmptyGroupstickerSearchRowView : TableRowView { case let .modern(_, insets): self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) - let headerLayout = TextViewLayout(.initialize(string: strings().groupStickersEmptyHeader, color: theme.colors.redUI, font: .medium(.text)), maximumNumberOfLines: 1) - let descLayout = TextViewLayout(.initialize(string: strings().groupStickersEmptyDesc, color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + let headerLayout = TextViewLayout(.initialize(string: item.packType == .stickers ? strings().groupStickersEmptyHeader : strings().groupStickersEmptyHeaderEmoji, color: theme.colors.redUI, font: .medium(.text)), maximumNumberOfLines: 1) + let descLayout = TextViewLayout(.initialize(string: item.packType == .stickers ? strings().groupStickersEmptyDesc : strings().groupStickersEmptyDescEmoji, color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) headerLayout.measure(width: item.blockWidth - insets.left - insets.right - 40) descLayout.measure(width: item.blockWidth - insets.left - insets.right - 40) descView.update(descLayout) diff --git a/Telegram-Mac/FastSettings.swift b/Telegram-Mac/FastSettings.swift index dc8763037..99acc6fc5 100644 --- a/Telegram-Mac/FastSettings.swift +++ b/Telegram-Mac/FastSettings.swift @@ -630,7 +630,12 @@ class FastSettings { static var premiumPerks:[String] { - let perks = [PremiumValue.stories.rawValue, PremiumValue.wallpapers.rawValue, PremiumValue.peer_colors.rawValue, PremiumValue.saved_tags.rawValue] + let perks = [PremiumValue.stories.rawValue, + PremiumValue.wallpapers.rawValue, + PremiumValue.peer_colors.rawValue, + PremiumValue.saved_tags.rawValue, + PremiumValue.last_seen.rawValue, + PremiumValue.messages_privacy.rawValue] let dismissedPerks = UserDefaults.standard.value(forKey: "dismissedPerks") as? [String] ?? [] return perks.filter { !dismissedPerks.contains($0) } } diff --git a/Telegram-Mac/GroupEmojiPackController.swift b/Telegram-Mac/GroupEmojiPackController.swift index 269dcf601..8dcf9136d 100644 --- a/Telegram-Mac/GroupEmojiPackController.swift +++ b/Telegram-Mac/GroupEmojiPackController.swift @@ -16,9 +16,11 @@ import Postbox private final class Arguments { let context: AccountContext let select:(State.Item)->Void - init(context: AccountContext, select:@escaping(State.Item)->Void) { + let openStickerBot:(String)->Void + init(context: AccountContext, select:@escaping(State.Item)->Void, openStickerBot:@escaping(String)->Void) { self.context = context self.select = select + self.openStickerBot = openStickerBot } } @@ -39,11 +41,18 @@ private struct State : Equatable { var searchState: SearchState? var searchResult: SearchResult? var selected: Item? + var loading: Bool = false + var string: String? } private func _id_pack(_ id: ItemCollectionId) -> InputDataIdentifier { return InputDataIdentifier("_id_pack_\(id.id)") } +private func _id_pack_selected(_ id: ItemCollectionId) -> InputDataIdentifier { + return InputDataIdentifier("_id_pack_\(id.id)_selected") +} +private let _id_input = InputDataIdentifier("_id_input") +private let _id_loading = InputDataIdentifier("_id_loading") private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { var entries:[InputDataEntry] = [] @@ -59,14 +68,56 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { sectionId += 1 } - - - struct Tuple : Equatable { let item: State.Item let selected: Bool let viewType: GeneralViewType } + + if state.searchResult == nil { + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.string), error: nil, identifier: _id_input, mode: .plain, data: .init(viewType: state.selected != nil || state.loading ? .firstItem : .singleItem, defaultText: "https://t.me/addstickers/", pasteFilter: { value in + if let index = value.range(of: "t.me/addstickers/") { + return (true, String(value[index.upperBound...])) + } + return (false, value) + }), placeholder: nil, inputPlaceholder: "https://t.me/addstickers/", filter: { text in + var filter = NSCharacterSet.alphanumerics + filter.insert(charactersIn: "_/") + return text.trimmingCharacters(in: filter.inverted) + }, limit: 25 + 30)) + + if state.loading { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_loading, equatable: .init(state), comparable: nil, item: { initialSize, stableId in + return LoadingTableItem(initialSize, height: 50, stableId: stableId, viewType: .lastItem) + })) + } else { + if let item = state.selected { + let tuple = Tuple(item: item, selected: false, viewType: .lastItem) + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_pack_selected(tuple.item.id), equatable: .init(tuple), comparable: nil, item: { initialSize, stableId in + return StickerSetTableRowItem(initialSize, context: arguments.context, stableId: stableId, info: tuple.item.info, topItem: tuple.item.item, itemCount: tuple.item.count, unread: false, editing: .init(editable: false, editing: false), enabled: true, control: tuple.selected ? .selected : .empty, viewType: tuple.viewType, action: { + arguments.select(tuple.item) + }) + })) + } else if let string = state.string, !string.isEmpty { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_loading, equatable: .init(state), comparable: nil, item: { initialSize, stableId in + return EmptyGroupstickerSearchRowItem(initialSize, height: 50, stableId: stableId, viewType: .lastItem, type: .emojies) + })) + } + } + + entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(strings().groupEmojiPackCreateInfo, linkHandler: arguments.openStickerBot), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + } + + + + + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + var tuples: [Tuple] = [] @@ -124,11 +175,13 @@ func GroupEmojiPackController(context: AccountContext) -> InputDataController { let actionsDisposable = DisposableSet() let searchDisposable = MetaDisposable() - + let resolveDisposable = MetaDisposable() actionsDisposable.add(searchDisposable) - + actionsDisposable.add(resolveDisposable) let initialState = State() + var closeSearch:(()->Void)? = nil + let statePromise = ValuePromise(ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((State) -> State) -> Void = { f in @@ -168,7 +221,7 @@ func GroupEmojiPackController(context: AccountContext) -> InputDataController { return current } - if let result = result, let state = state { + if let _ = result, let state = state { let emojies = context.sharedContext.inputSource.searchEmoji(postbox: context.account.postbox, engine: context.engine, sharedContext: context.sharedContext, query: state.request, completeMatch: false, checkPrediction: false) |> map(Optional.init) |> delay(0.2, queue: .concurrentDefaultQueue()) let signal = context.engine.stickers.searchEmojiSetsRemotely(query: state.request) @@ -197,13 +250,22 @@ func GroupEmojiPackController(context: AccountContext) -> InputDataController { var current = current if current.selected?.id == item.id { current.selected = nil + current.string = nil } else { current.selected = item + current.string = item.info.shortName } return current } + closeSearch?() }, source: stateValue.with { $0.selected?.id == item.id } ? .removeGroupEmojiPack : .installGroupEmojiPack), for: context.window) + }, openStickerBot: { name in + _ = (resolveUsername(username: name, context: context) |> deliverOnMainQueue).start(next: { peer in + if let peer = peer { + context.bindings.rootNavigation().push(ChatAdditionController(context: context, chatLocation: .peer(peer.id))) + } + }) }) let searchValue:Atomic = Atomic(value: .none({ searchState in @@ -240,7 +302,7 @@ func GroupEmojiPackController(context: AccountContext) -> InputDataController { return InputDataSignalValue(entries: entries(state, arguments: arguments), searchState: searchData) } - let controller = InputDataController(dataSignal: signal, title: strings().groupEmojiPackTitle, customRightButton: { controller in + let controller = InputDataController(dataSignal: signal, title: strings().groupEmojiPackTitle, removeAfterDisappear: false, customRightButton: { controller in let bar = ImageBarView(controller: controller, theme.icons.chatSearch) bar.button.set(handler: { _ in updateSearchValue { current in @@ -263,6 +325,7 @@ func GroupEmojiPackController(context: AccountContext) -> InputDataController { return bar }) + controller.searchKeyInvocation = { updateSearchValue { current in switch current { @@ -282,6 +345,61 @@ func GroupEmojiPackController(context: AccountContext) -> InputDataController { return .invoked } + controller.updateDatas = { data in + + let text = data[_id_input]?.stringValue ?? "" + + updateState { current in + var current = current + current.string = text + return current + } + + if text.isEmpty { + resolveDisposable.set(nil) + } else { + resolveDisposable.set((context.engine.stickers.loadedStickerPack(reference: .name(text), forceActualized: false) |> deliverOnMainQueue).start(next: { result in + switch result { + case .fetching: + updateState { current in + var current = current + current.loading = true + return current + } + case .none: + updateState { current in + var current = current + current.loading = false + current.selected = nil + return current + } + case let .result(info, items, _): + updateState { current in + var current = current + current.loading = false + if let first = items.first { + current.selected = .init(info: info, id: info.id, item: first, count: info.count) + } else { + current.selected = nil + } + return current + } + } + })) + } + return .none + } + + closeSearch = { + updateSearchValue { _ in + return .none({ searchState in + updateSearchState { _ in + return nil + } + }) + } + } + controller.onDeinit = { actionsDisposable.dispose() } diff --git a/Telegram-Mac/GroupInfoEntries.swift b/Telegram-Mac/GroupInfoEntries.swift index e0f2e8172..19f9a9356 100644 --- a/Telegram-Mac/GroupInfoEntries.swift +++ b/Telegram-Mac/GroupInfoEntries.swift @@ -425,6 +425,26 @@ final class GroupInfoArguments : PeerInfoArguments { func stats(_ datacenterId: Int32) { self.pushViewController(GroupStatsViewController(context, peerId: peerId)) } + func boosts(_ access: GroupAccess) { + let context = self.context + + if access.canEditGroupInfo { + self.pushViewController(ChannelBoostStatsController(context: context, peerId: peerId, isGroup: true)) + } else { + let signal: Signal<(Peer, ChannelBoostStatus?, MyBoostStatus?)?, NoError> = context.account.postbox.loadedPeerWithId(peerId) |> mapToSignal { value in + return combineLatest(context.engine.peers.getChannelBoostStatus(peerId: value.id), context.engine.peers.getMyBoostStatus()) |> map { + (value, $0, $1) + } + } + _ = showModalProgress(signal: signal, for: context.window).start(next: { value in + if let value = value, let boosts = value.1 { + showModal(with: BoostChannelModalController(context: context, peer: value.0, boosts: boosts, myStatus: value.2), for: context.window) + } else { + alert(for: context.window, info: strings().unknownError) + } + }) + } + } func makeVoiceChat(_ current: CachedChannelData.ActiveCall?, callJoinPeerId: PeerId?) { let context = self.context diff --git a/Telegram-Mac/ImageUtils.swift b/Telegram-Mac/ImageUtils.swift index 6f5cf87e8..9e1c99561 100644 --- a/Telegram-Mac/ImageUtils.swift +++ b/Telegram-Mac/ImageUtils.swift @@ -40,7 +40,8 @@ private extension PeerNameColors.Colors { extension PeerNameColors { - public func get(_ color: PeerNameColor, dark: Bool = presentation.colors.isDark) -> Colors { + + public func get(_ color: PeerNameColor, dark: Bool = false) -> Colors { if dark, let colors = self.darkColors[color.rawValue] { return colors } else if let colors = self.colors[color.rawValue] { @@ -50,7 +51,7 @@ extension PeerNameColors { } } - public func getProfile(_ color: PeerNameColor, dark: Bool = presentation.colors.isDark, subject: Subject = .background) -> Colors { + public func getProfile(_ color: PeerNameColor, dark: Bool = false, subject: Subject = .background) -> Colors { switch subject { case .background: if dark, let colors = self.profileDarkColors[color.rawValue] { @@ -58,7 +59,7 @@ extension PeerNameColors { } else if let colors = self.profileColors[color.rawValue] { return colors } else { - return PeerNameColors.Colors(main: NSColor(rgb: 0xcc5049)) + return Colors(main: NSColor(rgb: 0xcc5049)) } case .palette: if dark, let colors = self.profilePaletteDarkColors[color.rawValue] { @@ -79,7 +80,7 @@ extension PeerNameColors { } } - static func with(availableReplyColors: EngineAvailableColorOptions, availableProfileColors: EngineAvailableColorOptions) -> PeerNameColors { + public static func with(availableReplyColors: EngineAvailableColorOptions, availableProfileColors: EngineAvailableColorOptions) -> PeerNameColors { var colors: [Int32: Colors] = [:] var darkColors: [Int32: Colors] = [:] var displayOrder: [Int32] = [] @@ -92,13 +93,17 @@ extension PeerNameColors { var profileDisplayOrder: [Int32] = [] var nameColorsChannelMinRequiredBoostLevel: [Int32: Int32] = [:] - + var nameColorsGroupMinRequiredBoostLevel: [Int32: Int32] = [:] if !availableReplyColors.options.isEmpty { for option in availableReplyColors.options { if let requiredChannelMinBoostLevel = option.value.requiredChannelMinBoostLevel { nameColorsChannelMinRequiredBoostLevel[option.key] = requiredChannelMinBoostLevel } + if let requiredGroupMinBoostLevel = option.value.requiredGroupMinBoostLevel { + nameColorsGroupMinRequiredBoostLevel[option.key] = requiredGroupMinBoostLevel + } + if let parsedLight = PeerNameColors.Colors(colors: option.value.light.background) { colors[option.key] = parsedLight } @@ -106,8 +111,10 @@ extension PeerNameColors { darkColors[option.key] = parsedDark } - if !displayOrder.contains(option.key) { - displayOrder.append(option.key) + for option in availableReplyColors.options { + if !displayOrder.contains(option.key) { + displayOrder.append(option.key) + } } } } else { @@ -137,8 +144,10 @@ extension PeerNameColors { if let parsedStoryDark = (option.value.dark?.stories).flatMap(PeerNameColors.Colors.init(colors:)) { profileStoryDarkColors[option.key] = parsedStoryDark } - if !profileDisplayOrder.contains(option.key) { - profileDisplayOrder.append(option.key) + for option in availableProfileColors.options { + if !profileDisplayOrder.contains(option.key) { + profileDisplayOrder.append(option.key) + } } } } @@ -154,7 +163,8 @@ extension PeerNameColors { profileStoryColors: profileStoryColors, profileStoryDarkColors: profileStoryDarkColors, profileDisplayOrder: profileDisplayOrder, - nameColorsChannelMinRequiredBoostLevel: nameColorsChannelMinRequiredBoostLevel + nameColorsChannelMinRequiredBoostLevel: nameColorsChannelMinRequiredBoostLevel, + nameColorsGroupMinRequiredBoostLevel: nameColorsGroupMinRequiredBoostLevel ) } } diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index dc486986f..fb29a6324 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259033 + 259084 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/LottieLocalAnimations.swift b/Telegram-Mac/LottieLocalAnimations.swift index 5cd194ea4..07e2e43e5 100644 --- a/Telegram-Mac/LottieLocalAnimations.swift +++ b/Telegram-Mac/LottieLocalAnimations.swift @@ -280,7 +280,8 @@ enum LocalAnimatedSticker : String { case menu_eye_slash case menu_lighting case menu_quote - + case menu_boost + case emoji_category_activities case emoji_category_angry case emoji_category_arrow_to_search diff --git a/Telegram-Mac/MessagesPrivacyController.swift b/Telegram-Mac/MessagesPrivacyController.swift index 43f0a5089..7e8dcd468 100644 --- a/Telegram-Mac/MessagesPrivacyController.swift +++ b/Telegram-Mac/MessagesPrivacyController.swift @@ -87,12 +87,12 @@ func MessagesPrivacyController(context: AccountContext, globalSettings: GlobalPr let arguments = Arguments(context: context, alert: { if !context.isPremium { showModalText(for: context.window, text: strings().privacySettingsMessagesPremiumError, button: strings().alertLearnMore, callback: { _ in - showModal(with: PremiumBoardingController(context: context, source: .messages_privacy), for: context.window) + showModal(with: PremiumBoardingController(context: context, source: .messages_privacy, openFeatures: true), for: context.window) }) return } }, premium: { - showModal(with: PremiumBoardingController(context: context, source: .messages_privacy), for: context.window) + showModal(with: PremiumBoardingController(context: context, source: .messages_privacy, openFeatures: true), for: context.window) }, toggle: { value in updateState { current in var current = current diff --git a/Telegram-Mac/PeerInfoHeadItem.swift b/Telegram-Mac/PeerInfoHeadItem.swift index f67b9c384..a59efe141 100644 --- a/Telegram-Mac/PeerInfoHeadItem.swift +++ b/Telegram-Mac/PeerInfoHeadItem.swift @@ -14,7 +14,7 @@ import TelegramCore import ColorPalette import TelegramMedia -fileprivate final class ActionButton : Control { +final class ActionButton : Control { fileprivate let imageView: ImageView = ImageView() fileprivate let textView: TextView = TextView() @@ -87,7 +87,6 @@ fileprivate final class ActionButton : Control { super.layout() imageView.centerX(y: 5) textView.centerX(y: frame.height - textView.frame.height - 11) - } } @@ -118,7 +117,7 @@ extension TelegramPeerPhoto : Equatable { fileprivate let actionItemWidth: CGFloat = 145 fileprivate let actionItemInsetWidth: CGFloat = 20 -private struct SubActionItem { +struct SubActionItem { let text: String let destruct: Bool let action:()->Void @@ -131,7 +130,7 @@ private struct SubActionItem { } } -private final class ActionItem { +final class ActionItem { let text: String let destruct: Bool let image: CGImage @@ -266,6 +265,7 @@ private func actionItems(item: PeerInfoHeadItem, width: CGFloat, theme: Telegram } else if let peer = item.peer, peer.isSupergroup || peer.isGroup, let arguments = item.arguments as? GroupInfoArguments { let access = peer.groupAccess + if access.canAddMembers { items.append(ActionItem(text: strings().peerInfoActionAddMembers, color: item.accentColor, image: theme.icons.profile_add_member, animation: .menu_plus, action: { arguments.addMember(access.canCreateInviteLink) @@ -277,6 +277,10 @@ private func actionItems(item: PeerInfoHeadItem, width: CGFloat, theme: Telegram })) } + items.append(ActionItem(text: strings().peerInfoActionBoostGroup, color: item.accentColor, image: theme.icons.profile_boost, animation: .menu_boost, action: { + arguments.boosts(peer.groupAccess) + })) + if let cachedData = item.peerView.cachedData as? CachedChannelData, let peer = peer as? TelegramChannel { if peer.groupAccess.canMakeVoiceChat { diff --git a/Telegram-Mac/PremiumBoardingController.swift b/Telegram-Mac/PremiumBoardingController.swift index 8847191a5..f8dce5f08 100644 --- a/Telegram-Mac/PremiumBoardingController.swift +++ b/Telegram-Mac/PremiumBoardingController.swift @@ -153,12 +153,12 @@ enum PremiumLogEventsSource : Equatable { return .no_ads case .recommended_channels: return nil - case .last_seen: - return nil - case .messages_privacy: - return nil case .saved_tags: return .saved_tags + case .last_seen: + return .last_seen + case .messages_privacy: + return .messages_privacy } } @@ -256,16 +256,20 @@ enum PremiumValue : String { case wallpapers case peer_colors case saved_tags + case last_seen + case messages_privacy func gradient(_ index: Int) -> [NSColor] { let colors:[NSColor] = [ NSColor(rgb: 0xef6922), NSColor(rgb: 0xe95a2c), NSColor(rgb: 0xe74e33), + NSColor(rgb: 0xe54837), NSColor(rgb: 0xe3433c), NSColor(rgb: 0xdb374b), NSColor(rgb: 0xcb3e6d), NSColor(rgb: 0xbc4395), NSColor(rgb: 0xab4ac4), NSColor(rgb: 0x9b4fed), + NSColor(rgb: 0x7861ff), NSColor(rgb: 0x8958ff), NSColor(rgb: 0x676bff), NSColor(rgb: 0x5b79ff), @@ -350,6 +354,10 @@ enum PremiumValue : String { return NSImage(named: "Icon_Premium_Peer_Colors")!.precomposed(presentation.colors.accent) case .saved_tags: return NSImage(named: "Icon_Premium_Boarding_Tag")!.precomposed(presentation.colors.accent) + case .last_seen: + return NSImage(named: "Icon_Premium_Boarding_LastSeen")!.precomposed(presentation.colors.accent) + case .messages_privacy: + return NSImage(named: "Icon_Premium_Boarding_MessagePrivacy")!.precomposed(presentation.colors.accent) } } @@ -389,6 +397,10 @@ enum PremiumValue : String { return strings().premiumBoardingColorsTitle case .saved_tags: return strings().premiumBoardingSavedTagsTitle + case .last_seen: + return strings().premiumBoardingLastSeenTitle + case .messages_privacy: + return strings().premiumBoardingMessagePrivacyTitle } } func info(_ limits: PremiumLimitConfig) -> String { @@ -427,6 +439,10 @@ enum PremiumValue : String { return strings().premiumBoardingColorsInfo case .saved_tags: return strings().premiumBoardingSavedTagsInfo + case .last_seen: + return strings().premiumBoardingLastSeenInfo + case .messages_privacy: + return strings().premiumBoardingMessagePrivacyInfo } } } @@ -434,7 +450,7 @@ enum PremiumValue : String { private struct State : Equatable { - var values:[PremiumValue] = [.double_limits, .stories, .more_upload, .faster_download, .voice_to_text, .no_ads, .infinite_reactions, .emoji_status, .premium_stickers, .animated_emoji, .advanced_chat_management, .profile_badge, .animated_userpics, .translations, .saved_tags] + var values:[PremiumValue] = [.double_limits, .stories, .more_upload, .faster_download, .voice_to_text, .no_ads, .infinite_reactions, .emoji_status, .premium_stickers, .animated_emoji, .advanced_chat_management, .profile_badge, .animated_userpics, .translations, .saved_tags, .last_seen, .messages_privacy] let source: PremiumLogEventsSource var premiumProduct: InAppPurchaseManager.Product? diff --git a/Telegram-Mac/PremiumBoardingFeaturesController.swift b/Telegram-Mac/PremiumBoardingFeaturesController.swift index 9e59f883e..acbbcfffa 100644 --- a/Telegram-Mac/PremiumBoardingFeaturesController.swift +++ b/Telegram-Mac/PremiumBoardingFeaturesController.swift @@ -256,6 +256,22 @@ final class PremiumBoardingFeaturesView: View { }) slideView.addSlide(savedTags) + let lastSeen = PremiumFeatureSlideView(frame: slideView.bounds, presentation: presentation) + lastSeen.setup(context: context, type: .last_seen, decoration: .badgeStars, getView: { _ in + let view = PremiumDemoLegacyPhoneView(frame: .zero) + view.setup(context: context, video: configuration.videos[PremiumValue.last_seen.rawValue], position: .top) + return view + }) + slideView.addSlide(lastSeen) + + let messagesPrivacy = PremiumFeatureSlideView(frame: slideView.bounds, presentation: presentation) + messagesPrivacy.setup(context: context, type: .messages_privacy, decoration: .badgeStars, getView: { _ in + let view = PremiumDemoLegacyPhoneView(frame: .zero) + view.setup(context: context, video: configuration.videos[PremiumValue.messages_privacy.rawValue], position: .top) + return view + }) + slideView.addSlide(messagesPrivacy) + switch value { case .double_limits: slideView.displaySlide(at: 1, animated: false) @@ -291,6 +307,10 @@ final class PremiumBoardingFeaturesView: View { slideView.displaySlide(at: 15, animated: false) case .saved_tags: slideView.displaySlide(at: 16, animated: false) + case .last_seen: + slideView.displaySlide(at: 17, animated: false) + case .messages_privacy: + slideView.displaySlide(at: 18, animated: false) } needsLayout = true diff --git a/Telegram-Mac/PremiumLimitConfig.swift b/Telegram-Mac/PremiumLimitConfig.swift index cf5b77760..ff66dc625 100644 --- a/Telegram-Mac/PremiumLimitConfig.swift +++ b/Telegram-Mac/PremiumLimitConfig.swift @@ -25,6 +25,15 @@ final class PremiumPromoOrder { if let order = data["premium_promo_order"] as? [String] { premiumValues = order.compactMap { PremiumValue(rawValue: $0) } } + + #if DEBUG + if !premiumValues.contains(.last_seen) { + premiumValues.append(.last_seen) + } + if !premiumValues.contains(.messages_privacy) { + premiumValues.append(.messages_privacy) + } + #endif } } } diff --git a/Telegram-Mac/PremiumShowStatusController.swift b/Telegram-Mac/PremiumShowStatusController.swift index bb0bf38fd..421d4d9f9 100644 --- a/Telegram-Mac/PremiumShowStatusController.swift +++ b/Telegram-Mac/PremiumShowStatusController.swift @@ -347,7 +347,7 @@ func PremiumShowStatusController(context: AccountContext, peer: EnginePeer, sour }) close?() }, premium: { - showModal(with: PremiumBoardingController(context: context, source: .last_seen), for: context.window) + showModal(with: PremiumBoardingController(context: context, source: .last_seen, openFeatures: true), for: context.window) close?() }) diff --git a/Telegram-Mac/ReactionsWindowController.swift b/Telegram-Mac/ReactionsWindowController.swift index 85eced648..209a718db 100644 --- a/Telegram-Mac/ReactionsWindowController.swift +++ b/Telegram-Mac/ReactionsWindowController.swift @@ -204,7 +204,7 @@ final class ReactionsWindowController : NSObject { self.onClose = onClose self.name = name - self.emojies = .init(context, mode: context.peerId == peerId && tagsGloballyEnabled ? .defaultTags : .reactions, selectedItems: selectedItems, presentation: presentation) + self.emojies = .init(context, mode: context.peerId == peerId ? .defaultTags : .reactions, selectedItems: selectedItems, presentation: presentation) self.emojies.loadViewIfNeeded() super.init() diff --git a/Telegram-Mac/SelectivePrivacySettingsController.swift b/Telegram-Mac/SelectivePrivacySettingsController.swift index 496f623bb..a6bbcedcc 100644 --- a/Telegram-Mac/SelectivePrivacySettingsController.swift +++ b/Telegram-Mac/SelectivePrivacySettingsController.swift @@ -929,7 +929,7 @@ class SelectivePrivacySettingsController: TableViewController { return current } }, openPremium: { - showModal(with: PremiumBoardingController(context: context, source: .last_seen), for: context.window) + showModal(with: PremiumBoardingController(context: context, source: .last_seen, openFeatures: true), for: context.window) }) diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index 89c1debd3f10536c0cb8a70e6d231917fd9a4b13..7139fced638d52ad3639aff07a99a82d5699978d 100644 GIT binary patch delta 1114 zcmZ8gT}T{P6ux(7cXsB^b(pT>HZ{gwW!oCFsHzNFTYu>BzkQjE|v(9~238z|98&noRhSk5x% z9Pans^PTU`{1Th^D|Y^B$p0eirC&cvu4|9u(^Vmzotq;crbEN&5O>od&(vL%CAo(-*-)T(xj&*$85KNw>x;^#F2B|89avuoN0U)kLJkqSYBh6xV!Wpg7 zkXaNq&ZR^ELlZ=q+XwdSG;E}^q6lxMDDC-<^jL3P81Mzqw2OQ3&DS0inXHg*-+~yj z5fR+RNS0K-G0rWlHF^Xl{}8tOTUH$MZGfG)F9agjwMW6+_yh4S$Bq?Ih0iqe*Y6$FpLsOmepLm_GmZY8)5QdiLv zGs+zEGm@yb5ykYF9>n%363dDiVKlu;zMOi7R#sh4+WmK`9DmoT0^uq;R$EQC z9zUqt!?$CGdpjm09GAZ2>bf{sE|OAzfh_Sht+*E*TyA!b+!Tn`=#safu14R6cvwD# zXtfmBmEs!07Zr)xn3Op4E3r3RRDzhlubJ5UEnVx28d;3lVqKuU-DkO-HPU_-tF-q*Vs{|0GX3s73m*!FUoJw#+tT~XTl@CqP#(RRyoIRc$+VZe0W;5 zO+1=2b3SH&pj9^PCvmS`QEX&(ki+g>@D^f7Dp91D$TibaDSEn%H4)GNO}CGA6Ck|? z_7*F9w1&oGdI3@i`5KM%)AKT4?^}N#43@tjFCZZoh2W(kNQm~ HQ>*?9gUD=Z delta 265 zcmbR7+VI3>!wu);o3#|$wG<4v9(JHFat3Q5VHa?h|dnh9NVP?IHy=lpC`*BGW|gSr^xg({+vAR zdET7c^SrrU&6sZR5@h%jE|Kj9Pq`XIrtAIW;@fWZi_3#``;GrxO^nk6IJgC-yL@I9 zn(olZ!8ZNCb1tAFChn~5Zp_>)jMHB*bE`}*VBzkXp7)uPWBUPCZXM(4lkzwvrde@o zO`mXwlVy5AJrm#b57yio(@!~Yi?ml+ac{4(;!$Co9;404H(g){m(X+_TOPUXH*9$H L%%*QE;Ryl&;G9=( diff --git a/Telegram-Mac/tgs/menu/menu_boost.tgs b/Telegram-Mac/tgs/menu/menu_boost.tgs new file mode 100644 index 0000000000000000000000000000000000000000..81725bc683e78f8acd5946277fa33fc1099d38c3 GIT binary patch literal 724 zcmV;_0xSI=iwFpdq`PGR17dG)b963hb8l_{?N&>Vn=lamE0NCz*g$TlIo+ zpe);@2@e@|yQ?Vwz4I_BG^|vqd#SWaz}WN6H{%(@%t<_oTtvPOd@ZE7w<3>~6#HJ} zFmy14F#Z&|PGQ`+v28%o7CH{ZuIy_*EOBQ+VDbsjN>Lx@waWbXn zIGNI8PVlx8`Dh>N^Wxn8MSAsHe*Y=II#?uFrLU1-oh&k}%~ghHUzE4Z{V#9kR9erj zq<=2Q-3EJ!riIzNkMBQizSh;)+089}<~QEv#&!=q0Cw8k!@uAm187lH0eV?GE+2<2SbKcl!!lXVj|i zBNnaxs;!uX=ep;ZI*wVdG3{D$)l{>6uQosN%K=G{_OPaep&H6HNi?PLi!IyXzO#+M z49}ajwx>p_at)OWqN~a)vHLUG+pG3+;EG(0L_vgzYD#CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259033 + 259084 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/ColorPalette/Sources/ColorPalette/ColorPalette.swift b/packages/ColorPalette/Sources/ColorPalette/ColorPalette.swift index 71864c089..437d0d50a 100644 --- a/packages/ColorPalette/Sources/ColorPalette/ColorPalette.swift +++ b/packages/ColorPalette/Sources/ColorPalette/ColorPalette.swift @@ -11,154 +11,7 @@ import Colors -public class PeerNameColors: Equatable { - public enum Subject { - case background - case palette - case stories - } - - public struct Colors: Equatable { - public let main: NSColor - public let secondary: NSColor? - public let tertiary: NSColor? - - public init(main: NSColor, secondary: NSColor?, tertiary: NSColor?) { - self.main = main - self.secondary = secondary - self.tertiary = tertiary - } - - public init(main: NSColor) { - self.main = main - self.secondary = nil - self.tertiary = nil - } - - public init?(colors: [NSColor]) { - guard let first = colors.first else { - return nil - } - self.main = first - if colors.count == 3 { - self.secondary = colors[1] - self.tertiary = colors[2] - } else if colors.count == 2, let second = colors.last { - self.secondary = second - self.tertiary = nil - } else { - self.secondary = nil - self.tertiary = nil - } - } - } - - public static var defaultSingleColors: [Int32: Colors] { - return [ - 0: Colors(main: NSColor(rgb: 0xcc5049)), - 1: Colors(main: NSColor(rgb: 0xd67722)), - 2: Colors(main: NSColor(rgb: 0x955cdb)), - 3: Colors(main: NSColor(rgb: 0x40a920)), - 4: Colors(main: NSColor(rgb: 0x309eba)), - 5: Colors(main: NSColor(rgb: 0x368ad1)), - 6: Colors(main: NSColor(rgb: 0xc7508b)) - ] - } - - public static var defaultValue: PeerNameColors { - return PeerNameColors( - colors: defaultSingleColors, - darkColors: [:], - displayOrder: [5, 3, 1, 0, 2, 4, 6], - profileColors: [:], - profileDarkColors: [:], - profilePaletteColors: [:], - profilePaletteDarkColors: [:], - profileStoryColors: [:], - profileStoryDarkColors: [:], - profileDisplayOrder: [], - nameColorsChannelMinRequiredBoostLevel: [:] - ) - } - - public let colors: [Int32: Colors] - public let darkColors: [Int32: Colors] - public let displayOrder: [Int32] - - public let profileColors: [Int32: Colors] - public let profileDarkColors: [Int32: Colors] - public let profilePaletteColors: [Int32: Colors] - public let profilePaletteDarkColors: [Int32: Colors] - public let profileStoryColors: [Int32: Colors] - public let profileStoryDarkColors: [Int32: Colors] - public let profileDisplayOrder: [Int32] - public let nameColorsChannelMinRequiredBoostLevel: [Int32: Int32] - - - public init( - colors: [Int32: Colors], - darkColors: [Int32: Colors], - displayOrder: [Int32], - profileColors: [Int32: Colors], - profileDarkColors: [Int32: Colors], - profilePaletteColors: [Int32: Colors], - profilePaletteDarkColors: [Int32: Colors], - profileStoryColors: [Int32: Colors], - profileStoryDarkColors: [Int32: Colors], - profileDisplayOrder: [Int32], - nameColorsChannelMinRequiredBoostLevel: [Int32: Int32] - ) { - self.colors = colors - self.darkColors = darkColors - self.displayOrder = displayOrder - self.profileColors = profileColors - self.profileDarkColors = profileDarkColors - self.profilePaletteColors = profilePaletteColors - self.profilePaletteDarkColors = profilePaletteDarkColors - self.profileStoryColors = profileStoryColors - self.profileStoryDarkColors = profileStoryDarkColors - self.profileDisplayOrder = profileDisplayOrder - self.nameColorsChannelMinRequiredBoostLevel = nameColorsChannelMinRequiredBoostLevel - } - - public static func == (lhs: PeerNameColors, rhs: PeerNameColors) -> Bool { - if lhs.colors != rhs.colors { - return false - } - if lhs.darkColors != rhs.darkColors { - return false - } - if lhs.displayOrder != rhs.displayOrder { - return false - } - if lhs.profileColors != rhs.profileColors { - return false - } - if lhs.profileDarkColors != rhs.profileDarkColors { - return false - } - if lhs.profilePaletteColors != rhs.profilePaletteColors { - return false - } - if lhs.profilePaletteDarkColors != rhs.profilePaletteDarkColors { - return false - } - if lhs.profileStoryColors != rhs.profileStoryColors { - return false - } - if lhs.profileStoryDarkColors != rhs.profileStoryDarkColors { - return false - } - if lhs.profileDisplayOrder != rhs.profileDisplayOrder { - return false - } - if lhs.nameColorsChannelMinRequiredBoostLevel != rhs.nameColorsChannelMinRequiredBoostLevel { - return false - } - return true - } -} public final class InputViewTheme: Equatable { diff --git a/packages/ColorPalette/Sources/ColorPalette/PeerNamesColor.swift b/packages/ColorPalette/Sources/ColorPalette/PeerNamesColor.swift new file mode 100644 index 000000000..50c198b5f --- /dev/null +++ b/packages/ColorPalette/Sources/ColorPalette/PeerNamesColor.swift @@ -0,0 +1,160 @@ +// +// File.swift +// +// +// Created by Mikhail Filimonov on 01.02.2024. +// + +import Foundation +import Cocoa + +public class PeerNameColors: Equatable { + public enum Subject { + case background + case palette + case stories + } + + public struct Colors: Equatable { + public let main: NSColor + public let secondary: NSColor? + public let tertiary: NSColor? + + public init(main: NSColor, secondary: NSColor?, tertiary: NSColor?) { + self.main = main + self.secondary = secondary + self.tertiary = tertiary + } + + public init(main: NSColor) { + self.main = main + self.secondary = nil + self.tertiary = nil + } + + public init?(colors: [NSColor]) { + guard let first = colors.first else { + return nil + } + self.main = first + if colors.count == 3 { + self.secondary = colors[1] + self.tertiary = colors[2] + } else if colors.count == 2, let second = colors.last { + self.secondary = second + self.tertiary = nil + } else { + self.secondary = nil + self.tertiary = nil + } + } + } + + public static var defaultSingleColors: [Int32: Colors] { + return [ + 0: Colors(main: NSColor(rgb: 0xcc5049)), + 1: Colors(main: NSColor(rgb: 0xd67722)), + 2: Colors(main: NSColor(rgb: 0x955cdb)), + 3: Colors(main: NSColor(rgb: 0x40a920)), + 4: Colors(main: NSColor(rgb: 0x309eba)), + 5: Colors(main: NSColor(rgb: 0x368ad1)), + 6: Colors(main: NSColor(rgb: 0xc7508b)) + ] + } + + public static var defaultValue: PeerNameColors { + return PeerNameColors( + colors: defaultSingleColors, + darkColors: [:], + displayOrder: [5, 3, 1, 0, 2, 4, 6], + profileColors: [:], + profileDarkColors: [:], + profilePaletteColors: [:], + profilePaletteDarkColors: [:], + profileStoryColors: [:], + profileStoryDarkColors: [:], + profileDisplayOrder: [], + nameColorsChannelMinRequiredBoostLevel: [:], + nameColorsGroupMinRequiredBoostLevel: [:] + ) + } + + public let colors: [Int32: Colors] + public let darkColors: [Int32: Colors] + public let displayOrder: [Int32] + + public let profileColors: [Int32: Colors] + public let profileDarkColors: [Int32: Colors] + public let profilePaletteColors: [Int32: Colors] + public let profilePaletteDarkColors: [Int32: Colors] + public let profileStoryColors: [Int32: Colors] + public let profileStoryDarkColors: [Int32: Colors] + public let profileDisplayOrder: [Int32] + + public let nameColorsChannelMinRequiredBoostLevel: [Int32: Int32] + public let nameColorsGroupMinRequiredBoostLevel: [Int32: Int32] + + + public init( + colors: [Int32: Colors], + darkColors: [Int32: Colors], + displayOrder: [Int32], + profileColors: [Int32: Colors], + profileDarkColors: [Int32: Colors], + profilePaletteColors: [Int32: Colors], + profilePaletteDarkColors: [Int32: Colors], + profileStoryColors: [Int32: Colors], + profileStoryDarkColors: [Int32: Colors], + profileDisplayOrder: [Int32], + nameColorsChannelMinRequiredBoostLevel: [Int32: Int32], + nameColorsGroupMinRequiredBoostLevel: [Int32: Int32] + ) { + self.colors = colors + self.darkColors = darkColors + self.displayOrder = displayOrder + self.profileColors = profileColors + self.profileDarkColors = profileDarkColors + self.profilePaletteColors = profilePaletteColors + self.profilePaletteDarkColors = profilePaletteDarkColors + self.profileStoryColors = profileStoryColors + self.profileStoryDarkColors = profileStoryDarkColors + self.profileDisplayOrder = profileDisplayOrder + self.nameColorsChannelMinRequiredBoostLevel = nameColorsChannelMinRequiredBoostLevel + self.nameColorsGroupMinRequiredBoostLevel = nameColorsGroupMinRequiredBoostLevel + } + + + public static func == (lhs: PeerNameColors, rhs: PeerNameColors) -> Bool { + if lhs.colors != rhs.colors { + return false + } + if lhs.darkColors != rhs.darkColors { + return false + } + if lhs.displayOrder != rhs.displayOrder { + return false + } + if lhs.profileColors != rhs.profileColors { + return false + } + if lhs.profileDarkColors != rhs.profileDarkColors { + return false + } + if lhs.profilePaletteColors != rhs.profilePaletteColors { + return false + } + if lhs.profilePaletteDarkColors != rhs.profilePaletteDarkColors { + return false + } + if lhs.profileStoryColors != rhs.profileStoryColors { + return false + } + if lhs.profileStoryDarkColors != rhs.profileStoryDarkColors { + return false + } + if lhs.profileDisplayOrder != rhs.profileDisplayOrder { + return false + } + return true + } +} diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index f1be94a74..7e343df65 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -713,6 +713,8 @@ public final class L10n { public static var boostGiftStartConfirmationText: String { return L10n.tr("Localizable", "BoostGift.StartConfirmation.Text") } /// Start Giveaway public static var boostGiftStartConfirmationTitle: String { return L10n.tr("Localizable", "BoostGift.StartConfirmation.Title") } + /// Boost this group to send messages + public static var boostGroupChatInputAction: String { return L10n.tr("Localizable", "BoostGroup.ChatInput.Action") } /// To boost **%1$@**, reassign a previous boost or gift **Telegram Premium** to a friend to get **%2$@** additional boosts public static func boostReassignInfo(_ p1: String, _ p2: String) -> String { return L10n.tr("Localizable", "BoostReassign.Info", p1, p2) @@ -1401,8 +1403,10 @@ public final class L10n { public static var channelBoostPrivateError: String { return L10n.tr("Localizable", "Channel.Boost.PrivateError") } /// Get Boosts Via Gifts public static var channelBoostsStatsGetBoostsViaGifts: String { return L10n.tr("Localizable", "Channel.BoostsStats.GetBoostsViaGifts") } - /// Get more boosts for your channel by gifting Premium to your subscribers. + /// Get more boosts for your channel by gifting Premium to your subscribers. public static var channelBoostsStatsGetBoostsViaGiftsInfo: String { return L10n.tr("Localizable", "Channel.BoostsStats.GetBoostsViaGifts.Info") } + /// Get more boosts for your group by gifting Premium to your subscribers. + public static var channelBoostsStatsGetBoostsViaGiftsInfoGroup: String { return L10n.tr("Localizable", "Channel.BoostsStats.GetBoostsViaGifts.Info.Group") } /// Select a giveaway you already paid for to set it up. public static var channelBoostsStatsPrepaidInfo: String { return L10n.tr("Localizable", "Channel.BoostsStats.Prepaid.Info") } /// %d-months subscriptions @@ -1895,6 +1899,8 @@ public final class L10n { public static func channelBoostBoostedChannelReachedLevel(_ p1: String, _ p2: String) -> String { return L10n.tr("Localizable", "ChannelBoost.BoostedChannelReachedLevel", p1, p2) } + /// Boost Group + public static var channelBoostBoostGroup: String { return L10n.tr("Localizable", "ChannelBoost.BoostGroup") } /// Use Emoji Statuses public static var channelBoostEmojiStatus: String { return L10n.tr("Localizable", "ChannelBoost.EmojiStatus") } /// Enable Colors @@ -1957,6 +1963,8 @@ public final class L10n { public static func channelBoostHelpUpgradeChannelText(_ p1: String, _ p2: String, _ p3: String) -> String { return L10n.tr("Localizable", "ChannelBoost.HelpUpgradeChannelText", p1, p2, p3) } + /// Help Upgrade This Group + public static var channelBoostHelpUpgradeGroup: String { return L10n.tr("Localizable", "ChannelBoost.HelpUpgradeGroup") } /// Increase Story Limit public static var channelBoostIncreaseLimit: String { return L10n.tr("Localizable", "ChannelBoost.IncreaseLimit") } /// Your channel needs %1$@ to post %2$@.\n\nAsk your **Premium** subscribers to boost your channel with this link: @@ -2075,6 +2083,8 @@ public final class L10n { public static var channelBoostErrorPremiumNeededTitle: String { return L10n.tr("Localizable", "ChannelBoost.Error.PremiumNeededTitle") } /// Read More public static var channelBoostErrorPremiumNeededTextOK: String { return L10n.tr("Localizable", "ChannelBoost.Error.PremiumNeededText.OK") } + /// Voice-To-Text Conversion + public static var channelBoostTableAudioTranscription: String { return L10n.tr("Localizable", "ChannelBoost.Table.AudioTranscription") } /// %d public static func channelBoostTableCustomReactionsCountable(_ p1: Int) -> String { return L10n.tr("Localizable", "ChannelBoost.Table.CustomReactions_countable", p1) @@ -2105,6 +2115,8 @@ public final class L10n { } /// Custom Channel Background public static var channelBoostTableCustomWallpaper: String { return L10n.tr("Localizable", "ChannelBoost.Table.CustomWallpaper") } + /// Custom Emoji Pack + public static var channelBoostTableEmojiPack: String { return L10n.tr("Localizable", "ChannelBoost.Table.EmojiPack") } /// 1000+ Emoji Statuses public static var channelBoostTableEmojiStatus: String { return L10n.tr("Localizable", "ChannelBoost.Table.EmojiStatus") } /// Custom Logo for Links and Quotes @@ -2279,6 +2291,10 @@ public final class L10n { public static func channelBoostTableWallpaperZero(_ p1: Int) -> String { return L10n.tr("Localizable", "ChannelBoost.Table.Wallpaper_zero", p1) } + /// Boost Channel + public static var channelBoostTitleChannel: String { return L10n.tr("Localizable", "ChannelBoost.Title.Channel") } + /// Boost Group + public static var channelBoostTitleGroup: String { return L10n.tr("Localizable", "ChannelBoost.Title.Group") } /// Channel Info public static var channelEventFilterChannelInfo: String { return L10n.tr("Localizable", "ChannelEventFilter.ChannelInfo") } /// Deleted Messages @@ -7781,6 +7797,8 @@ public final class L10n { public static var groupUsersTooMuchError: String { return L10n.tr("Localizable", "Group.UsersTooMuchError") } /// Change Group Info public static var groupEditAdminPermissionChangeInfo: String { return L10n.tr("Localizable", "Group.EditAdmin.Permission.ChangeInfo") } + /// All members will be able to use these emoji in the group, even if they don't have Telegram Premium.\n\nYou can create your own custom emoji set using the [@stickers](stickers) bot. + public static var groupEmojiPackCreateInfo: String { return L10n.tr("Localizable", "Group.EmojiPack.CreateInfo") } /// CHOOSE EMOJI PACK public static var groupEmojiPackHeader: String { return L10n.tr("Localizable", "Group.EmojiPack.Header") } /// Group Emoji Pack @@ -7953,6 +7971,10 @@ public final class L10n { public static var groupStickersEmptyDesc: String { return L10n.tr("Localizable", "GroupStickers.EmptyDesc") } /// No such sticker set found public static var groupStickersEmptyHeader: String { return L10n.tr("Localizable", "GroupStickers.EmptyHeader") } + /// Try again or choose from the list below + public static var groupStickersEmptyDescEmoji: String { return L10n.tr("Localizable", "GroupStickers.EmptyDesc.Emoji") } + /// No such emoji set found + public static var groupStickersEmptyHeaderEmoji: String { return L10n.tr("Localizable", "GroupStickers.EmptyHeader.Emoji") } /// No groups in common public static var groupsInCommonEmpty: String { return L10n.tr("Localizable", "GroupsInCommon.Empty") } /// View @@ -10195,6 +10217,8 @@ public final class L10n { public static var peerInfoAboutPlaceholder: String { return L10n.tr("Localizable", "PeerInfo.About.Placeholder") } /// Add public static var peerInfoActionAddMembers: String { return L10n.tr("Localizable", "PeerInfo.Action.AddMembers") } + /// Boost Group + public static var peerInfoActionBoostGroup: String { return L10n.tr("Localizable", "PeerInfo.Action.BoostGroup") } /// Call public static var peerInfoActionCall: String { return L10n.tr("Localizable", "PeerInfo.Action.Call") } /// Delete @@ -10861,6 +10885,14 @@ public final class L10n { public static var premiumBoardingGotInfo: String { return L10n.tr("Localizable", "Premium.Boarding.Got.Info") } /// You are all set! public static var premiumBoardingGotTitle: String { return L10n.tr("Localizable", "Premium.Boarding.Got.Title") } + /// View the last seen and read times of others even if you hide yours. + public static var premiumBoardingLastSeenInfo: String { return L10n.tr("Localizable", "Premium.Boarding.LastSeen.Info") } + /// Last Seen Times + public static var premiumBoardingLastSeenTitle: String { return L10n.tr("Localizable", "Premium.Boarding.LastSeen.Title") } + /// Restrict people you don't know from sending you messages. + public static var premiumBoardingMessagePrivacyInfo: String { return L10n.tr("Localizable", "Premium.Boarding.MessagePrivacy.Info") } + /// Message Privacy + public static var premiumBoardingMessagePrivacyTitle: String { return L10n.tr("Localizable", "Premium.Boarding.MessagePrivacy.Title") } /// No more ads in public channels where Telegram sometimes shows ads. public static var premiumBoardingNoAdsInfo: String { return L10n.tr("Localizable", "Premium.Boarding.NoAds.Info") } /// No Ads @@ -13719,6 +13751,12 @@ public final class L10n { public static var statsBoostsPremiumSubscribers: String { return L10n.tr("Localizable", "Stats.Boosts.PremiumSubscribers") } /// Show More public static var statsBoostsShowMore: String { return L10n.tr("Localizable", "Stats.Boosts.ShowMore") } + /// Your group is currently boosted by these users. + public static var statsBoostsBoostersInfoGroup: String { return L10n.tr("Localizable", "Stats.Boosts.BoostersInfo.Group") } + /// Share this link with your group members to get more boosts. + public static var statsBoostsLinkInfoGroup: String { return L10n.tr("Localizable", "Stats.Boosts.LinkInfo.Group") } + /// No users currently boost your group + public static var statsBoostsNoBoostersYetGroup: String { return L10n.tr("Localizable", "Stats.Boosts.NoBoostersYet.Group") } /// Actions public static var statsGroupTopAdminActions: String { return L10n.tr("Localizable", "Stats.GroupTopAdmin.Actions") } /// Promote diff --git a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift index 40b730538..2d7c205a8 100644 --- a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift +++ b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift @@ -6544,6 +6544,19 @@ public final class TelegramIconsTheme { return image } } + public var profile_boost: CGImage { + if let image = cached.with({ $0["profile_boost"] }) { + return image + } else { + let image = _profile_boost() + _ = cached.modify { current in + var current = current + current["profile_boost"] = image + return current + } + return image + } + } public var chat_quiz_explanation: CGImage { if let image = cached.with({ $0["chat_quiz_explanation"] }) { return image @@ -10145,6 +10158,32 @@ public final class TelegramIconsTheme { return image } } + public var channel_feature_emoji_pack: CGImage { + if let image = cached.with({ $0["channel_feature_emoji_pack"] }) { + return image + } else { + let image = _channel_feature_emoji_pack() + _ = cached.modify { current in + var current = current + current["channel_feature_emoji_pack"] = image + return current + } + return image + } + } + public var channel_feature_voice_to_text: CGImage { + if let image = cached.with({ $0["channel_feature_voice_to_text"] }) { + return image + } else { + let image = _channel_feature_voice_to_text() + _ = cached.modify { current in + var current = current + current["channel_feature_voice_to_text"] = image + return current + } + return image + } + } public var chat_hidden_author: CGImage { if let image = cached.with({ $0["chat_hidden_author"] }) { return image @@ -10688,6 +10727,7 @@ public final class TelegramIconsTheme { private let _profile_unblock: ()->CGImage private let _profile_translate: ()->CGImage private let _profile_join_channel: ()->CGImage + private let _profile_boost: ()->CGImage private let _chat_quiz_explanation: ()->CGImage private let _chat_quiz_explanation_bubble_incoming: ()->CGImage private let _chat_quiz_explanation_bubble_outgoing: ()->CGImage @@ -10965,6 +11005,8 @@ public final class TelegramIconsTheme { private let _channel_feature_reaction: ()->CGImage private let _channel_feature_status: ()->CGImage private let _channel_feature_stories: ()->CGImage + private let _channel_feature_emoji_pack: ()->CGImage + private let _channel_feature_voice_to_text: ()->CGImage private let _chat_hidden_author: ()->CGImage private let _chat_my_notes: ()->CGImage private let _premium_required_forward: ()->CGImage @@ -11473,6 +11515,7 @@ public final class TelegramIconsTheme { profile_unblock: @escaping()->CGImage, profile_translate: @escaping()->CGImage, profile_join_channel: @escaping()->CGImage, + profile_boost: @escaping()->CGImage, chat_quiz_explanation: @escaping()->CGImage, chat_quiz_explanation_bubble_incoming: @escaping()->CGImage, chat_quiz_explanation_bubble_outgoing: @escaping()->CGImage, @@ -11750,6 +11793,8 @@ public final class TelegramIconsTheme { channel_feature_reaction: @escaping()->CGImage, channel_feature_status: @escaping()->CGImage, channel_feature_stories: @escaping()->CGImage, + channel_feature_emoji_pack: @escaping()->CGImage, + channel_feature_voice_to_text: @escaping()->CGImage, chat_hidden_author: @escaping()->CGImage, chat_my_notes: @escaping()->CGImage, premium_required_forward: @escaping()->CGImage @@ -12257,6 +12302,7 @@ public final class TelegramIconsTheme { self._profile_unblock = profile_unblock self._profile_translate = profile_translate self._profile_join_channel = profile_join_channel + self._profile_boost = profile_boost self._chat_quiz_explanation = chat_quiz_explanation self._chat_quiz_explanation_bubble_incoming = chat_quiz_explanation_bubble_incoming self._chat_quiz_explanation_bubble_outgoing = chat_quiz_explanation_bubble_outgoing @@ -12534,6 +12580,8 @@ public final class TelegramIconsTheme { self._channel_feature_reaction = channel_feature_reaction self._channel_feature_status = channel_feature_status self._channel_feature_stories = channel_feature_stories + self._channel_feature_emoji_pack = channel_feature_emoji_pack + self._channel_feature_voice_to_text = channel_feature_voice_to_text self._chat_hidden_author = chat_hidden_author self._chat_my_notes = chat_my_notes self._premium_required_forward = premium_required_forward diff --git a/submodules/telegram-ios b/submodules/telegram-ios index c9814634d..71e9b3ab8 160000 --- a/submodules/telegram-ios +++ b/submodules/telegram-ios @@ -1 +1 @@ -Subproject commit c9814634d724cf6f4d58f001a49bfd4119dab1fb +Subproject commit 71e9b3ab8748d26db8cf148e986bc7f936549348 diff --git a/tools/generate-images.swift b/tools/generate-images.swift index 32d93c459..28e0d4742 100644 --- a/tools/generate-images.swift +++ b/tools/generate-images.swift @@ -527,6 +527,7 @@ func initialize() -> [String] { array.append("profile_unblock") array.append("profile_translate") array.append("profile_join_channel") + array.append("profile_boost") array.append("chat_quiz_explanation") array.append("chat_quiz_explanation_bubble_incoming") @@ -912,7 +913,9 @@ func initialize() -> [String] { array.append("channel_feature_reaction") array.append("channel_feature_status") array.append("channel_feature_stories") - + array.append("channel_feature_emoji_pack") + array.append("channel_feature_voice_to_text") + array.append("chat_hidden_author") array.append("chat_my_notes") From e9cd49c9e209d65bdd995a375b36006560f9468d Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 2 Feb 2024 17:45:51 +0400 Subject: [PATCH 02/50] bug fixes --- Telegram-Mac/Appearance.swift | 4 + .../Icon_Boost_Boost.imageset/Contents.json | 22 ++ .../ic_pf_boost_plus.png | Bin 0 -> 934 bytes .../ic_pf_boost_plus@2x.png | Bin 0 -> 1649 bytes .../Icon_Boost_Gift.imageset/Contents.json | 22 ++ .../Icon_Boost_Gift.imageset/ic_pf_gift.png | Bin 0 -> 983 bytes .../ic_pf_gift@2x.png | Bin 0 -> 1852 bytes .../Contents.json | 22 ++ .../boosts_14.png | Bin 0 -> 753 bytes .../boosts_14@2x.png | Bin 0 -> 1018 bytes .../Contents.json | 22 ++ .../boost_14.png | Bin 0 -> 688 bytes .../boost_14@2x.png | Bin 0 -> 885 bytes .../Icon_Boost_Info.imageset/Contents.json | 22 ++ .../Icon_Boost_Info.imageset/ic_pf_info.png | Bin 0 -> 958 bytes .../ic_pf_info@2x.png | Bin 0 -> 1746 bytes .../menu_boost_plus.imageset/Contents.json | 22 ++ .../menu_boost_plus.png | Bin 0 -> 329 bytes .../menu_boost_plus@2x.png | Bin 0 -> 583 bytes .../BoostChannelModalController.swift | 164 +++++++++----- Telegram-Mac/BoostFeatureRowItem.swift | 30 ++- .../ChannelBoostStatsController.swift | 173 ++++++++++++-- .../ChannelPermissionsController.swift | 27 ++- Telegram-Mac/ChatController.swift | 3 +- Telegram-Mac/ChatInterfaceInteraction.swift | 1 - Telegram-Mac/ChatInterfaceState.swift | 2 +- Telegram-Mac/ChatRowItem.swift | 2 +- Telegram-Mac/CustomizeAccountController.swift | 163 ------------- Telegram-Mac/EmojiesController.swift | 21 ++ Telegram-Mac/FastSettings.swift | 2 +- Telegram-Mac/GiveawayModalController.swift | 33 ++- Telegram-Mac/GroupEmojiPackController.swift | 34 ++- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/LottieLocalAnimations.swift | 3 +- Telegram-Mac/MessagesPrivacyController.swift | 4 +- Telegram-Mac/PeerInfoHeadItem.swift | 21 +- Telegram-Mac/PremiumBoardingController.swift | 20 +- .../PremiumBoardingFeaturesController.swift | 6 +- Telegram-Mac/PremiumLimitConfig.swift | 4 +- Telegram-Mac/SelectColorController.swift | 191 ++++++++++++++-- Telegram-Mac/SelectSizeRowItem.swift | 19 +- Telegram-Mac/en.lproj/Localizable.strings | Bin 816536 -> 826652 bytes Telegram-Mac/tgs/menu/menu_boost_plus.tgs | Bin 0 -> 1201 bytes Telegram.xcodeproj/project.pbxproj | 8 +- TelegramShare/Info.plist | 2 +- .../Sources/Localization/Localizable.swift | 214 +++++++++++++++--- .../Sources/TelegramIconsTheme.swift | 48 ++++ tools/generate-images.swift | 6 + 48 files changed, 999 insertions(+), 340 deletions(-) create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Boost.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Boost.imageset/ic_pf_boost_plus.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Boost.imageset/ic_pf_boost_plus@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Gift.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Gift.imageset/ic_pf_gift.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Gift.imageset/ic_pf_gift@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Multiple.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Multiple.imageset/boosts_14.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Multiple.imageset/boosts_14@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Single.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Single.imageset/boost_14.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Single.imageset/boost_14@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Info.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Info.imageset/ic_pf_info.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Boost_Info.imageset/ic_pf_info@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/menu_boost_plus.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/menu_boost_plus.imageset/menu_boost_plus.png create mode 100644 Telegram-Mac/Assets.xcassets/menu_boost_plus.imageset/menu_boost_plus@2x.png delete mode 100644 Telegram-Mac/CustomizeAccountController.swift create mode 100644 Telegram-Mac/tgs/menu/menu_boost_plus.tgs diff --git a/Telegram-Mac/Appearance.swift b/Telegram-Mac/Appearance.swift index e778c9b81..c49e45d23 100644 --- a/Telegram-Mac/Appearance.swift +++ b/Telegram-Mac/Appearance.swift @@ -2611,6 +2611,10 @@ private func generateIcons(from palette: ColorPalette, bubbled: Bool) -> Telegra profile_translate: { generateProfileIcon(NSImage(named: "Icon_Profile_Translate")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, profile_join_channel: { generateProfileIcon(NSImage(named: "Icon_Profile_JoinChannel")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, profile_boost: { generateProfileIcon(NSImage(named: "Icon_Profile_Boost")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + stats_boost_boost: { generateProfileIcon(NSImage(named: "Icon_Boost_Boost")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + stats_boost_giveaway: { generateProfileIcon(NSImage(named: "Icon_Boost_Gift")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + stats_boost_info: { generateProfileIcon(NSImage(named: "Icon_Boost_Info")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + chat_quiz_explanation: { NSImage(named: "Icon_QuizExplanation")!.precomposed(palette.accentIcon) }, chat_quiz_explanation_bubble_incoming: { NSImage(named: "Icon_QuizExplanation")!.precomposed(palette.accentIconBubble_incoming) }, chat_quiz_explanation_bubble_outgoing: { NSImage(named: "Icon_QuizExplanation")!.precomposed(palette.accentIconBubble_outgoing) }, diff --git a/Telegram-Mac/Assets.xcassets/Icon_Boost_Boost.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Boost_Boost.imageset/Contents.json new file mode 100644 index 000000000..cd0149aa6 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Boost_Boost.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_pf_boost_plus.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_pf_boost_plus@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Boost_Boost.imageset/ic_pf_boost_plus.png b/Telegram-Mac/Assets.xcassets/Icon_Boost_Boost.imageset/ic_pf_boost_plus.png new file mode 100644 index 0000000000000000000000000000000000000000..a4085344ca67a813cd6fe679e5820eb9884cf2ca GIT binary patch literal 934 zcmV;X16lluP)LGn!xUe{6b6V| zcMqhG1v=E~VkM?JF}0iG2oVM%uG*-YKo<57*Cgr!E)bYh6}nsCmF$qPfkBV$BBm|E zjhJk^X}iF_{N}9JHY*rmD7u?3m!VDilf;EM3^O!wMkb#ZFLk5zu)niNObX4jMBqQ;6Mh-v_OE@WlrPqI316fiP_J&5f@`g zFOc{FGyFu0LJQ>HlW4;Y5pG4SmOE3}wXr8bDln(mlF7>P3Y@@(h4#N%pc|LMrj2sP zwkuDEe}sxkmTGd?;gO?FBNiQ0Ji-=wrbpsJ*b^1PsQ@l`M$03Wc4LK2dm{#GXPz7h z6bL;Ue*tV*l4*-bXTJ1=CrJQP*z4bx#B@Vi3E0`D8*pmqc6>q0n}iv^$h7B`?zC8? z8Zd)hN_Xd7eaWf8oWy;Ap`{v0f>YOL`82tF)iWu@@LdPHA#~*Y5oXLi^@a7}54xwG zd01_qWij$@(MNP?8^fHK-OLPbh~b{U4eG%=n42I&kieegShB5-r8*bEjv0@^8BFJK z9*-sguS!k4E6FXgxU(s=w%`g>V%pB%xap zQL8ay?he>=+9$4C;aLqElVD_$E}{9PiF1L6J(M`=5~~0t5tt=Sx)*KTf(xR20J5IA z4^=*DdwVA#=bm@bck+{?1$@1FF5zdtZKNMWpY?cJR*j0Y(Nj7;0`-RQA z(Rq7Yo9c$2u#Z4Mhk4y?;+X9M86LDe?afOWWsGSU9p{Tgo2XYrza(cYs2nU*Wk0CX zH0iOiTD3|Fm7ANHq7(iQyiy1yHYofJ!6Ig~f2Ss#;olJd0l+Z|-%-J%F#rGn07*qo IM6N<$f}6gr1ONa4 literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Boost_Boost.imageset/ic_pf_boost_plus@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Boost_Boost.imageset/ic_pf_boost_plus@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7dcb44effdc8e2b8f6e712ea1e71558b651d7342 GIT binary patch literal 1649 zcmV-%29EiOP)}2rNfnHiDHRkQ@Q)w=JY9L)BW^H%k!+MX+)NCIi?Awv_?(Q%qNw^N$Mqs4H zyb||lnf3(a(Kgqj{cc6HY$TnAIMkz3q6L~4&=28~NIH#hDTZj6W(Dl4Gy3}(&Dpl5 zk>C+|DMo0BW&|9l5RbT}5!c0Ta8J|vQxnrVz_whd9*H;EA@P`FGBcCqjM<9%Bam&~ zBTZLDwr-aZxW*Up2{oxFAWzvj{-#Su*bk;OrBCWi>FNR+@xH@fM^P6^BK}%c#A*WW zuXquaMJzAi{)-n;NyKsjz8vu)N{d)7j4=))#LK}rNscrMC>HQseZ~HG+bSICFs*>i zQ`iKWw|L1Y%2=+%b>7|i0-E$};?aVDD`sR5-{;xG{3$%g*H`g09i8XrkS(B@y2p6M zzqvSr?EU z2XF?zkW}ADt|W(WLoYcB*9DyGHN6miF~nan4>(XRW71A?6eg#zZ?^9ywfz3W`4`-a zUs(xwp_;7IHUa0OgVe1H=UGulmuZ)*1XTD9KC#fre7Zm$=ne+AELNk?2*`#}dF4FT z?Oq(^#VqMY&cq1#!>fXjd=6Q%P+lx-m=+a80{Sw2xp9c6ZXRwu@H%2$z%G1Xv0>^m z?mciEJ?MhvTn^!$(WYf1je8FqCuHpc`c{2-Q(4(Y8Xr7x9I}G}z40#Gvw7-9__w?k z1|Eb3Hx-(F{3g>QMR!S+^e{-Tg$O8;G*W# zErM6mRV9bi2nVpsx58|r3{gw*Dy+F*uBKZzM=YR(Jx3zZ1nEYnGq_=9djy`6mzyXImOL>l~vnM(BNKA?(0Y?3O-wKIdvorOSLLvszHNd$y&0bq5#q z9CC=93qQao{8%t8hl1o(&bRc)ulKW=hd@tZpHTP@yF!5R0r}xEc{QIfBQPGS_}V3Q zfN8KS9ZL9uS_;Uain6plwjPqTGYyt~#lfw$1iXOvdI$SL_hp+* z`e;%)-a79KmtvY3s?@pdT&{e9X>CK^%b9|1L^jh<0xu7S*^wdRGV# z$Bx9&Cif)XN!A_tRkIg4t9($sJBwd{ zUueG^%d~ForTPer<5w}NE;l;TsCney?F3{vZx%avgb$Np47Y8!+g?D%*hN0Vk?WdD zIrx~!NUYsVuBh7 ztE(f~z$3cU>vS&0oP_#Ku#Y|>k(hQe6Uf5;;*vz2!UY0jQ=yv$UdS#9i)xJ6CKCGe zaV^GG7gY=F$s1>*wqC&y197(gbQyX|zmvESyCKE_+BD^})e~On<&XHeLvc(?4M+*k z1@g3Vhh4)1qMyCQ@)V!T6Z*czOCoWGFJhQC7Xl+0sHp`4yeu=a$7XJiiz((fS5MrC zZunMxIBYy`+tSFiDD(*G(fbX1#uUdoWEz+1Rj-5=RBV5iV|vO**upPJqo@V;tlSMK3fsD_z1A4{e{sg)oP0Zhh+8!w;|*mZ4uLqL&DaZSn~%k5nGQ zoN47*pQbcjBQQrOlQ2Ud^l3r@xPrrjQ1@%Y4TtO`0bIbLiX#C#93Q)(At+`^)I-O> zp8N%V$+Gn#Pv=0i=@}7hoTGpbd&Xc}Ofy@0qFA@5;vC!F9f^9UY84%3^g1u4KAvia zwn1Ca2;5t)mOG`ZV7<^NrpqI`E-tyoCTPz*5;w!PT*&v?gM-4aE|zQwhnX1LlB%hj zaA2k5CUjeJz^mx9y32dh-}{{WvF2dk-W^sJnGaQ=u(X z@O!G_7Y^*uJqc^eS2H8yZgtjni@wJeS5I^oMz1vPjl1&BfWS9pb4C8)hMa_La=FfZ zAcyf(d^~|E>}+2}vZzVUB^N4Pybi|sl5J6w94G^S4Yruj=*?@Hg+T3fb&4F<^m%IY z#&Y34Q1z&}!?{iSJl||DbN&7lLzIC-0JPO-FWt> zs?PB$yXubY?g?C@_p#3^i!f{w$gt6-H2jhRvpYs)M$O_T(K_rl(Lbq@G0IX_VAC}l zF|MhHHVX+mfkkw|AJWlbR>=h`e?zbc-dUI`hpYKF#D78mA3?+Vq{ILK002ovPDHLk FV1h=U$3y@C literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Boost_Gift.imageset/ic_pf_gift@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Boost_Gift.imageset/ic_pf_gift@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..752d66232cf192d5d7c8fe9a42a1a17fd3eb36a0 GIT binary patch literal 1852 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 literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Multiple.imageset/boosts_14@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Multiple.imageset/boosts_14@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f500c6175d953b1c751f5af3b43c7b460d0fda69 GIT binary patch literal 1018 zcmVU>h*?d$b`aT5T$j!+ zmv){4Q-A$k)t||l8vWnYL5&#};E{=yZHo!sB_1(oDdMW&yvxUe(=MMoo|R;r_?B4G z3TlS9Ni3&3^n3@Gie;^}gV^4m)mN9r9O_ZbVG|}8icnF31O?%L{L_vm;#3HD6?Va!XbBie+=k*;rwF^@ErnnmGiG7&g~k7pMfj= z2Nx-7yi*22%K!iXIAvH#W=%~1DgXcg2mk?xX#fNO00031000^Q000000-yo_1ONa4 z0RR917@z|H1ONa40RR918~^|S05pB^R{#J3{7FPXR7efgQ@v^yK@{DDhzL={pE89_ zum~wc1W_ab34tK^625?as+5VKFOViSwxYg(rb%HyQrH-*{E7AnV$^eH@12~vJNd>& zAp>{s`MWc_`|a0tvc?SmW*-;hZy`6BMm-!k@YY=KiSkUZvxY6#8zkw*vN{U9Fw=XY z9KV2KznBzU?wLUGyJ7b9AfHWOx;UKVw2AC_?Hlr*jR+Qkka>LZHxT4;Q#KHJvCl^^ zhKrj8PfvuN)`ayKa?W@39Hrbw`sVNH(9~}hc3y-@xYM$zk&E>|;oWdpl*ah+JtL~DPeIH4JdA17 zO%!+Y0_^-Bq^D5N%fq)#i981h7-C?D1S|4hmFpv5JvwrU_V69rRZ$}s>!iUM-0%U4 zk5pSNzc!P#6(IlVR0WV3SGkW3raU_;iqY`>#iI#=YERu5nCIozi68;9W&>? z#p-RdGW-GBq{TNHzQyW|uT1ZlSU=O2U*A>K2Xep<;A`R$w)_wu(pQjp*ITHGq20Y~ o_?3}TnghL4N(F2AMfjhRKUZ46l^|CV^8f$<07*qoM6N<$g0srYL;wH) literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Single.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Single.imageset/Contents.json new file mode 100644 index 000000000..da83642bc --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Single.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "boost_14.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "boost_14@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Single.imageset/boost_14.png b/Telegram-Mac/Assets.xcassets/Icon_Boost_Indicator_Single.imageset/boost_14.png new file mode 100644 index 0000000000000000000000000000000000000000..d9b0b4121402d2888e1c5b5315fec42fa0db3500 GIT binary patch literal 688 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CH!N$PAn6v9_6_6^;baoENc6N8p&&e+eE=WvH zb;&F)$VsdWFlJ!Tm{>YtulL~qk)!^rgDzNg@X5*;bvj?U#8p<9p)KNS5W>1Tcvc-} zZjFj&P@!L&Q@k_gfOLSQ z_BSStm(wQt99gpU%C0BX&F%AoSEX%Qu}FvO-{0M@KVSTM&%YzmcXyefQVfA3#$?3EOIFtKxLZa3#WRXM{9%}1*q>^XhB>9XE@>v>aC=KPoz zafvIdZHK4Ma_cWO?9;na*KTdTac}6jqby3J7duVSQNt=W?HYpKeij zLV#XBlg=p@PeI{LM?POzm?&ga$l>^E!j9tCb^L38pUlmDFFGT);GF)gTTaXta~Dp& zdiz!di-Z(I-YXx|5cgLLzdb8D@SD?M22A z2+sm$IGYb7>BZ8`4x~1Dx;Tbth$bgEFiREeW9bRyNHTD)jB)s7H>KC>Z&TsShMxZh ze15--ckytCNPB3vm6a}dz^I@mr@=J!S8_0)k#veUmj&;WgF1a~(*zld;ut@gr#MPT zFKHG$=D4hRg5`z<&SK3Qof~*O754E9A8>7E`l=w%ye8!EQVz9soGCTzNfVMLJYL!m z!PBvEmkOH(V^Bw*|D#I`BJ9io3QbC~OExm-I5jZ19#>ZgQ&`Yvk;xDwtgw~;tMEaP OFFalST-G@yGywp*mU>h*?d$b`aT5T$j!+ zmv){4Q-A$k)t||l8vWnYL5&#};E{=yZHo!sB_1(oDdMW&yvxUe(=MMoo|R;r_?B4G z3TlS9Ni3&3^n3@Gie;^}gV^4m)mN9r9O_ZbVG|}8icnF31O?%L{L_vm;#3HD6?Va!XbBie+=k*;rwF^@ErnnmGiG7&g~k7pMfj= z2Nx-7yi*22%K!iXIAvH#W=%~1DgXcg2mk?xX#fNO00031000^Q000000-yo_1ONa4 z0RR917@z|H1ONa40RR918~^|S05pB^R{#J3cS%G+R7efoRJ%$9K@eO;Q1HOV511-O zV&sVmg7_aMeuZ$UkMzJy@Dsd0&`bmcF%mN$Xke-+KB|^!ZD(huXWqJlnmNBr#KVkfFzhp54YBM329Q*B+{c0N1F+sSO-9Yai>Wh{=pF% zFLSDgD=2`M5E#Q&eIoV{=OJXvDZe1JzHwp}n1iBFj9HaWuJFSW3ePTEvgn_=@(xm4 z8v>d2SjFVMPQjiNXLV>O@v*eivww?Xu0J~c@f~U>mEWrTPdbhjZ%h-xP7+A%A>u__ z`rF|$M@-EwB6j$o#|6-tAoFj)S)Ak*ao&p)wO}f*bf=pQMqqZO%-Lk2TMd?A#Gje$ zobzL@Ie!ePu@7=HAov8fjCfYuy|-3WHRZkIrPRB&@`I{P9I0c@)2zj<@|hDz{Zm{6 z8_lk^xyaf<{55EqOsac00000 LNkvXXu0mjfeGYlV literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Boost_Info.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Boost_Info.imageset/Contents.json new file mode 100644 index 000000000..02d313ec5 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Boost_Info.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_pf_info.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_pf_info@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Boost_Info.imageset/ic_pf_info.png b/Telegram-Mac/Assets.xcassets/Icon_Boost_Info.imageset/ic_pf_info.png new file mode 100644 index 0000000000000000000000000000000000000000..ba25543bc174de113ebd6a95ca221c0659e5d4e0 GIT binary patch literal 958 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 V*{^K10YINJc)I$ztaD0e0suf9b~OM1 literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/menu_boost_plus.imageset/menu_boost_plus@2x.png b/Telegram-Mac/Assets.xcassets/menu_boost_plus.imageset/menu_boost_plus@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6a8d06878f19ae8a142f27aedea2170d93fdb933 GIT binary patch literal 583 zcmV-N0=WH&P)56zh_!af?Z9orZA3!?Wi&Yr1ut%tB0JYp0C&_6Zt+)cpZNVvL!ewGu`}ZK zhUXcjrsApz#+=Khlp0t?SgEli#?odPFSvHDQe)0yM@osfL%GU$#)C3U=$vIxV1vh( zEXxWtJwcx?!%&4wu2OS3<`$F!NoAj_)ch_s!|vM(LV+b7tFCgK&`ZFdJoED_?{!DH zYF8sORcY_z(6mlQEwGoWD}gM#eyrOi>d&MyY`Cqt$=sOVKT#;CEeGsyMgb`6ZYY-6 zk6}YO0^T&qXAK_{6HC<<6F+k+DkA$R^M|0^<>T0t;wTj}YDr^I+j3>rxZwR@D?Qb` zZ_f)cbW3c@bIX~MS`u?(yvey0qmV&gP>;kY^niX(J<|Ft^Wa=|RtTA7t_fRpt5g&m z>svh#6#2S7@dw8kYaO^H1uEGWYN3*Kz?}RR`n@4|<_hWqHpT+k{BAFQeMU91Nz&e+ z7Ym66zHF*nq7SS7E~{h2T#@Z$p-_eL%9*yRkxinI#LX55@2Wxe_6l{O>uLYz_yIj2 VJIu|?y1W1Y002ovPDHLkV1l|)0B`^R literal 0 HcmV?d00001 diff --git a/Telegram-Mac/BoostChannelModalController.swift b/Telegram-Mac/BoostChannelModalController.swift index b0bd6980b..dbc26348b 100644 --- a/Telegram-Mac/BoostChannelModalController.swift +++ b/Telegram-Mac/BoostChannelModalController.swift @@ -202,15 +202,17 @@ private final class Arguments { let context: AccountContext let presentation: TelegramPresentationTheme let isGroup: Bool + let onlyFeatures: Bool let boost:()->Void let openChannel:()->Void let shareLink:(String)->Void let copyLink:(String)->Void let openGiveaway:()->Void - init(context: AccountContext, presentation: TelegramPresentationTheme, isGroup: Bool, boost:@escaping()->Void, openChannel:@escaping()->Void, shareLink: @escaping(String)->Void, copyLink: @escaping(String)->Void, openGiveaway:@escaping()->Void) { + init(context: AccountContext, presentation: TelegramPresentationTheme, isGroup: Bool, onlyFeatures: Bool, boost:@escaping()->Void, openChannel:@escaping()->Void, shareLink: @escaping(String)->Void, copyLink: @escaping(String)->Void, openGiveaway:@escaping()->Void) { self.context = context self.presentation = presentation self.isGroup = isGroup + self.onlyFeatures = onlyFeatures self.boost = boost self.copyLink = copyLink self.shareLink = shareLink @@ -289,6 +291,8 @@ private struct State : Equatable { title = strings().channelBoostProfileIcon case .emojiStatus: title = strings().channelBoostEmojiStatus + case .emojiPack: + title = strings().channelBoostEmojiPack case .reactions: title = strings().channelBoostEnableReactions case .wallpaper: @@ -338,6 +342,7 @@ private struct State : Equatable { + private final class BoostRowItem : TableRowItem { fileprivate let context: AccountContext fileprivate let state: State @@ -374,29 +379,52 @@ private final class BoostRowItem : TableRowItem { case let .profileColor(level): string = strings().channelBoostEnableProfileColorLevelText("\(level)") case let .emojiStatus(level): - string = strings().channelBoostEnableEmojiStatusLevelText("\(level)") + if state.isGroup { + string = strings().channelBoostEnableEmojiStatusLevelTextGroup("\(level)") + } else { + string = strings().channelBoostEnableEmojiStatusLevelText("\(level)") + } case let .wallpaper(level): - string = strings().channelBoostEnableWallpapersText("\(level)") + if state.isGroup { + string = strings().channelBoostEnableWallpapersTextGroup("\(level)") + } else { + string = strings().channelBoostEnableWallpapersText("\(level)") + } case .reactions: - string = strings().channelBoostEnableReactionsText("\(level + 1)", "\(level)") + if state.isGroup { + string = strings().channelBoostEnableReactionsTextGroup("\(level + 1)", "\(level)") + } else { + string = strings().channelBoostEnableReactionsText("\(level + 1)", "\(level)") + } default: if level == 0 { - string = strings().channelBoostEnableStoriesText(valueString) + if state.isGroup { + string = strings().channelBoostZeroLevelTextGroup(valueString) + } else { + string = strings().channelBoostZeroLevelTextChannel(valueString) + } } else { - string = strings().channelBoostIncreaseLimitText(valueString, "\(level + 1)") + if state.isGroup { + string = strings().channelBoostIncreaseLimitTextGroup(valueString) + } else { + string = strings().channelBoostIncreaseLimitTextChannel(valueString) + } } } } else { string = "" } } else { - let storiesString = strings().channelBoostStoriesPerDayCountable(level + 1) if let remaining = remaining { let valueString: String = strings().channelBoostMoreBoostsCountable(remaining) if remaining == 0 { - string = strings().channelBoostBoostedChannelReachedLevel("\(level + 1)", storiesString) + if state.isGroup { + string = strings().channelBoostBoostedChannelReachedLevelGroup("\(level + 1)") + } else { + string = strings().channelBoostBoostedChannelReachedLevelChannel("\(level + 1)") + } } else { - string = strings().channelBoostBoostedChannelMoreRequired(valueString, storiesString) + string = strings().channelBoostBoostedChannelMoreRequiredNew(valueString) } } else { @@ -405,7 +433,6 @@ private final class BoostRowItem : TableRowItem { } if state.boosted { - let storiesString = strings().channelBoostStoriesPerDayCountable(level + 1) if let remaining = remaining { let valueString: String = strings().channelBoostMoreBoostsCountable(remaining) if level == 0 { @@ -415,19 +442,26 @@ private final class BoostRowItem : TableRowItem { string = strings().channelBoostEnableStoriesMoreRequired(valueString) } } else { - if remaining == 0 { - string = strings().channelBoostBoostedChannelReachedLevel("\(level + 1)", storiesString) + if state.isGroup { + string = strings().channelBoostBoostedChannelReachedLevelGroup("\(level + 1)") } else { - string = strings().channelBoostBoostedChannelMoreRequired(valueString, storiesString) + string = strings().channelBoostBoostedChannelReachedLevelChannel("\(level + 1)") } } } else { - string = strings().channelBoostBoostedChannelReachedLevel("\(level + 1)", storiesString) + if state.isGroup { + string = strings().channelBoostBoostedChannelReachedLevelGroup("\(level + 1)") + } else { + string = strings().channelBoostBoostedChannelReachedLevelChannel("\(level + 1)") + } } } } else { - let storiesString = strings().channelBoostStoriesPerDayCountable(level) - string = strings().channelBoostMaxLevelReachedText("\(level)", storiesString) + if state.isGroup { + string = strings().channelBoostMaxLevelReachedTextGroup("\(level)") + } else { + string = strings().channelBoostMaxLevelReachedTextChannel("\(level)") + } } @@ -955,8 +989,8 @@ private final class AcceptRowView : TableRowView { private let _id_accept = InputDataIdentifier("accept") -private func _id_perk(_ perk: BoostChannelPerk, _ level: Int32) -> InputDataIdentifier { - return .init("perk_\(perk.title().hashValue)_\(level)") +private func _id_perk(_ perk: BoostChannelPerk, _ level: Int32, _ isGroup: Bool) -> InputDataIdentifier { + return .init("perk_\(perk.title(isGroup: isGroup).hashValue)_\(level)") } private func _id_perk_level(_ level: Int32) -> InputDataIdentifier { return .init("perk_level_\(level)") @@ -969,45 +1003,70 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { var index: Int32 = 0 var sectionId: Int32 = 0 - entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("whole"), equatable: .init(state), comparable: nil, item: { initialSize, stableId in - return BoostRowItem(initialSize, presentation: arguments.presentation, state: state, context: arguments.context, boost: arguments.boost, openChannel: arguments.openChannel) - })) - index += 1 - - var noLastSection = false - - if state.isAdmin, state.status.nextLevelBoosts != nil { + if arguments.onlyFeatures { + let text = NSMutableAttributedString() - entries.append(.sectionId(sectionId, type: .customModern(20))) - sectionId += 1 - - entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("link"), equatable: InputDataEquatable(state.link), comparable: nil, item: { initialSize, stableId in - return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .singleItem, text: state.link, font: .normal(.text), insets: NSEdgeInsets(left: 20, right: 20), rightAction: .init(image: arguments.presentation.icons.fast_copy_link, action: { - arguments.copyLink(state.link) - }), customTheme: .initialize(arguments.presentation)) + //.initialize(string: "Additional Features\nBy gaining boosts, your group levels and unlocks more features.") + text.append(string: strings().channelBoostAdditionalFeaturesTitle, color: theme.colors.text, font: .medium(.header)) + text.append(string: "\n") + text.append(string: strings().channelBoostAdditionalFeaturesText, color: theme.colors.text, font: .normal(.text)) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("whole"), equatable: .init(state), comparable: nil, item: { initialSize, stableId in + + return AnimatedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: .menu_lighting, text: text, stickerSize: NSMakeSize(60, 60)) })) index += 1 - - entries.append(.sectionId(sectionId, type: .customModern(10))) - sectionId += 1 - - entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(strings().boostGetBoosts, linkHandler: { _ in - arguments.openGiveaway() - }), data: .init(color: arguments.presentation.colors.text, viewType: .textBottomItem, fontSize: 13, centerViewAlignment: true, alignment: .center, linkColor: arguments.presentation.colors.link))) - - } else { - entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_accept, equatable: .init(state), comparable: nil, item: { initialSize, stableId in - return AcceptRowItem(initialSize, state: state, context: arguments.context, presentation: arguments.presentation, boost: arguments.boost) + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("whole"), equatable: .init(state), comparable: nil, item: { initialSize, stableId in + return BoostRowItem(initialSize, presentation: arguments.presentation, state: state, context: arguments.context, boost: arguments.boost, openChannel: arguments.openChannel) })) index += 1 - noLastSection = true } + + + var noLastSection = false + + if !arguments.onlyFeatures { + if state.isAdmin, state.status.nextLevelBoosts != nil { + + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("link"), equatable: InputDataEquatable(state.link), comparable: nil, item: { initialSize, stableId in + return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .singleItem, text: state.link, font: .normal(.text), insets: NSEdgeInsets(left: 20, right: 20), rightAction: .init(image: arguments.presentation.icons.fast_copy_link, action: { + arguments.copyLink(state.link) + }), customTheme: .initialize(arguments.presentation)) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .customModern(10))) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(strings().boostGetBoosts, linkHandler: { _ in + arguments.openGiveaway() + }), data: .init(color: arguments.presentation.colors.text, viewType: .textBottomItem, fontSize: 13, centerViewAlignment: true, alignment: .center, linkColor: arguments.presentation.colors.link))) + + + } else { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_accept, equatable: .init(state), comparable: nil, item: { initialSize, stableId in + return AcceptRowItem(initialSize, state: state, context: arguments.context, presentation: arguments.presentation, boost: arguments.boost) + })) + index += 1 + noLastSection = true + } + } + + var nextLevels: ClosedRange? - if state.status.level < 10 { - nextLevels = Int32(state.status.level) + 1 ... 10 + if arguments.onlyFeatures { + nextLevels = 1 ... 10 + } else { + if state.status.level < 10 { + nextLevels = Int32(state.status.level) + 1 ... 10 + } } + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.appConfiguration) @@ -1093,8 +1152,8 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { })) for perk in perks { - entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_perk(perk, level), equatable: .init(perk), comparable: nil, item: { initialSize, stableId in - return BoostFeatureRowItem(initialSize, stableId: stableId, perk: perk) + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_perk(perk, level, isGroup), equatable: .init(perk), comparable: nil, item: { initialSize, stableId in + return BoostFeatureRowItem(initialSize, stableId: stableId, isGroup: isGroup, perk: perk) })) } } @@ -1117,10 +1176,11 @@ enum BoostChannelSource : Equatable { case profileColor(Int32) case profileIcon(Int32) case emojiStatus(Int32) + case emojiPack(Int32) case wallpaper(Int32) } -func BoostChannelModalController(context: AccountContext, peer: Peer, boosts: ChannelBoostStatus, myStatus: MyBoostStatus?, infoOnly: Bool = false, source: BoostChannelSource = .basic, presentation: TelegramPresentationTheme = theme) -> InputDataModalController { +func BoostChannelModalController(context: AccountContext, peer: Peer, boosts: ChannelBoostStatus, myStatus: MyBoostStatus?, infoOnly: Bool = false, onlyFeatures: Bool = false, source: BoostChannelSource = .basic, presentation: TelegramPresentationTheme = theme) -> InputDataModalController { let actionsDisposable = DisposableSet() @@ -1140,7 +1200,7 @@ func BoostChannelModalController(context: AccountContext, peer: Peer, boosts: Ch var close:(()->Void)? = nil - let arguments = Arguments(context: context, presentation: presentation, isGroup: peer.isGroup || peer.isSupergroup, boost: { + let arguments = Arguments(context: context, presentation: presentation, isGroup: peer.isGroup || peer.isSupergroup, onlyFeatures: onlyFeatures, boost: { let myStatus = stateValue.with { $0.myStatus } let nextLevelBoosts = stateValue.with { $0.status.nextLevelBoosts } @@ -1206,7 +1266,7 @@ func BoostChannelModalController(context: AccountContext, peer: Peer, boosts: Ch showModalText(for: context.window, text: strings().shareLinkCopied) copyToClipboard(link) }, openGiveaway: { - showModal(with: GiveawayModalController(context: context, peerId: peer.id, prepaid: nil), for: context.window) + showModal(with: GiveawayModalController(context: context, peerId: peer.id, prepaid: nil, isGroup: stateValue.with { $0.isGroup }), for: context.window) }) let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in diff --git a/Telegram-Mac/BoostFeatureRowItem.swift b/Telegram-Mac/BoostFeatureRowItem.swift index f1e924042..0a484eba5 100644 --- a/Telegram-Mac/BoostFeatureRowItem.swift +++ b/Telegram-Mac/BoostFeatureRowItem.swift @@ -22,7 +22,7 @@ enum BoostChannelPerk: Equatable { case customWallpaper case audioTranscription case emojiPack - func title() -> String { + func title(isGroup: Bool) -> String { switch self { case let .story(value): return strings().channelBoostTableStoriesPerDayCountable(Int(value)) @@ -31,9 +31,17 @@ enum BoostChannelPerk: Equatable { case let .nameColor(value): return strings().channelBoostTableNameColorCountable(Int(value)) case let .profileColor(value): - return strings().channelBoostTableProfileColorCountable(Int(value)) + if isGroup { + return strings().channelBoostTableProfileColorGroupCountable(Int(value)) + } else { + return strings().channelBoostTableProfileColorCountable(Int(value)) + } case .profileIcon: - return strings().channelBoostTableProfileLogo + if isGroup { + return strings().channelBoostTableProfileLogoGroup + } else { + return strings().channelBoostTableProfileLogo + } case let .linkColor(value): return strings().channelBoostTableStyleForHeadersCountable(Int(value)) case .linkIcon: @@ -41,9 +49,17 @@ enum BoostChannelPerk: Equatable { case .emojiStatus: return strings().channelBoostTableEmojiStatus case let .wallpaper(value): - return strings().channelBoostTableWallpaperCountable(Int(value)) + if isGroup { + return strings().channelBoostTableWallpaperGroupCountable(Int(value)) + } else { + return strings().channelBoostTableWallpaperCountable(Int(value)) + } case .customWallpaper: - return strings().channelBoostTableCustomWallpaper + if isGroup { + return strings().channelBoostTableCustomWallpaperGroup + } else { + return strings().channelBoostTableCustomWallpaper + } case .audioTranscription: return strings().channelBoostTableAudioTranscription case .emojiPack: @@ -88,9 +104,9 @@ enum BoostChannelPerk: Equatable { final class BoostFeatureRowItem : GeneralRowItem { let perk: BoostChannelPerk fileprivate let textLayout: TextViewLayout - init(_ initialSize: NSSize, stableId: AnyHashable, perk: BoostChannelPerk) { + init(_ initialSize: NSSize, stableId: AnyHashable, isGroup: Bool, perk: BoostChannelPerk) { self.perk = perk - self.textLayout = .init(.initialize(string: perk.title(), color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 1) + self.textLayout = .init(.initialize(string: perk.title(isGroup: isGroup), color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 1) super.init(initialSize, stableId: stableId) } diff --git a/Telegram-Mac/ChannelBoostStatsController.swift b/Telegram-Mac/ChannelBoostStatsController.swift index 00a2a23fa..82cec9bf6 100644 --- a/Telegram-Mac/ChannelBoostStatsController.swift +++ b/Telegram-Mac/ChannelBoostStatsController.swift @@ -13,6 +13,46 @@ import SwiftSignalKit import TelegramCore import Postbox +private func actionItems(state: State, width: CGFloat, arguments: Arguments, theme: TelegramPresentationTheme) -> [ActionItem] { + var items: [ActionItem] = [] + + var rowItemsCount: Int = 1 + + while width - (ActionItem.actionItemWidth + ActionItem.actionItemInsetWidth) > ((ActionItem.actionItemWidth * CGFloat(rowItemsCount)) + (CGFloat(rowItemsCount - 1) * ActionItem.actionItemInsetWidth)) { + rowItemsCount += 1 + } + rowItemsCount = min(rowItemsCount, 4) + + + + if state.isGroup { + items.append(.init(text: strings().statsBoostsActionBoost, color: theme.colors.accent, image: theme.icons.stats_boost_boost, animation: .menu_boost_plus, action: { + arguments.boost(false) + })) + + items.append(.init(text: strings().statsBoostsActionGiveaway, color: theme.colors.accent, image: theme.icons.stats_boost_giveaway, animation: .menu_gift, action: { + arguments.giveaway(nil) + })) + + items.append(.init(text: strings().statsBoostsActionInfo, color: theme.colors.accent, image: theme.icons.stats_boost_info, animation: .menu_show_info, action: { + arguments.boost(true) + })) + } + + if items.count > rowItemsCount { + var subItems:[SubActionItem] = [] + while items.count > rowItemsCount - 1 { + let item = items.removeLast() + subItems.insert(SubActionItem(text: item.text, animation: item.animation, destruct: item.destruct, action: item.action), at: 0) + } + if !subItems.isEmpty { + items.append(ActionItem(text: strings().peerInfoActionMore, color: theme.colors.accent, image: theme.icons.profile_more, animation: .menu_plus, action: { }, subItems: subItems)) + } + } + + return items +} + private func generateBoostReason(_ text: String, color: NSColor = theme.colors.accent) -> CGImage { let attr = NSMutableAttributedString() @@ -220,11 +260,33 @@ private final class BoostRowItem : TableRowItem { fileprivate let context: AccountContext private let _stableId: AnyHashable fileprivate let state: State - init(_ initialSize: NSSize, stableId: AnyHashable, state: State, context: AccountContext) { + fileprivate var items: [ActionItem] = [] + fileprivate let textLayout: TextViewLayout? + fileprivate let arguments: Arguments + init(_ initialSize: NSSize, stableId: AnyHashable, state: State, context: AccountContext, arguments: Arguments) { self.context = context self.state = state self._stableId = stableId + self.arguments = arguments + + if state.isGroup { + let attr: NSAttributedString = .initialize(string: strings().statsBoostsGroupInfo, color: theme.colors.text, font: .normal(.text)).detectBold(with: .medium(.text)) + self.textLayout = .init(attr, alignment: .center) + } else { + self.textLayout = nil + } + super.init(initialSize) + + _ = makeSize(initialSize.width) + } + + override func makeSize(_ width: CGFloat = CGFloat.greatestFiniteMagnitude, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + self.items = actionItems(state: state, width: width, arguments: arguments, theme: theme) + + textLayout?.measure(width: blockWidth) + return true } var blockWidth: CGFloat { @@ -237,11 +299,15 @@ private final class BoostRowItem : TableRowItem { override var height: CGFloat { - if state.isGroup { - return 100 + 100 - } else { - return 100 + var height: CGFloat = 100 + if !items.isEmpty { + let maxActionSize: NSSize = items.max(by: { $0.size.height < $1.size.height })!.size + height += maxActionSize.height } + if let textLayout { + height += textLayout.layoutSize.height + 20 + } + return height } override func viewClass() -> AnyClass { @@ -252,7 +318,9 @@ private final class BoostRowItem : TableRowItem { private final class BoostRowItemView : TableRowView { private let lineView = LineView(frame: .zero) private let top = TypeView(frame: .zero) - + private let actionsView = View() + + private var textView: TextView? private class LineView: View { @@ -436,12 +504,54 @@ private final class BoostRowItemView : TableRowView { } - + private func _actionItemWidth(_ items: [ActionItem]) -> CGFloat { + guard let item = item as? BoostRowItem else { + return 0 + } + let width = (item.blockWidth - (ActionItem.actionItemInsetWidth * CGFloat(items.count - 1))) + + return max(ActionItem.actionItemWidth, min(150, width / CGFloat(items.count))) + } + + private func layoutActionItems(_ items: [ActionItem], animated: Bool) { + + if !items.isEmpty { + let maxActionSize: NSSize = items.max(by: { $0.size.height < $1.size.height })!.size + + + while actionsView.subviews.count > items.count { + actionsView.subviews.removeLast() + } + while actionsView.subviews.count < items.count { + actionsView.addSubview(ActionButton(frame: .zero)) + } + + let inset: CGFloat = 0 + + let actionItemWidth = _actionItemWidth(items) + + actionsView.change(size: NSMakeSize(actionItemWidth * CGFloat(items.count) + CGFloat(items.count - 1) * ActionItem.actionItemInsetWidth, maxActionSize.height), animated: animated) + + var x: CGFloat = inset + + for (i, item) in items.enumerated() { + let view = actionsView.subviews[i] as! ActionButton + view.updateAndLayout(item: item, bgColor: theme.colors.background) + view.setFrameSize(NSMakeSize(actionItemWidth, maxActionSize.height)) + view.change(pos: NSMakePoint(x, 0), animated: false) + x += actionItemWidth + ActionItem.actionItemInsetWidth + } + + } else { + actionsView.removeAllSubviews() + } + } required init(frame frameRect: NSRect) { super.init(frame: frameRect) self.addSubview(top) self.addSubview(lineView) + self.addSubview(actionsView) } override var backdorColor: NSColor { @@ -465,7 +575,24 @@ private final class BoostRowItemView : TableRowView { let size = top.update(state: item.state, context: item.context, transition: .immediate) top.setFrameSize(size) - + + if let textLayout = item.textLayout { + let current: TextView + if let view = self.textView { + current = view + } else { + current = TextView() + current.userInteractionEnabled = false + current.isSelectable = false + self.textView = current + addSubview(current) + } + current.update(textLayout) + } else if let view = self.textView { + performSubviewRemoval(view, animated: animated) + self.textView = nil + } + needsLayout = true } @@ -476,6 +603,8 @@ private final class BoostRowItemView : TableRowView { guard let item = self.item as? BoostRowItem else { return } + + layoutActionItems(item.items, animated: transition.isAnimated) transition.updateFrame(view: lineView, frame: lineView.centerFrameX(y: top.frame.height + 10)) @@ -484,6 +613,12 @@ private final class BoostRowItemView : TableRowView { transition.updateFrame(view: top, frame: CGRect(origin: topPoint, size: top.frame.size)) + if let textView = textView { + transition.updateFrame(view: textView, frame: textView.centerFrameX(y: lineView.frame.maxY + 10)) + } + + transition.updateFrame(view: actionsView, frame: actionsView.centerFrameX(y: size.height - actionsView.frame.height)) + } } @@ -496,7 +631,8 @@ private final class Arguments { let showMore:()->Void let giveaway:(PrepaidGiveaway?)->Void let openSlug:(String)->Void - init(context: AccountContext, openPeerInfo:@escaping(PeerId)->Void, shareLink: @escaping(String)->Void, copyLink: @escaping(String)->Void, showMore:@escaping()->Void, giveaway:@escaping(PrepaidGiveaway?)->Void, openSlug:@escaping(String)->Void) { + let boost:(Bool)->Void + init(context: AccountContext, openPeerInfo:@escaping(PeerId)->Void, shareLink: @escaping(String)->Void, copyLink: @escaping(String)->Void, showMore:@escaping()->Void, giveaway:@escaping(PrepaidGiveaway?)->Void, openSlug:@escaping(String)->Void, boost:@escaping(Bool)->Void) { self.context = context self.shareLink = shareLink self.copyLink = copyLink @@ -504,14 +640,17 @@ private final class Arguments { self.showMore = showMore self.giveaway = giveaway self.openSlug = openSlug + self.boost = boost } } + + private struct State : Equatable { var peer: PeerEquatable? var boostStatus: ChannelBoostStatus? var booster: ChannelBoostersContext.State? - + var myStatus: MyBoostStatus? var revealed: Bool = false var isGroup: Bool @@ -579,7 +718,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { sectionId += 1 entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("level"), equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in - return BoostRowItem(initialSize, stableId: stableId, state: state, context: arguments.context) + return BoostRowItem(initialSize, stableId: stableId, state: state, context: arguments.context, arguments: arguments) })) entries.append(.sectionId(sectionId, type: .normal)) @@ -769,13 +908,14 @@ func ChannelBoostStatsController(context: AccountContext, peerId: PeerId, isGrou let boostersContext = ChannelBoostersContext(account: context.account, peerId: peerId, gift: false) - actionsDisposable.add(combineLatest(context.account.postbox.loadedPeerWithId(peerId), boostData, boostersContext.state).start(next: { peer, boostData, boosters in + actionsDisposable.add(combineLatest(context.account.postbox.loadedPeerWithId(peerId), boostData, boostersContext.state, context.engine.peers.getMyBoostStatus()).start(next: { peer, boostData, boosters, myStatus in updateState { current in var current = current current.peer = .init(peer) current.boostStatus = boostData current.booster = boosters + current.myStatus = myStatus return current } })) @@ -790,9 +930,16 @@ func ChannelBoostStatsController(context: AccountContext, peerId: PeerId, isGrou }, showMore: { [weak boostersContext] in boostersContext?.loadMore() }, giveaway: { prepaid in - showModal(with: GiveawayModalController(context: context, peerId: peerId, prepaid: prepaid), for: context.window) + showModal(with: GiveawayModalController(context: context, peerId: peerId, prepaid: prepaid, isGroup: isGroup), for: context.window) }, openSlug: { slug in execute(inapp: .gift(link: "", slug: slug, context: context)) + }, boost: { features in + let status = stateValue.with { $0.boostStatus } + let myStatus = stateValue.with { $0.myStatus } + let peer = stateValue.with { $0.peer?.peer } + if let status = status, let peer = peer { + showModal(with: BoostChannelModalController(context: context, peer: peer, boosts: status, myStatus: myStatus, onlyFeatures: features), for: context.window) + } }) let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in diff --git a/Telegram-Mac/ChannelPermissionsController.swift b/Telegram-Mac/ChannelPermissionsController.swift index 584881a0a..fe3d02e60 100644 --- a/Telegram-Mac/ChannelPermissionsController.swift +++ b/Telegram-Mac/ChannelPermissionsController.swift @@ -57,6 +57,13 @@ private struct State: Equatable { var peer: PeerEquatable? var cachedData: CachedDataEquatable? var restrictBoosters: Int32? = nil + + var slowmodeTimeout: Int32? { + if let cachedDatra = cachedData?.data as? CachedChannelData { + return cachedDatra.slowModeTimeout + } + return nil + } } func stringForGroupPermission(right: TelegramChatBannedRightsFlags, channel: TelegramChannel?) -> String { @@ -296,9 +303,8 @@ private func entries(state: State, arguments: Arguments) -> [InputDataEntry] { } let insertBoost:(TelegramChatBannedRightsFlags)->Void = { rigths in - //TODO LANG - if rigths.contains(.banSendMedia) || rigths.contains(.banSendText) { - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_do_not_boosters, data: .init(name: "Do Not Restrict Boosters", color: theme.colors.text, type: .switchable(state.restrictBoosters != nil), viewType: state.restrictBoosters != nil ? .firstItem : .singleItem, action: { + if rigths.contains(.banSendMedia) || rigths.contains(.banSendText) || (state.slowmodeTimeout != nil && state.slowmodeTimeout! > 0) { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_do_not_boosters, data: .init(name: strings().channelPermissionRestrictBoosters, color: theme.colors.text, type: .switchable(state.restrictBoosters != nil), viewType: state.restrictBoosters != nil ? .firstItem : .singleItem, action: { if state.restrictBoosters != nil { arguments.updateBoostNeed(nil) } else { @@ -315,13 +321,18 @@ private func entries(state: State, arguments: Arguments) -> [InputDataEntry] { "3", "4", "5"] - return SelectSizeRowItem(initialSize, stableId: stableId, current: boost, sizes: list, hasMarkers: false, titles: titles, viewType: .lastItem, selectAction: { index in + let images:[CGImage] = [NSImage(named: "Icon_Boost_Indicator_Single")!.precomposed(theme.colors.grayText, flipVertical: true), + NSImage(named: "Icon_Boost_Indicator_Multiple")!.precomposed(theme.colors.grayText, flipVertical: true), + NSImage(named: "Icon_Boost_Indicator_Multiple")!.precomposed(theme.colors.grayText, flipVertical: true), + NSImage(named: "Icon_Boost_Indicator_Multiple")!.precomposed(theme.colors.grayText, flipVertical: true), + NSImage(named: "Icon_Boost_Indicator_Multiple")!.precomposed(theme.colors.grayText, flipVertical: true)] + return SelectSizeRowItem(initialSize, stableId: stableId, current: boost, sizes: list, hasMarkers: false, titles: titles, titlesImages: images, viewType: .lastItem, selectAction: { index in arguments.updateBoostNeed(list[index]) }) })) - infoText = "Choose how many boosts a user must give to the group to bypass restrictions on sending messages." + infoText = strings().channelPermissionRestrictBoostersActive } else { - infoText = "Turn this on to always allow users who boosted your group to send messages and media." + infoText = strings().channelPermissionRestrictBoostersNonActive } entries.append(.desc(sectionId: sectionId, index: index, text: .plain(infoText), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) @@ -573,7 +584,7 @@ final class ChannelPermissionsController : TableViewController { let peerId = self.peerId let context = self.context - let statePromise = ValuePromise(ignoreRepeated: true) + let statePromise = ValuePromise(State(), ignoreRepeated: true) let stateValue = Atomic(value: State()) let updateState: ((State) -> State) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) @@ -953,7 +964,7 @@ final class ChannelPermissionsController : TableViewController { actionsDisposable.add(peersPromise.get().start(next: { participants in updateState { current in var current = current - current.participants = participants + current.participants = participants ?? [] return current } })) diff --git a/Telegram-Mac/ChatController.swift b/Telegram-Mac/ChatController.swift index aadfad3dc..2f85ebd4b 100644 --- a/Telegram-Mac/ChatController.swift +++ b/Telegram-Mac/ChatController.swift @@ -775,7 +775,8 @@ class ChatControllerView : View, ChatInputDelegate { } else if peerStatus.canAddContact && settings.contains(.canAddContact) { value = .addContact(block: settings.contains(.canReport) || settings.contains(.canBlock), autoArchived: settings.contains(.autoArchived)) } else if settings.contains(.canReport) { - value = .report(autoArchived: settings.contains(.autoArchived), status: interfaceState.peer?.emojiStatus) + let isUser = interfaceState.peer?.isUser == true + value = .report(autoArchived: settings.contains(.autoArchived), status: isUser ? interfaceState.peer?.emojiStatus : nil) } else if settings.contains(.canShareContact) { value = .shareInfo } else if let pinnedMessageId = interfaceState.pinnedMessageId, !interfaceState.interfaceState.dismissedPinnedMessageId.contains(pinnedMessageId.messageId), !interfaceState.hidePinnedMessage, interfaceState.chatMode != .pinned { diff --git a/Telegram-Mac/ChatInterfaceInteraction.swift b/Telegram-Mac/ChatInterfaceInteraction.swift index 6b1c4861a..c028d8046 100644 --- a/Telegram-Mac/ChatInterfaceInteraction.swift +++ b/Telegram-Mac/ChatInterfaceInteraction.swift @@ -732,7 +732,6 @@ final class ChatInteraction : InterfaceObserver { } if FastSettings.shouldConfirmWebApp(botId) { verifyAlert_button(for: context.window, header: strings().webAppFirstOpenTitle, information: strings().webAppFirstOpenInfo(peer.displayTitle), successHandler: { result in - FastSettings.markWebAppAsConfirmed(botId) invoke() }) diff --git a/Telegram-Mac/ChatInterfaceState.swift b/Telegram-Mac/ChatInterfaceState.swift index 15a1bce9c..7e72bd226 100644 --- a/Telegram-Mac/ChatInterfaceState.swift +++ b/Telegram-Mac/ChatInterfaceState.swift @@ -1119,7 +1119,7 @@ struct ChatInterfaceState: Codable, Equatable { .withUpdatedReplyMessageId(state.replySubject) .withUpdatedTimestamp(timestamp) } else { - result = result.withUpdatedHistoryScrollState(self.historyScrollState) + result = result.withUpdatedInputState(.init()).withUpdatedHistoryScrollState(self.historyScrollState) } return result } diff --git a/Telegram-Mac/ChatRowItem.swift b/Telegram-Mac/ChatRowItem.swift index 9d37badb0..d758d52b8 100644 --- a/Telegram-Mac/ChatRowItem.swift +++ b/Telegram-Mac/ChatRowItem.swift @@ -1973,7 +1973,7 @@ class ChatRowItem: TableRowItem { if range.location != NSNotFound { attr.addAttribute(.link, value: link, range: range) if message.forwardInfo?.author != nil || message.forwardInfo == nil { - attr.addAttribute(.font, value: NSFont.bold(.short), range: range) + attr.addAttribute(.font, value: NSFont.medium(.short), range: range) } } } else { diff --git a/Telegram-Mac/CustomizeAccountController.swift b/Telegram-Mac/CustomizeAccountController.swift deleted file mode 100644 index 2ec7d789e..000000000 --- a/Telegram-Mac/CustomizeAccountController.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// CustomizeAccountController.swift -// Telegram -// -// Created by Mike Renoir on 20.11.2023. -// Copyright © 2023 Telegram. All rights reserved. -// - -import Foundation -import TelegramCore -import Postbox -import SwiftSignalKit -import TGUIKit - -private final class CenterView : TitledBarView { - let segment: CatalinaStyledSegmentController - var select:((Int)->Void)? = nil - init(controller: ViewController) { - self.segment = CatalinaStyledSegmentController(frame: NSMakeRect(0, 0, 240, 30)) - super.init(controller: controller) - - segment.add(segment: .init(title: strings().customizeNameTitle, handler: { [weak self] in - self?.select?(0) - })) - - segment.add(segment: .init(title: strings().customizeProfileTitle, handler: { [weak self] in - self?.select?(1) - })) - - self.addSubview(segment.view) - - updateLocalizationAndTheme(theme: theme) - } - - override func updateLocalizationAndTheme(theme: PresentationTheme) { - super.updateLocalizationAndTheme(theme: theme) - - segment.theme = CatalinaSegmentTheme(backgroundColor: theme.colors.listBackground, foregroundColor: theme.colors.background, activeTextColor: theme.colors.text, inactiveTextColor: theme.colors.listGrayText) - - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - override func layout() { - super.layout() - segment.view.frame = focus(NSMakeSize(min(frame.width - 40, 600), 30)) - } -} - -final class CustomizeAccountController : SectionViewController { - private let name: ViewController - private let profile: ViewController - private let context: AccountContext - private let peerId: PeerId - private let peer: Peer - private let profileState: SelectColorCallback = .init() - private let nameState: SelectColorCallback = .init() - init(_ context: AccountContext, peer: Peer) { - self.context = context - self.peerId = peer.id - self.peer = peer - self.name = SelectColorController(context: context, source: peer.isChannel ? .channel(peer) : .account(peer), callback: nameState) - self.profile = SelectColorController(context: context, source: peer.isChannel ? .channel(peer) : .account(peer), callback: profileState) - - var items:[SectionControllerItem] = [] - items.append(SectionControllerItem(title: { "" }, controller: name)) - items.append(SectionControllerItem(title: { "" }, controller: profile)) - - super.init(sections: items, selected: 0, hasHeaderView: false, hasBar: true) - - } - - override func getCenterBarViewOnce() -> TitledBarView { - return CenterView(controller: self) - } - - override func getRightBarViewOnce() -> BarView { - return TextButtonBarView(controller: self, text: strings().selectColorApply, style: barPresentation, alignment:.Right) - } - - override var enableBack: Bool { - return true - } - - override var supportSwipes: Bool { - return self.selectedIndex == 0 - } - - override func viewDidLoad() { - super.viewDidLoad() - - centerView.select = { [weak self] index in - self?.select(index, true) - } - - self.selectionUpdateHandler = { [weak self] index in - self?.centerView.segment.set(selected: index, animated: true) - } - let context = self.context - - let channel_color_level_min = context.appConfiguration.getGeneralValue("channel_color_level_min", orElse: 1) - - - let invoke:()->Void = { [weak self] in - let nameState = self?.nameState.getState?() - let profileState = self?.profileState.getState?() - - - let nameColor = nameState?.0 ?? .blue - let backgroundEmojiId = nameState?.1 - let profileColor = profileState?.0 - let profileBackgroundEmojiId = profileState?.1 - - if let peer = self?.peer, peer.isChannel { - let peerId = peer.id - let signal = showModalProgress(signal: combineLatest(context.engine.peers.getChannelBoostStatus(peerId: peerId), context.engine.peers.getMyBoostStatus()), for: context.window) - - _ = signal.start(next: { stats, myStatus in - if let stats = stats { - if stats.level < channel_color_level_min { - showModal(with: BoostChannelModalController(context: context, peer: peer, boosts: stats, myStatus: myStatus, infoOnly: true, source: .nameColor(channel_color_level_min)), for: context.window) - } else { - _ = context.engine.peers.updatePeerNameColorAndEmoji(peerId: peerId, nameColor: nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId).start() - self?.navigationController?.back() - showModalText(for: context.window, text: strings().selectColorSuccessChannel) - } - } - }) - } else { - if context.isPremium { - _ = context.engine.accountData.updateNameColorAndEmoji(nameColor: nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId).start() - showModalText(for: context.window, text: strings().selectColorSuccessUser) - self?.navigationController?.back() - } else { - showModalText(for: context.window, text: strings().selectColorPremium, callback: { _ in - showModal(with: PremiumBoardingController(context: context), for: context.window) - }) - } - } - } - - self.rightBarView.set(handler:{ _ in - invoke() - }, for: .Click) - - nameState.validate = { - invoke() - } - profileState.validate = { - invoke() - } - } - - private var centerView: CenterView { - return self.centerBarView as! CenterView - } -} diff --git a/Telegram-Mac/EmojiesController.swift b/Telegram-Mac/EmojiesController.swift index dd0635335..be296ce64 100644 --- a/Telegram-Mac/EmojiesController.swift +++ b/Telegram-Mac/EmojiesController.swift @@ -2200,6 +2200,27 @@ final class EmojiesController : TelegramGenericViewController take(1) + } else if mode == .emoji { + if let peer = chatInteraction?.peer, peer.isGroup || peer.isSupergroup { + let emojiPack = getCachedDataView(peerId: peer.id, postbox: context.account.postbox) |> map { $0 as? CachedChannelData } |> map { $0?.emojiPack } + iconStatusEmoji = emojiPack |> mapToSignal { info in + if let info = info { + return context.engine.stickers.loadedStickerPack(reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) + |> map { result -> [TelegramMediaFile] in + switch result { + case let .result(_, items, _): + return items.map(\.file) + default: + return [] + } + } + |> take(1) + } else { + return .single([]) + } + + } + } } diff --git a/Telegram-Mac/FastSettings.swift b/Telegram-Mac/FastSettings.swift index 99acc6fc5..1cf5d204f 100644 --- a/Telegram-Mac/FastSettings.swift +++ b/Telegram-Mac/FastSettings.swift @@ -635,7 +635,7 @@ class FastSettings { PremiumValue.peer_colors.rawValue, PremiumValue.saved_tags.rawValue, PremiumValue.last_seen.rawValue, - PremiumValue.messages_privacy.rawValue] + PremiumValue.message_privacy.rawValue] let dismissedPerks = UserDefaults.standard.value(forKey: "dismissedPerks") as? [String] ?? [] return perks.filter { !dismissedPerks.contains($0) } } diff --git a/Telegram-Mac/GiveawayModalController.swift b/Telegram-Mac/GiveawayModalController.swift index b16a808ed..bf4ad8c54 100644 --- a/Telegram-Mac/GiveawayModalController.swift +++ b/Telegram-Mac/GiveawayModalController.swift @@ -348,6 +348,8 @@ private struct State : Equatable { var countries: [Country] = [] + var isGroup: Bool + var prizeDescriptionValue: String? { if let prizeDescription = prizeDescription, additionalPrizes { return prizeDescription @@ -396,7 +398,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().giveawayHeaderTitle), data: .init(color: theme.colors.text, detectBold: true, viewType: .modern(position: .inner, insets: .init()), fontSize: 18, centerViewAlignment: true, alignment: .center))) index += 1 - entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().giveawayHeaderText), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .modern(position: .inner, insets: .init()), fontSize: 13, centerViewAlignment: true, alignment: .center))) + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(state.isGroup ? strings().giveawayHeaderTextGroup : strings().giveawayHeaderText), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .modern(position: .inner, insets: .init()), fontSize: 13, centerViewAlignment: true, alignment: .center))) index += 1 entries.append(.sectionId(sectionId, type: .normal)) @@ -467,7 +469,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 - entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().giveawayChannelsHeader), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textTopItem))) + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(state.isGroup ? strings().giveawayChannelsHeaderGroup : strings().giveawayChannelsHeader), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textTopItem))) index += 1 var channels: [PeerEquatable] = state.channels @@ -504,7 +506,17 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { for item in channelItems { entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_peer(item.peer.peer.id), equatable: .init(item), comparable: nil, item: { initialSize, stableId in - return ShortPeerRowItem(initialSize, peer: item.peer.peer, account: arguments.context.account, context: nil, status: item.peer.peer.id == state.channels[0].peer.id ? strings().giveawayChannelsBoostReceiveCountable(Int(item.quantity * perSentGift)) : nil, inset: NSEdgeInsets(left: 20, right: 20), viewType: item.viewType, contextMenuItems: { + let status: String? + if item.peer.peer.id == state.channels[0].peer.id { + if state.isGroup { + status = strings().giveawayChannelsBoostReceiveGroupCountable(Int(item.quantity * perSentGift)) + } else { + status = strings().giveawayChannelsBoostReceiveCountable(Int(item.quantity * perSentGift)) + } + } else { + status = nil + } + return ShortPeerRowItem(initialSize, peer: item.peer.peer, account: arguments.context.account, context: nil, status: status, inset: NSEdgeInsets(left: 20, right: 20), viewType: item.viewType, contextMenuItems: { var items: [ContextMenuItem] = [] if item.deletable { items.append(ContextMenuItem(strings().giveawayChannelsContextRemove, handler: { @@ -516,10 +528,10 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { })) } if !maximumReached { - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_add_channel, data: .init(name: strings().giveawayChannelsAdd, color: theme.colors.accent, icon: theme.icons.proxyAddProxy, viewType: .lastItem, action: arguments.addChannel))) + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_add_channel, data: .init(name: state.isGroup ? strings().giveawayChannelsAddGroup : strings().giveawayChannelsAdd, color: theme.colors.accent, icon: theme.icons.proxyAddProxy, viewType: .lastItem, action: arguments.addChannel))) } - entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().givewayChannelsInfo), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textBottomItem))) + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(state.isGroup ? strings().givewayChannelsInfoGroup : strings().givewayChannelsInfo), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textBottomItem))) index += 1 entries.append(.sectionId(sectionId, type: .normal)) @@ -553,7 +565,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_select_date, data: .init(name: strings().giveawayDateEnds, color: theme.colors.text, type: .nextContext(stringForFullDate(timestamp: Int32(state.date.timeIntervalSince1970))), viewType: .singleItem, action: arguments.selectDate))) if state.quantity > 0 { - entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().giveawayDateInfo), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textBottomItem))) + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(state.isGroup ? strings().giveawayDateInfoGroup : strings().giveawayDateInfo), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textBottomItem))) index += 1 } case .specific: @@ -694,7 +706,7 @@ enum GiveawaySubject { case prepaid(count: Int32, month: Int32) } -func GiveawayModalController(context: AccountContext, peerId: PeerId, prepaid: PrepaidGiveaway?) -> InputDataModalController { +func GiveawayModalController(context: AccountContext, peerId: PeerId, prepaid: PrepaidGiveaway?, isGroup: Bool) -> InputDataModalController { let actionsDisposable = DisposableSet() @@ -719,7 +731,7 @@ func GiveawayModalController(context: AccountContext, peerId: PeerId, prepaid: P } else { type = .random } - let initialState = State(type: type, channels: [], canMakePayment: canMakePayment) + let initialState = State(type: type, channels: [], canMakePayment: canMakePayment, isGroup: isGroup) var close: (()->Void)? = nil @@ -824,7 +836,10 @@ func GiveawayModalController(context: AccountContext, peerId: PeerId, prepaid: P if let peerId = peerIds.first { return context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue |> mapToSignal { peer in if peer.addressName == nil { - return verifyAlertSignal(for: context.window, header: strings().giveawayChannelsAddPrivateHeader, information: strings().giveawayChannelsAddPrivateText, ok: strings().giveawayChannelsAddPrivateOk) |> map { $0 == .basic } + let header: String = isGroup ? strings().giveawayChannelsAddPrivateHeaderGroup : strings().giveawayChannelsAddPrivateHeader + let info = isGroup ? strings().giveawayChannelsAddPrivateTextGroup : strings().giveawayChannelsAddPrivateText + let ok = isGroup ? strings().giveawayChannelsAddPrivateOkGroup : strings().giveawayChannelsAddPrivateOk + return verifyAlertSignal(for: context.window, header: header, information: info, ok: ok) |> map { $0 == .basic } } else { return .single(true) } diff --git a/Telegram-Mac/GroupEmojiPackController.swift b/Telegram-Mac/GroupEmojiPackController.swift index 8dcf9136d..9a7e419be 100644 --- a/Telegram-Mac/GroupEmojiPackController.swift +++ b/Telegram-Mac/GroupEmojiPackController.swift @@ -171,7 +171,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { return entries } -func GroupEmojiPackController(context: AccountContext) -> InputDataController { +func GroupEmojiPackController(context: AccountContext, peerId: PeerId, selected: StickerPackCollectionInfo?, updated: @escaping(StickerPackCollectionInfo?)->Void) -> InputDataController { let actionsDisposable = DisposableSet() let searchDisposable = MetaDisposable() @@ -205,6 +205,33 @@ func GroupEmojiPackController(context: AccountContext) -> InputDataController { return current } })) + if let info = selected { + let signal: Signal<(StickerPackCollectionInfo, StickerPackItem)?, NoError> = context.engine.stickers.loadedStickerPack(reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) |> map { value in + switch value { + case let .result(info, items, _): + if let item = items.first { + return (info, item) + } else { + return nil + } + default: + return nil + } + } + actionsDisposable.add(signal.start(next: { selected in + updateState { current in + var current = current + if let selected = selected { + current.selected = .init(info: selected.0, id: selected.0.id, item: selected.1, count: selected.0.count) + current.string = selected.0.shortName + } + return current + } + })) + + } + + actionsDisposable.add(searchStatePromise.get().start(next: { state in @@ -257,6 +284,7 @@ func GroupEmojiPackController(context: AccountContext) -> InputDataController { } return current } + updated(stateValue.with { $0.selected?.info }) closeSearch?() }, source: stateValue.with { $0.selected?.id == item.id } ? .removeGroupEmojiPack : .installGroupEmojiPack), for: context.window) @@ -298,6 +326,7 @@ func GroupEmojiPackController(context: AccountContext) -> InputDataController { } }) + let signal = combineLatest(statePromise.get(), searchPromise.get()) |> deliverOnPrepareQueue |> map { state, searchData in return InputDataSignalValue(entries: entries(state, arguments: arguments), searchState: searchData) } @@ -325,6 +354,9 @@ func GroupEmojiPackController(context: AccountContext) -> InputDataController { return bar }) + controller.afterDisappear = { + } + controller.searchKeyInvocation = { updateSearchValue { current in diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index fb29a6324..62ea4a8fa 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259084 + 259142 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/LottieLocalAnimations.swift b/Telegram-Mac/LottieLocalAnimations.swift index 07e2e43e5..3f407052a 100644 --- a/Telegram-Mac/LottieLocalAnimations.swift +++ b/Telegram-Mac/LottieLocalAnimations.swift @@ -281,7 +281,8 @@ enum LocalAnimatedSticker : String { case menu_lighting case menu_quote case menu_boost - + case menu_boost_plus + case emoji_category_activities case emoji_category_angry case emoji_category_arrow_to_search diff --git a/Telegram-Mac/MessagesPrivacyController.swift b/Telegram-Mac/MessagesPrivacyController.swift index 7e8dcd468..616f86a18 100644 --- a/Telegram-Mac/MessagesPrivacyController.swift +++ b/Telegram-Mac/MessagesPrivacyController.swift @@ -87,12 +87,12 @@ func MessagesPrivacyController(context: AccountContext, globalSettings: GlobalPr let arguments = Arguments(context: context, alert: { if !context.isPremium { showModalText(for: context.window, text: strings().privacySettingsMessagesPremiumError, button: strings().alertLearnMore, callback: { _ in - showModal(with: PremiumBoardingController(context: context, source: .messages_privacy, openFeatures: true), for: context.window) + showModal(with: PremiumBoardingController(context: context, source: .message_privacy, openFeatures: true), for: context.window) }) return } }, premium: { - showModal(with: PremiumBoardingController(context: context, source: .messages_privacy, openFeatures: true), for: context.window) + showModal(with: PremiumBoardingController(context: context, source: .message_privacy, openFeatures: true), for: context.window) }, toggle: { value in updateState { current in var current = current diff --git a/Telegram-Mac/PeerInfoHeadItem.swift b/Telegram-Mac/PeerInfoHeadItem.swift index a59efe141..1a53ac637 100644 --- a/Telegram-Mac/PeerInfoHeadItem.swift +++ b/Telegram-Mac/PeerInfoHeadItem.swift @@ -114,8 +114,6 @@ extension TelegramPeerPhoto : Equatable { } } -fileprivate let actionItemWidth: CGFloat = 145 -fileprivate let actionItemInsetWidth: CGFloat = 20 struct SubActionItem { let text: String @@ -131,6 +129,11 @@ struct SubActionItem { } final class ActionItem { + + static let actionItemWidth: CGFloat = 145 + static let actionItemInsetWidth: CGFloat = 20 + + let text: String let destruct: Bool let image: CGImage @@ -150,9 +153,9 @@ final class ActionItem { self.subItems = subItems self.destruct = destruct self.textLayout = TextViewLayout(.initialize(string: text, color: color, font: .normal(.text)), alignment: .center) - self.textLayout.measure(width: actionItemWidth) + self.textLayout.measure(width: ActionItem.actionItemWidth) - self.size = NSMakeSize(actionItemWidth, image.backingSize.height + textLayout.layoutSize.height + 10) + self.size = NSMakeSize(ActionItem.actionItemWidth, image.backingSize.height + textLayout.layoutSize.height + 10) } } @@ -163,7 +166,7 @@ private func actionItems(item: PeerInfoHeadItem, width: CGFloat, theme: Telegram var rowItemsCount: Int = 1 - while width - (actionItemWidth + actionItemInsetWidth) > ((actionItemWidth * CGFloat(rowItemsCount)) + (CGFloat(rowItemsCount - 1) * actionItemInsetWidth)) { + while width - (ActionItem.actionItemWidth + ActionItem.actionItemInsetWidth) > ((ActionItem.actionItemWidth * CGFloat(rowItemsCount)) + (CGFloat(rowItemsCount - 1) * ActionItem.actionItemInsetWidth)) { rowItemsCount += 1 } rowItemsCount = min(rowItemsCount, 4) @@ -1375,9 +1378,9 @@ private final class PeerInfoHeadView : GeneralRowView { return 0 } - let width = (item.blockWidth - (actionItemInsetWidth * CGFloat(items.count - 1))) + let width = (item.blockWidth - (ActionItem.actionItemInsetWidth * CGFloat(items.count - 1))) - return max(actionItemWidth, min(170, width / CGFloat(items.count))) + return max(ActionItem.actionItemWidth, min(170, width / CGFloat(items.count))) } private func layoutActionItems(_ items: [ActionItem], animated: Bool) { @@ -1397,7 +1400,7 @@ private final class PeerInfoHeadView : GeneralRowView { let actionItemWidth = _actionItemWidth(items) - actionsView.change(size: NSMakeSize(actionItemWidth * CGFloat(items.count) + CGFloat(items.count - 1) * actionItemInsetWidth, maxActionSize.height), animated: animated) + actionsView.change(size: NSMakeSize(actionItemWidth * CGFloat(items.count) + CGFloat(items.count - 1) * ActionItem.actionItemInsetWidth, maxActionSize.height), animated: animated) var x: CGFloat = inset @@ -1406,7 +1409,7 @@ private final class PeerInfoHeadView : GeneralRowView { view.updateAndLayout(item: item, bgColor: rowItem.actionColor) view.setFrameSize(NSMakeSize(actionItemWidth, maxActionSize.height)) view.change(pos: NSMakePoint(x, 0), animated: false) - x += actionItemWidth + actionItemInsetWidth + x += actionItemWidth + ActionItem.actionItemInsetWidth } } else { diff --git a/Telegram-Mac/PremiumBoardingController.swift b/Telegram-Mac/PremiumBoardingController.swift index f8dce5f08..9b9b47636 100644 --- a/Telegram-Mac/PremiumBoardingController.swift +++ b/Telegram-Mac/PremiumBoardingController.swift @@ -62,7 +62,7 @@ enum PremiumLogEventsSource : Equatable { case no_ads case recommended_channels case last_seen - case messages_privacy + case message_privacy case saved_tags var value: String { switch self { @@ -108,8 +108,8 @@ enum PremiumLogEventsSource : Equatable { return "recommended_channels" case .last_seen: return "last_seen" - case .messages_privacy: - return "messages_privacy" + case .message_privacy: + return "message_privacy" case .saved_tags: return "saved_tags" } @@ -157,8 +157,8 @@ enum PremiumLogEventsSource : Equatable { return .saved_tags case .last_seen: return .last_seen - case .messages_privacy: - return .messages_privacy + case .message_privacy: + return .message_privacy } } @@ -257,7 +257,7 @@ enum PremiumValue : String { case peer_colors case saved_tags case last_seen - case messages_privacy + case message_privacy func gradient(_ index: Int) -> [NSColor] { let colors:[NSColor] = [ NSColor(rgb: 0xef6922), NSColor(rgb: 0xe95a2c), @@ -356,7 +356,7 @@ enum PremiumValue : String { return NSImage(named: "Icon_Premium_Boarding_Tag")!.precomposed(presentation.colors.accent) case .last_seen: return NSImage(named: "Icon_Premium_Boarding_LastSeen")!.precomposed(presentation.colors.accent) - case .messages_privacy: + case .message_privacy: return NSImage(named: "Icon_Premium_Boarding_MessagePrivacy")!.precomposed(presentation.colors.accent) } } @@ -399,7 +399,7 @@ enum PremiumValue : String { return strings().premiumBoardingSavedTagsTitle case .last_seen: return strings().premiumBoardingLastSeenTitle - case .messages_privacy: + case .message_privacy: return strings().premiumBoardingMessagePrivacyTitle } } @@ -441,7 +441,7 @@ enum PremiumValue : String { return strings().premiumBoardingSavedTagsInfo case .last_seen: return strings().premiumBoardingLastSeenInfo - case .messages_privacy: + case .message_privacy: return strings().premiumBoardingMessagePrivacyInfo } } @@ -450,7 +450,7 @@ enum PremiumValue : String { private struct State : Equatable { - var values:[PremiumValue] = [.double_limits, .stories, .more_upload, .faster_download, .voice_to_text, .no_ads, .infinite_reactions, .emoji_status, .premium_stickers, .animated_emoji, .advanced_chat_management, .profile_badge, .animated_userpics, .translations, .saved_tags, .last_seen, .messages_privacy] + var values:[PremiumValue] = [.double_limits, .stories, .more_upload, .faster_download, .voice_to_text, .no_ads, .infinite_reactions, .emoji_status, .premium_stickers, .animated_emoji, .advanced_chat_management, .profile_badge, .animated_userpics, .translations, .saved_tags, .last_seen, .message_privacy] let source: PremiumLogEventsSource var premiumProduct: InAppPurchaseManager.Product? diff --git a/Telegram-Mac/PremiumBoardingFeaturesController.swift b/Telegram-Mac/PremiumBoardingFeaturesController.swift index acbbcfffa..aab67ae71 100644 --- a/Telegram-Mac/PremiumBoardingFeaturesController.swift +++ b/Telegram-Mac/PremiumBoardingFeaturesController.swift @@ -265,9 +265,9 @@ final class PremiumBoardingFeaturesView: View { slideView.addSlide(lastSeen) let messagesPrivacy = PremiumFeatureSlideView(frame: slideView.bounds, presentation: presentation) - messagesPrivacy.setup(context: context, type: .messages_privacy, decoration: .badgeStars, getView: { _ in + messagesPrivacy.setup(context: context, type: .message_privacy, decoration: .badgeStars, getView: { _ in let view = PremiumDemoLegacyPhoneView(frame: .zero) - view.setup(context: context, video: configuration.videos[PremiumValue.messages_privacy.rawValue], position: .top) + view.setup(context: context, video: configuration.videos[PremiumValue.message_privacy.rawValue], position: .top) return view }) slideView.addSlide(messagesPrivacy) @@ -309,7 +309,7 @@ final class PremiumBoardingFeaturesView: View { slideView.displaySlide(at: 16, animated: false) case .last_seen: slideView.displaySlide(at: 17, animated: false) - case .messages_privacy: + case .message_privacy: slideView.displaySlide(at: 18, animated: false) } diff --git a/Telegram-Mac/PremiumLimitConfig.swift b/Telegram-Mac/PremiumLimitConfig.swift index ff66dc625..7ba382d65 100644 --- a/Telegram-Mac/PremiumLimitConfig.swift +++ b/Telegram-Mac/PremiumLimitConfig.swift @@ -30,8 +30,8 @@ final class PremiumPromoOrder { if !premiumValues.contains(.last_seen) { premiumValues.append(.last_seen) } - if !premiumValues.contains(.messages_privacy) { - premiumValues.append(.messages_privacy) + if !premiumValues.contains(.message_privacy) { + premiumValues.append(.message_privacy) } #endif } diff --git a/Telegram-Mac/SelectColorController.swift b/Telegram-Mac/SelectColorController.swift index 43035947a..353d0506f 100644 --- a/Telegram-Mac/SelectColorController.swift +++ b/Telegram-Mac/SelectColorController.swift @@ -89,7 +89,7 @@ private class ProfilePreviewRowItem : GeneralRowItem { fileprivate let nameLayout: TextViewLayout fileprivate let statusLayout: TextViewLayout fileprivate let getColor:(PeerNameColor)->PeerNameColors.Colors - init(_ initialSize: NSSize, stableId: AnyHashable, peer: Peer, subscribers: Int?, nameColor: PeerNameColor?, backgroundEmojiId: Int64?, context: AccountContext, theme: TelegramPresentationTheme, viewType: GeneralViewType, getColor:@escaping(PeerNameColor)->PeerNameColors.Colors) { + init(_ initialSize: NSSize, stableId: AnyHashable, peer: Peer, subscribers: Int?, isGroup: Bool, nameColor: PeerNameColor?, backgroundEmojiId: Int64?, context: AccountContext, theme: TelegramPresentationTheme, viewType: GeneralViewType, getColor:@escaping(PeerNameColor)->PeerNameColors.Colors) { self.theme = theme self.peer = peer self.getColor = getColor @@ -109,7 +109,11 @@ private class ProfilePreviewRowItem : GeneralRowItem { let status: String if let subscribers = subscribers { - status = strings().peerStatusSubscribersCountable(subscribers) + if isGroup { + status = strings().peerStatusMemberCountable(subscribers) + } else { + status = strings().peerStatusSubscribersCountable(subscribers) + } } else { status = strings().peerStatusRecently } @@ -799,6 +803,12 @@ private struct State : Equatable { var wallpaper: TelegramWallpaper? var actualWallpaper: TelegramWallpaper? + + var emojiPack: StickerPackCollectionInfo? + var actualEmojiPack: StickerPackCollectionInfo? + + + var icon_emojiPack: CGImage? func isSame(to peer: Peer) -> Bool { if peer.profileColor != selected_profile { @@ -819,6 +829,9 @@ private struct State : Equatable { if wallpaper != actualWallpaper { return false } + if emojiPack != actualEmojiPack { + return false + } return true } } @@ -858,8 +871,10 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { var afterNameImage_NameIcon: CGImage? = nil var afterNameImage_ProfileIcon: CGImage? = nil var afterNameImage_WallpaperIcon: CGImage? = nil + var afterNameImage_EmojiPack: CGImage? = nil - if state.peer._asPeer().isChannel { + switch arguments.source { + case .channel: if state.boostLevel < arguments.premiumConfiguration.minChannelEmojiStatusLevel { afterNameImage_emojiStatus = generateDisclosureActionBoostLevelBadgeImage(text: strings().boostBadgeLevel(Int(arguments.premiumConfiguration.minChannelEmojiStatusLevel))) } @@ -872,6 +887,21 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { if state.boostLevel < arguments.premiumConfiguration.minChannelWallpaperLevel { afterNameImage_WallpaperIcon = generateDisclosureActionBoostLevelBadgeImage(text: strings().boostBadgeLevel(Int(arguments.premiumConfiguration.minChannelWallpaperLevel))) } + case .group: + if state.boostLevel < arguments.premiumConfiguration.minGroupEmojiStatusLevel { + afterNameImage_emojiStatus = generateDisclosureActionBoostLevelBadgeImage(text: strings().boostBadgeLevel(Int(arguments.premiumConfiguration.minGroupEmojiStatusLevel))) + } + if state.boostLevel < arguments.premiumConfiguration.minGroupProfileIconLevel { + afterNameImage_ProfileIcon = generateDisclosureActionBoostLevelBadgeImage(text: strings().boostBadgeLevel(Int(arguments.premiumConfiguration.minGroupProfileIconLevel))) + } + if state.boostLevel < arguments.premiumConfiguration.minGroupWallpaperLevel { + afterNameImage_WallpaperIcon = generateDisclosureActionBoostLevelBadgeImage(text: strings().boostBadgeLevel(Int(arguments.premiumConfiguration.minGroupWallpaperLevel))) + } + if state.boostLevel < arguments.premiumConfiguration.minGroupEmojiPackLevel { + afterNameImage_EmojiPack = generateDisclosureActionBoostLevelBadgeImage(text: strings().boostBadgeLevel(Int(arguments.premiumConfiguration.minGroupEmojiPackLevel))) + } + default: + break } switch arguments.source { @@ -978,16 +1008,20 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { index += 1 let previewViewType: GeneralViewType + let isGroup: Bool switch arguments.source { case .group: previewViewType = .singleItem + isGroup = true default: previewViewType = .firstItem + isGroup = false } + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_preview_profile, equatable: .init(state), comparable: nil, item: { initialSize, stableId in - return ProfilePreviewRowItem(initialSize, stableId: stableId, peer: arguments.source.peer, subscribers: state.subscribers, nameColor: state.selected_profile, backgroundEmojiId: state.backgroundEmojiId_profile, context: arguments.context, theme: theme, viewType: previewViewType, getColor: { + return ProfilePreviewRowItem(initialSize, stableId: stableId, peer: arguments.source.peer, subscribers: state.subscribers, isGroup: isGroup, nameColor: state.selected_profile, backgroundEmojiId: state.backgroundEmojiId_profile, context: arguments.context, theme: theme, viewType: previewViewType, getColor: { arguments.getColor($0, .profile) }) })) @@ -1107,7 +1141,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { let emojiPackInfo: String = strings().selectColorEmojiPackInfoGroup let statusType: GeneralInteractedType - if let icon = state.icon_emojiStatus { + if let icon = state.icon_emojiPack { statusType = .imageContext(icon, "") } else { statusType = .nextContext("") @@ -1115,7 +1149,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_emoji_pack, data: .init(name: title, color: theme.colors.text, icon: nil, type: statusType, viewType: .singleItem, enabled: true, action: { arguments.showEmojiPack() - }, afterNameImage: afterNameImage_emojiStatus))) + }, afterNameImage: afterNameImage_EmojiPack))) entries.append(.desc(sectionId: sectionId, index: index, text: .plain(emojiPackInfo), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) index += 1 default: @@ -1236,6 +1270,14 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c statePromise.set(stateValue.modify (f)) } + let isGroup: Bool + switch source { + case .group: + isGroup = true + default: + isGroup = false + } + callback?.getState = { return stateValue.with { ($0.selected, $0.backgroundEmojiId) } } @@ -1260,13 +1302,16 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c } } - let wallpaper = getCachedDataView(peerId: peerId, postbox: context.account.postbox) + let cachedData = getCachedDataView(peerId: peerId, postbox: context.account.postbox) |> map { $0 as? CachedChannelData } - |> map { $0?.wallpaper } + |> map { $0 } |> take(1) - actionsDisposable.add(wallpaper.start(next: { wallpaper in + actionsDisposable.add(cachedData.start(next: { cachedData in + let wallpaper = cachedData?.wallpaper + let emojiPack = cachedData?.emojiPack + updateState { current in var current = current if let wallpaper = wallpaper { @@ -1276,6 +1321,8 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c } current.wallpaper = wallpaper current.actualWallpaper = wallpaper + current.emojiPack = emojiPack + current.actualEmojiPack = emojiPack return current } })) @@ -1389,6 +1436,52 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c emojiStatus = state.emojiStatus })) + + var emojiPack_Layer: InlineStickerItemLayer? + + let emojiPack: Signal<(StickerPackCollectionInfo, StickerPackItem)?, NoError> = statePromise.get() |> map { $0.emojiPack } |> distinctUntilChanged |> mapToSignal { info in + if let info = info { + return context.engine.stickers.loadedStickerPack(reference: StickerPackReference.id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) |> mapToSignal { result in + switch result { + case let .result(info, items, _): + if let item = items.first { + return .single((info, item)) + } else { + return .single(nil) + } + default: + return .complete() + } + } + } else { + return .single(nil) + } + } + + actionsDisposable.add(emojiPack.start(next: { info in + DispatchQueue.main.async { + if let item = info?.1 { + emojiPack_Layer = InlineStickerItemLayer(account: context.account, file: item.file, size: NSMakeSize(25, 25), playPolicy: .framesCount(1), textColor: theme.colors.accent) + emojiPack_Layer?.isPlayable = true + + emojiPack_Layer?.contentDidUpdate = { image in + updateState { current in + var current = current + current.icon_emojiPack = image + return current + } + } + } else { + emojiPack_Layer = nil + updateState { current in + var current = current + current.icon_emojiPack = nil + return current + } + } + } + })) + let subscribers = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ParticipantCount(id: source.peerId)) actionsDisposable.add(subscribers.start(next: { value in @@ -1475,7 +1568,13 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c showPopover(for: emojiControl, with: emojis) } }, showEmojiPack: { - context.bindings.rootNavigation().push(GroupEmojiPackController(context: context)) + context.bindings.rootNavigation().push(GroupEmojiPackController(context: context, peerId: peerId, selected: stateValue.with { $0.emojiPack }, updated: { info in + updateState { current in + var current = current + current.emojiPack = info + return current + } + })) }, removeIcon: { type in updateState { current in var current = current @@ -1564,7 +1663,7 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c let status = stateValue.with { $0.status } let myBoost = stateValue.with { $0.myStatus } if let status = status { - showModal(with: BoostChannelModalController(context: context, peer: source.peer, boosts: status, myStatus: myBoost), for: context.window) + showModal(with: BoostChannelModalController(context: context, peer: source.peer, boosts: status, myStatus: myBoost, infoOnly: true), for: context.window) } }) @@ -1573,7 +1672,7 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c } - let controller = InputDataController(dataSignal: signal, title: strings().telegramAppearanceViewController, hasDone: true, doneString: { + let controller = InputDataController(dataSignal: signal, title: strings().telegramAppearanceViewController, removeAfterDisappear: false, hasDone: true, doneString: { return strings().selectColorApply }) @@ -1582,6 +1681,8 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c } + + let invoke:()->Void = { let state = stateValue.with { $0 } @@ -1596,6 +1697,9 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c let wallpaper = state.wallpaper let actualWallpaper = state.actualWallpaper + let emojiPack = state.emojiPack + let actualEmojiPack = state.actualEmojiPack + let request:(@escaping()->Void)->Void = { f in @@ -1613,6 +1717,10 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c case .channel, .group: signals.append(context.engine.peers.updatePeerNameColorAndEmoji(peerId: peerId, nameColor: nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: profileColor, profileBackgroundEmojiId: profileBackgroundEmojiId) |> ignoreValues |> `catch` { _ in return Signal.complete() }) + + if emojiPack != actualEmojiPack { + signals.append(context.engine.peers.updateGroupSpecificEmojiset(peerId: peerId, info: emojiPack) |> ignoreValues |> `catch` { _ in return Signal.complete() }) + } } if emojiStatus?.fileId != state.peer.emojiStatus?.fileId { @@ -1646,16 +1754,49 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c let stats = stateValue.with { $0.status } let myStatus = stateValue.with { $0.myStatus } - let nameColorLevel = arguments.premiumConfiguration.minChannelNameColorLevel - let nameIconLevel = arguments.premiumConfiguration.minChannelNameIconLevel + let nameColorLevel: Int32 + let nameIconLevel: Int32 - let profileColorLevel = arguments.premiumConfiguration.minChannelProfileColorLevel - let profileIconLevel = arguments.premiumConfiguration.minChannelProfileIconLevel + let profileColorLevel: Int32 + let profileIconLevel: Int32 - let emojiStatusLevel = arguments.premiumConfiguration.minChannelEmojiStatusLevel + let emojiStatusLevel: Int32 + let emojiPackLevel: Int32 - let wallpaperLevel = arguments.premiumConfiguration.minChannelWallpaperLevel - let customWallpaperLevel = arguments.premiumConfiguration.minChannelCustomWallpaperLevel + let wallpaperLevel: Int32 + let customWallpaperLevel: Int32 + + + switch source { + case .channel: + + emojiPackLevel = 0 + + + nameColorLevel = arguments.premiumConfiguration.minChannelNameColorLevel + nameIconLevel = arguments.premiumConfiguration.minChannelNameIconLevel + + profileColorLevel = arguments.context.peerNameColors.nameColorsChannelMinRequiredBoostLevel[stateValue.with { $0.selected_profile?.rawValue ?? 0 }] ?? 0 + profileIconLevel = arguments.premiumConfiguration.minChannelProfileIconLevel + + emojiStatusLevel = arguments.premiumConfiguration.minChannelEmojiStatusLevel + wallpaperLevel = arguments.premiumConfiguration.minChannelWallpaperLevel + customWallpaperLevel = arguments.premiumConfiguration.minChannelCustomWallpaperLevel + case .group: + nameColorLevel = 0 + nameIconLevel = 0 + + profileColorLevel = arguments.context.peerNameColors.nameColorsGroupMinRequiredBoostLevel[stateValue.with { $0.selected_profile?.rawValue ?? 0 }] ?? 0 + profileIconLevel = arguments.premiumConfiguration.minGroupProfileIconLevel + + emojiStatusLevel = arguments.premiumConfiguration.minGroupEmojiStatusLevel + emojiPackLevel = arguments.premiumConfiguration.minGroupEmojiPackLevel + + wallpaperLevel = arguments.premiumConfiguration.minGroupWallpaperLevel + customWallpaperLevel = arguments.premiumConfiguration.minGroupCustomWallpaperLevel + default: + fatalError() + } if let stats = stats { if wallpaper != actualWallpaper { @@ -1685,16 +1826,24 @@ func SelectColorController(context: AccountContext, source: SelectColorSource, c showModal(with: BoostChannelModalController(context: context, peer: peer, boosts: stats, myStatus: myStatus, infoOnly: true, source: .profileIcon(profileIconLevel)), for: context.window) } else if stats.level < emojiStatusLevel, emojiStatus?.fileId != peer.emojiStatus?.fileId { showModal(with: BoostChannelModalController(context: context, peer: peer, boosts: stats, myStatus: myStatus, infoOnly: true, source: .emojiStatus(emojiStatusLevel)), for: context.window) + } else if stats.level < emojiPackLevel, emojiPack != actualEmojiPack { + showModal(with: BoostChannelModalController(context: context, peer: peer, boosts: stats, myStatus: myStatus, infoOnly: true, source: .emojiPack(emojiStatusLevel)), for: context.window) } else { request({ - showModalText(for: context.window, text: strings().selectColorSuccessChannel) + let text: String + if isGroup { + text = strings().selectColorSuccessAppearanceGroup + } else { + text = strings().selectColorSuccessAppearanceChannel + } + showModalText(for: context.window, text: text) }) } } case .account: if context.isPremium { request({ - showModalText(for: context.window, text: strings().selectColorSuccessUser) + showModalText(for: context.window, text: strings().selectColorSuccessAppearanceUser) }) } else { diff --git a/Telegram-Mac/SelectSizeRowItem.swift b/Telegram-Mac/SelectSizeRowItem.swift index 5b3f2a0d1..f8d130b21 100644 --- a/Telegram-Mac/SelectSizeRowItem.swift +++ b/Telegram-Mac/SelectSizeRowItem.swift @@ -14,15 +14,17 @@ import TGUIKit class SelectSizeRowItem: GeneralRowItem { fileprivate let titles:[String]? + fileprivate let titlesImages:[CGImage]? fileprivate let sizes: [Int32] fileprivate var current: Int32 fileprivate let initialCurrent: Int32 fileprivate let selectAction:(Int)->Void fileprivate let hasMarkers: Bool fileprivate let dottedIndexes: [Int] - init(_ initialSize: NSSize, stableId: AnyHashable, current: Int32, sizes: [Int32], hasMarkers: Bool, titles:[String]? = nil, dottedIndexes:[Int] = [], viewType: GeneralViewType = .legacy, selectAction: @escaping(Int)->Void) { + init(_ initialSize: NSSize, stableId: AnyHashable, current: Int32, sizes: [Int32], hasMarkers: Bool, titles:[String]? = nil, titlesImages: [CGImage]? = nil, dottedIndexes:[Int] = [], viewType: GeneralViewType = .legacy, selectAction: @escaping(Int)->Void) { self.sizes = sizes self.titles = titles + self.titlesImages = titlesImages self.dottedIndexes = dottedIndexes self.initialCurrent = current self.hasMarkers = hasMarkers @@ -174,7 +176,13 @@ private class SelectSizeRowView : TableRowView, ViewDisplayDelegate { if let titles = item.titles, titles.count == item.sizes.count { let title = titles[i] let titleNode = TextNode.layoutText(.initialize(string: title, color: theme.colors.text, font: .normal(.short)), backdorColor, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) - titleNode.1.draw(NSMakeRect(min(max(point.x - titleNode.0.size.width / 2 + 3, minX), frame.width - titleNode.0.size.width - minX), point.y - 15 - titleNode.0.size.height, titleNode.0.size.width, titleNode.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + let titleRect = NSMakeRect(min(max(point.x - titleNode.0.size.width / 2 + 3, minX), frame.width - titleNode.0.size.width - minX), point.y - 15 - titleNode.0.size.height, titleNode.0.size.width, titleNode.0.size.height) + + if let image = item.titlesImages?[i] { + ctx.draw(image, in: CGRect(origin: NSMakePoint(titleRect.minX - image.backingSize.width, titleRect.minY), size: image.backingSize)) + } + + titleNode.1.draw(titleRect, in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } } @@ -182,6 +190,7 @@ private class SelectSizeRowView : TableRowView, ViewDisplayDelegate { let perSize = NSMakeSize(10, 10) let perF = _focus(perSize) let titleNode = TextNode.layoutText(.initialize(string: title, color: theme.colors.text, font: .normal(.short)), backdorColor, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) + titleNode.1.draw(NSMakeRect(_focus(titleNode.0.size).minX, perF.minY - 15 - titleNode.0.size.height, titleNode.0.size.width, titleNode.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } @@ -290,6 +299,12 @@ private class SelectSizeRowView : TableRowView, ViewDisplayDelegate { if i == titles.count - 1 { rect.origin.x = min(rect.minX, (point.x + 5) - titleNode.0.size.width) } + + if let image = item.titlesImages?[i] { + ctx.draw(image, in: CGRect(origin: NSMakePoint(rect.minX - image.backingSize.width + 4, rect.minY), size: image.backingSize)) + rect.origin.x += 6 + } + titleNode.1.draw(rect, in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index 7139fced638d52ad3639aff07a99a82d5699978d..60f8363327691707468df8b6ff7c3b7f975b52a3 100644 GIT binary patch delta 2826 zcma)8ZERE58NT=0_rvF$xfkO&2{^{kz#@}oUPkHo7z=3#UI`f!3my)=i$ts#w{NNsAB)Tg0spZ9}JO8|^B(M*A_bHmxQoX_{tbt(rm8z4w|W zjxpPQ*!SM|oaa5y`*EIge*VE%pF3aKos^SG(v@^4)ufixlSa}rz{1yNZIO0LZPGre zo}LHj*NP{))M5-zG82Q>SX6BxV+YQjkPERSD16v;m8D_+QZZp(XQJ>9ytI)32eDt~ z#b`LAYpLiHZd_Klz|b`o%;&BdZ#<s2vw)iM-Z;h<;rUS(M2E}+Ie`s~u~nWHCykv_0*gOn zM`pST;a?LhXia{|2IMJ!Rd;c9DHl6(4-etf3hqbzK~-RQp<%|olTFvY4*p=xC=)UiR-Ka>5tiur(Idp3_fM5xV2K7ar{I_jaeW>^+ zyE2Wn8!2Y(`Gh^AV&kJO6XW$pDZ)2dIf5VQ2D)eQAgaD#48OX`d>H;M<>294O!G|d zgCjlE@{K!W_RIzW_vaZTrK_1@Hb*I*|G*TJS(Ii@FF{|g9B{U!RWoT9putYq(kkKqQtjX3Hh>79H&R(^w<2ydb~j=1<77mZ={i>$;&k>lly zN-?^VEaTbs6t+fHenIKkLTxbgdnE{SkJzy|B1Lkj(|#daBr6C<52bdTUGDh`Hgwrg z(m_1$Ay2VVQSH20RUSSTM)OTIfDOeo()VlGl$#)bqRd9(s_utaOfqsZ!XueUjki=S zb@(A;O7Z;vZFaIp_B$G6OZ1GH7Wv&g8-G|uZFrC};+oRp<>#o+8@!~$1Izdl>-(#C zo1U5X@EX#Wk*(CdhhOywJh@pmtY_EogOsfpFT%D?V<}F4oBI%o5#7l@>S4UJmNSgL zrHI}xmEmxVH)FEX2qC?bc2u@Kj9qJa4SsV;59I@i*T{Ycs2QUNjBq~tE3fGb?zER- zdbYtCrH)6G93wtm z>^3pBl|dAUO#{r{Z9l3GTsQ)pw5{dgr-O}s0KUH^~;leup z0^+~$1X8cN1s<>GG5oYg4x~<3dQ&xXO?i`F|2i0?WvW!dIE z=URxw6vB=sYt%v5f#llk7;ZMpEx%E_s8 z0B1w4LTsxLA**t^*d?R;0a4alMpdj`LBZKA0UMsR2+=RG(ksN|G7LZB(a>M3d2xKg z?MHCXWz4d#J-Z#@Q`-s7TXF=WDp{%h{T4%eO7v?atJ&wAXp>AX^JgO&Eo#?p6kIOf@{)>3m4gY@+04 z1vv4JqUpB$?~~$aI-wNca*%m@i|ESX9BWfC&A%%zsX}US;HT2WueFhGgxkNP`gJ-V r?SlkOwfbxf9^#sd7~`8qSz+b|VADb0xDmwh9kS1;kQPWQBqseE`QU=9 delta 666 zcmZvYK}b|l7=_<^^Jd{AcW0RI*R^X}PV2urX;)(1(QVAWr%>5e zKl=MAp}0;l%zmOkMOGR6ls%&D(;Bs?b{uUR^bnaXilF$NNT$}Q$}2a&(V~utuN20} zmYODi&?6o8Q4vLClLV@_DI|M-(U6YbEvnynij@)tQFv*P%$I2TUnTD0dSv^(gu)4* zz=FYHnJ~D;i{=kR7Rqw9y+~y{XM}c8VvWZ{Z#M@q+ocH0DHfP` z=MgA0c|&r%i%U8hF7uLHNps$o=^W3zmEm@gw=j58_)+W>Hj* zWjnWB?95yj=wdHHFmTg!lg-0{?aTv3|NBy;6kB;- zb<4hE)7hM}ZT)@O`tL6%@p=z$#aaKIO>J20;rQ6M^`eJWx)!AynDoLL|54+#4Gc>^ z{TqByubj1sEJ`Fu4vAz|R#{aT?q8MSa!#5WjDt^5uTyo&n z_D-~Bw3Czyr`UirB14Ekd0HC{iCdWjA_=KRBuHEY)?lZIhKkW@dB7_5iZ@lHwhA&< zL~Chx!Y1GV%VCpfBB>=1iH|IXG2>_htrEXMAk#RWhfz=*DU5T>-I>o@gTbrJ84HY= z8827nI$chi%1>1;T3e{ZL68t*kDpTHs;}MPwbKSJKVOgns)}2_wuIdcUz2myrLRZC ztFg;tQePyoV- zO!z{XV0F$#k=eqpsxxC@s)KCtW~nT%N?$1|S0QwHfe?U!8>EcD!s2MKwhE!Ko_CIi z%)s0GMCS^ThH41N<)|q&W@3p>0Z7?Zng}u0r7H;%s`Oxh%todW0H=T++}}D!k;yC1 zsdP4hRg@y*N(QOc2sJIM3skO*4UiCloF-yk5|c%;%T(D{19+$774S5YM(4Td`n)oJcw+HmwAHRQ?{JmWC zO*?t;cj81$KDEv9017L(2A}I~x5OLPUxv?HhU<>o7`(vY3a4#syvZ+{shoN#xJMs% zC`5BUuD9Fk=6q`F!)m<%gS^gFdwPFsA1>Oe`9S%y=ZYQIsMm^2B()7u#oTKC+-;JD zUv_hClJ`tkUz6l9G=H`AzI$%k9rKOLdBeItU^BT0O=WOmXicto?>va}i+;PKo^a6* zQSq)#8fcw1(CbCBzf1EBr;#gDWcjbyIGejS&d>nfy>qT^bCFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259084 + 259142 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index 7e343df65..70bbc6a34 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -1891,16 +1891,10 @@ public final class L10n { public static var channelBlacklistRestricted: String { return L10n.tr("Localizable", "ChannelBlacklist.Restricted") } /// Boost Channel public static var channelBoostBoostChannel: String { return L10n.tr("Localizable", "ChannelBoost.BoostChannel") } - /// %1$@ needed to be able to post %2$@. - public static func channelBoostBoostedChannelMoreRequired(_ p1: String, _ p2: String) -> String { - return L10n.tr("Localizable", "ChannelBoost.BoostedChannelMoreRequired", p1, p2) - } - /// This channel reached **Level %1$@** and can now post %2$@. - public static func channelBoostBoostedChannelReachedLevel(_ p1: String, _ p2: String) -> String { - return L10n.tr("Localizable", "ChannelBoost.BoostedChannelReachedLevel", p1, p2) - } /// Boost Group public static var channelBoostBoostGroup: String { return L10n.tr("Localizable", "ChannelBoost.BoostGroup") } + /// Use Emoji Pack + public static var channelBoostEmojiPack: String { return L10n.tr("Localizable", "ChannelBoost.EmojiPack") } /// Use Emoji Statuses public static var channelBoostEmojiStatus: String { return L10n.tr("Localizable", "ChannelBoost.EmojiStatus") } /// Enable Colors @@ -1965,26 +1959,12 @@ public final class L10n { } /// Help Upgrade This Group public static var channelBoostHelpUpgradeGroup: String { return L10n.tr("Localizable", "ChannelBoost.HelpUpgradeGroup") } - /// Increase Story Limit - public static var channelBoostIncreaseLimit: String { return L10n.tr("Localizable", "ChannelBoost.IncreaseLimit") } - /// Your channel needs %1$@ to post %2$@.\n\nAsk your **Premium** subscribers to boost your channel with this link: - public static func channelBoostIncreaseLimitText(_ p1: String, _ p2: String) -> String { - return L10n.tr("Localizable", "ChannelBoost.IncreaseLimitText", p1, p2) - } /// Level %@ public static func channelBoostLevel(_ p1: String) -> String { return L10n.tr("Localizable", "ChannelBoost.Level", p1) } /// Maximum Level Reached public static var channelBoostMaxLevelReached: String { return L10n.tr("Localizable", "ChannelBoost.MaxLevelReached") } - /// This channel reached **Level %1$@** and can now post %2$@. - public static func channelBoostMaxLevelReachedText(_ p1: String, _ p2: String) -> String { - return L10n.tr("Localizable", "ChannelBoost.MaxLevelReachedText", p1, p2) - } - /// %1$@ reached **Level %2$@** and can now post %3$@. - public static func channelBoostMaxLevelReachedTextAuthor(_ p1: String, _ p2: String, _ p3: String) -> String { - return L10n.tr("Localizable", "ChannelBoost.MaxLevelReachedTextAuthor", p1, p2, p3) - } /// %d public static func channelBoostMoreBoostsCountable(_ p1: Int) -> String { return L10n.tr("Localizable", "ChannelBoost.MoreBoosts_countable", p1) @@ -2059,6 +2039,22 @@ public final class L10n { } /// You Boosted The Channel public static var channelBoostYouBoostedOtherChannel: String { return L10n.tr("Localizable", "ChannelBoost.YouBoostedOtherChannel") } + /// By gaining boosts, your group levels and unlocks more features. + public static var channelBoostAdditionalFeaturesText: String { return L10n.tr("Localizable", "ChannelBoost.AdditionalFeatures.Text") } + /// Additional Features + public static var channelBoostAdditionalFeaturesTitle: String { return L10n.tr("Localizable", "ChannelBoost.AdditionalFeatures.Title") } + /// %1$@ more boosts needed to unlock new features. + public static func channelBoostBoostedChannelMoreRequiredNew(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.BoostedChannelMoreRequired.New", p1) + } + /// This channel reached **Level %1$@**. + public static func channelBoostBoostedChannelReachedLevelChannel(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.BoostedChannelReachedLevel.Channel", p1) + } + /// This group reached **Level %1$@**. + public static func channelBoostBoostedChannelReachedLevelGroup(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.BoostedChannelReachedLevel.Group", p1) + } /// %dm public static func channelBoostBoosterDuration(_ p1: Int) -> String { return L10n.tr("Localizable", "ChannelBoost.Booster.Duration", p1) @@ -2067,6 +2063,26 @@ public final class L10n { public static var channelBoostBoosterToBeDistributed: String { return L10n.tr("Localizable", "ChannelBoost.Booster.ToBeDistributed") } /// Unclaimed public static var channelBoostBoosterUnclaimed: String { return L10n.tr("Localizable", "ChannelBoost.Booster.Unclaimed") } + /// Your group needs **Level %1$@** to use emoji pack.\n\nAsk your **Premium** subscribers to boost your group with this link: + public static func channelBoostEnableEmojiStatusLevelTextGroup(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.EnableEmojiStatusLevelText.Group", p1) + } + /// Your group needs **Level %1$@** to change group cover color.\n\nAsk your **Premium** subscribers to boost your group with this link: + public static func channelBoostEnableProfileColorLevelTextGroup(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.EnableProfileColorLevelText.Group", p1) + } + /// Your group needs **Level %1$@** to change channel cover icon.\n\nAsk your **Premium** subscribers to boost your group with this link: + public static func channelBoostEnableProfileIconLevelTextGroup(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.EnableProfileIconLevelText.Group", p1) + } + /// Your group needs to reach **%1$@ level** to add %2$@ custom emoji as reactions.\n\nAsk your **Premium** subscribers to boost your group with this link: + public static func channelBoostEnableReactionsTextGroup(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.EnableReactionsText.Group", p1, p2) + } + /// Your group needs **%1$@ level** to change wallpaper.\n\nAsk your **Premium** subscribers to boost your group with this link: + public static func channelBoostEnableWallpapersTextGroup(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.EnableWallpapersText.Group", p1) + } /// You can change the channel you boost only once a day. Next time you can boost is in **%@**. public static func channelBoostErrorBoostTooOftenText(_ p1: String) -> String { return L10n.tr("Localizable", "ChannelBoost.Error.BoostTooOftenText", p1) @@ -2083,6 +2099,22 @@ public final class L10n { public static var channelBoostErrorPremiumNeededTitle: String { return L10n.tr("Localizable", "ChannelBoost.Error.PremiumNeededTitle") } /// Read More public static var channelBoostErrorPremiumNeededTextOK: String { return L10n.tr("Localizable", "ChannelBoost.Error.PremiumNeededText.OK") } + /// Your channel needs %1$@ to get access to featrues.\n\nAsk your **Premium** subscribers to boost your channel with this link: + public static func channelBoostIncreaseLimitTextChannel(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.IncreaseLimitText.Channel", p1) + } + /// Your group needs %1$@ to get access to featrues.\n\nAsk your **Premium** subscribers to boost your group with this link: + public static func channelBoostIncreaseLimitTextGroup(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.IncreaseLimitText.Group", p1) + } + /// This channel reached **Level %1$@** and have access to all features. + public static func channelBoostMaxLevelReachedTextChannel(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.MaxLevelReachedText.Channel", p1) + } + /// This group reached **Level %1$@** and have access to all features. + public static func channelBoostMaxLevelReachedTextGroup(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.MaxLevelReachedText.Group", p1) + } /// Voice-To-Text Conversion public static var channelBoostTableAudioTranscription: String { return L10n.tr("Localizable", "ChannelBoost.Table.AudioTranscription") } /// %d @@ -2291,10 +2323,78 @@ public final class L10n { public static func channelBoostTableWallpaperZero(_ p1: Int) -> String { return L10n.tr("Localizable", "ChannelBoost.Table.Wallpaper_zero", p1) } + /// Custom Group Background + public static var channelBoostTableCustomWallpaperGroup: String { return L10n.tr("Localizable", "ChannelBoost.Table.CustomWallpaper.Group") } + /// %d + public static func channelBoostTableProfileColorGroupCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.ProfileColor.Group_countable", p1) + } + /// %d Color for Group Covers + public static func channelBoostTableProfileColorGroupFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.ProfileColor.Group_few", p1) + } + /// %d Color for Group Covers + public static func channelBoostTableProfileColorGroupMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.ProfileColor.Group_many", p1) + } + /// %d Color for Group Cover + public static func channelBoostTableProfileColorGroupOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.ProfileColor.Group_one", p1) + } + /// %d Color for Group Covers + public static func channelBoostTableProfileColorGroupOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.ProfileColor.Group_other", p1) + } + /// %d Color for Group Covers + public static func channelBoostTableProfileColorGroupTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.ProfileColor.Group_two", p1) + } + /// %d Color for Group Covers + public static func channelBoostTableProfileColorGroupZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.ProfileColor.Group_zero", p1) + } + /// Custom Logo for Group Cover + public static var channelBoostTableProfileLogoGroup: String { return L10n.tr("Localizable", "ChannelBoost.Table.ProfileLogo.Group") } + /// %d + public static func channelBoostTableWallpaperGroupCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.Wallpaper.Group_countable", p1) + } + /// %d Group Backgrounds + public static func channelBoostTableWallpaperGroupFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.Wallpaper.Group_few", p1) + } + /// %d Group Backgrounds + public static func channelBoostTableWallpaperGroupMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.Wallpaper.Group_many", p1) + } + /// %d Group Background + public static func channelBoostTableWallpaperGroupOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.Wallpaper.Group_one", p1) + } + /// %d Group Backgrounds + public static func channelBoostTableWallpaperGroupOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.Wallpaper.Group_other", p1) + } + /// %d Group Backgrounds + public static func channelBoostTableWallpaperGroupTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.Wallpaper.Group_two", p1) + } + /// %d Group Backgrounds + public static func channelBoostTableWallpaperGroupZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.Table.Wallpaper.Group_zero", p1) + } /// Boost Channel public static var channelBoostTitleChannel: String { return L10n.tr("Localizable", "ChannelBoost.Title.Channel") } /// Boost Group public static var channelBoostTitleGroup: String { return L10n.tr("Localizable", "ChannelBoost.Title.Group") } + /// Your channel needs %1$@ to get access to features.\n\nAsk your **Premium** subscribers to boost your channel with this link: + public static func channelBoostZeroLevelTextChannel(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.ZeroLevelText.Channel", p1) + } + /// Your group needs %1$@ to get access to features.\n\nAsk your **Premium** subscribers to boost your group with this link: + public static func channelBoostZeroLevelTextGroup(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.ZeroLevelText.Group", p1) + } /// Channel Info public static var channelEventFilterChannelInfo: String { return L10n.tr("Localizable", "ChannelEventFilter.ChannelInfo") } /// Deleted Messages @@ -2347,6 +2447,12 @@ public final class L10n { public static var channelMembersGroupHideMembersInfo: String { return L10n.tr("Localizable", "ChannelMembers.Group.HideMembers.Info") } /// Add Members public static var channelMembersSelectTitle: String { return L10n.tr("Localizable", "ChannelMembers.Select.Title") } + /// Do Not Restrict Boosters + public static var channelPermissionRestrictBoosters: String { return L10n.tr("Localizable", "ChannelPermission.RestrictBoosters") } + /// Choose how many boosts a user must give to the group to bypass restrictions on sending messages. + public static var channelPermissionRestrictBoostersActive: String { return L10n.tr("Localizable", "ChannelPermission.RestrictBoosters.Active") } + /// Turn this on to always allow users who boosted your group to send messages and media. + public static var channelPermissionRestrictBoostersNonActive: String { return L10n.tr("Localizable", "ChannelPermission.RestrictBoosters.NonActive") } /// Add Reactions public static var channelReactionsAdd: String { return L10n.tr("Localizable", "ChannelReactions.Add") } /// Enable Reaction @@ -7617,6 +7723,8 @@ public final class L10n { } /// CHANNELS INCLUDED IN THE GIVEAWAY public static var giveawayChannelsHeader: String { return L10n.tr("Localizable", "Giveaway.Channels.Header") } + /// Add Group + public static var giveawayChannelsAddGroup: String { return L10n.tr("Localizable", "Giveaway.Channels.Add.Group") } /// Select Channel public static var giveawayChannelsAddSelectChannel: String { return L10n.tr("Localizable", "Giveaway.Channels.Add.SelectChannel") } /// Private Channel @@ -7625,8 +7733,46 @@ public final class L10n { public static var giveawayChannelsAddPrivateOk: String { return L10n.tr("Localizable", "Giveaway.Channels.Add.Private.Ok") } /// Are you sure you want to add a private channel? Users won't be able to join it without an invite link. public static var giveawayChannelsAddPrivateText: String { return L10n.tr("Localizable", "Giveaway.Channels.Add.Private.Text") } + /// Private Group + public static var giveawayChannelsAddPrivateHeaderGroup: String { return L10n.tr("Localizable", "Giveaway.Channels.Add.Private.Header.Group") } + /// Add + public static var giveawayChannelsAddPrivateOkGroup: String { return L10n.tr("Localizable", "Giveaway.Channels.Add.Private.Ok.Group") } + /// Are you sure you want to add a private group? Users won't be able to join it without an invite link. + public static var giveawayChannelsAddPrivateTextGroup: String { return L10n.tr("Localizable", "Giveaway.Channels.Add.Private.Text.Group") } + /// Select Group + public static var giveawayChannelsAddSelectChannelGroup: String { return L10n.tr("Localizable", "Giveaway.Channels.Add.SelectChannel.Group") } + /// %d + public static func giveawayChannelsBoostReceiveGroupCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Giveaway.Channels.BoostReceive.Group_countable", p1) + } + /// this group will receive %d boosts + public static func giveawayChannelsBoostReceiveGroupFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Giveaway.Channels.BoostReceive.Group_few", p1) + } + /// this group will receive %d boosts + public static func giveawayChannelsBoostReceiveGroupMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Giveaway.Channels.BoostReceive.Group_many", p1) + } + /// this group will receive %d boost + public static func giveawayChannelsBoostReceiveGroupOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Giveaway.Channels.BoostReceive.Group_one", p1) + } + /// this group will receive %d boosts + public static func giveawayChannelsBoostReceiveGroupOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Giveaway.Channels.BoostReceive.Group_other", p1) + } + /// this group will receive %d boosts + public static func giveawayChannelsBoostReceiveGroupTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Giveaway.Channels.BoostReceive.Group_two", p1) + } + /// this group will receive %d boosts + public static func giveawayChannelsBoostReceiveGroupZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Giveaway.Channels.BoostReceive.Group_zero", p1) + } /// Remove public static var giveawayChannelsContextRemove: String { return L10n.tr("Localizable", "Giveaway.Channels.Context.Remove") } + /// GROUPS INCLUDED IN THE GIVEAWAY + public static var giveawayChannelsHeaderGroup: String { return L10n.tr("Localizable", "Giveaway.Channels.Header.Group") } /// Ends public static var giveawayDateEnds: String { return L10n.tr("Localizable", "Giveaway.Date.Ends") } /// Choose when subscribers of your channel will be randomly selected to receive Telegram Premium. @@ -7635,12 +7781,16 @@ public final class L10n { public static var giveawayDateSelectDate: String { return L10n.tr("Localizable", "Giveaway.Date.SelectDate") } /// DATE WHEN GIVEAWAY ENDS public static var giveawayDateTitle: String { return L10n.tr("Localizable", "Giveaway.Date.Title") } + /// Choose when subscribers of your group will be randomly selected to receive Telegram Premium. + public static var giveawayDateInfoGroup: String { return L10n.tr("Localizable", "Giveaway.Date.Info.Group") } /// OK public static var giveawayDateSelectDateOK: String { return L10n.tr("Localizable", "Giveaway.Date.SelectDate.OK") } /// Get more boosts for your channel by gifting public static var giveawayHeaderText: String { return L10n.tr("Localizable", "Giveaway.Header.Text") } /// **Gift Telegram Premium** public static var giveawayHeaderTitle: String { return L10n.tr("Localizable", "Giveaway.Header.Title") } + /// Get more boosts for your group by gifting + public static var giveawayHeaderTextGroup: String { return L10n.tr("Localizable", "Giveaway.Header.Text.Group") } /// You can review the list of features and terms of use for Telegram Premium [here](premium). public static var giveawayPaymentOptionsInfo: String { return L10n.tr("Localizable", "Giveaway.PaymentOptions.Info") } /// %d Months @@ -7749,6 +7899,8 @@ public final class L10n { public static var giveawayTypeSpecificModalSelectUsers: String { return L10n.tr("Localizable", "Giveaway.Type.Specific.Modal.SelectUsers") } /// Choose the channels users need to be subscribed to take part in the giveaway. public static var givewayChannelsInfo: String { return L10n.tr("Localizable", "Giveway.Channels.Info") } + /// Choose the groups users need to be subscribed to take part in the giveaway. + public static var givewayChannelsInfoGroup: String { return L10n.tr("Localizable", "Giveway.Channels.Info.Group") } /// Off public static var globalTimerOff: String { return L10n.tr("Localizable", "GlobalTimer.Off") } /// Set Custom Time @@ -12677,10 +12829,12 @@ public final class L10n { public static var selectColorResetColorName: String { return L10n.tr("Localizable", "SelectColor.ResetColor.Name") } /// Reset Profile Color public static var selectColorResetColorProfile: String { return L10n.tr("Localizable", "SelectColor.ResetColor.Profile") } - /// Your channel name color has been updated! - public static var selectColorSuccessChannel: String { return L10n.tr("Localizable", "SelectColor.Success.Channel") } - /// Your name color has been updated! - public static var selectColorSuccessUser: String { return L10n.tr("Localizable", "SelectColor.Success.User") } + /// Your channel appearance has been updated! + public static var selectColorSuccessAppearanceChannel: String { return L10n.tr("Localizable", "SelectColor.Success.Appearance.Channel") } + /// Your channel appearance has been updated! + public static var selectColorSuccessAppearanceGroup: String { return L10n.tr("Localizable", "SelectColor.Success.Appearance.Group") } + /// Your profile appearance has been updated! + public static var selectColorSuccessAppearanceUser: String { return L10n.tr("Localizable", "SelectColor.Success.Appearance.User") } /// Channel Color public static var selectColorTitleChannel: String { return L10n.tr("Localizable", "SelectColor.Title.Channel") } /// Your Name Color @@ -13737,6 +13891,8 @@ public final class L10n { public static func statsBoostsExpiresOn(_ p1: String) -> String { return L10n.tr("Localizable", "Stats.Boosts.ExpiresOn", p1) } + /// Members of your group can **boost** it so that it **levels up** and gets **exclusive features**. + public static var statsBoostsGroupInfo: String { return L10n.tr("Localizable", "Stats.Boosts.GroupInfo") } /// Level public static var statsBoostsLevel: String { return L10n.tr("Localizable", "Stats.Boosts.Level") } /// LINK FOR BOOSTING @@ -13751,6 +13907,12 @@ public final class L10n { public static var statsBoostsPremiumSubscribers: String { return L10n.tr("Localizable", "Stats.Boosts.PremiumSubscribers") } /// Show More public static var statsBoostsShowMore: String { return L10n.tr("Localizable", "Stats.Boosts.ShowMore") } + /// Boost + public static var statsBoostsActionBoost: String { return L10n.tr("Localizable", "Stats.Boosts.Action.Boost") } + /// Giveaway + public static var statsBoostsActionGiveaway: String { return L10n.tr("Localizable", "Stats.Boosts.Action.Giveaway") } + /// Features + public static var statsBoostsActionInfo: String { return L10n.tr("Localizable", "Stats.Boosts.Action.Info") } /// Your group is currently boosted by these users. public static var statsBoostsBoostersInfoGroup: String { return L10n.tr("Localizable", "Stats.Boosts.BoostersInfo.Group") } /// Share this link with your group members to get more boosts. diff --git a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift index 2d7c205a8..da1269f3e 100644 --- a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift +++ b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift @@ -6557,6 +6557,45 @@ public final class TelegramIconsTheme { return image } } + public var stats_boost_boost: CGImage { + if let image = cached.with({ $0["stats_boost_boost"] }) { + return image + } else { + let image = _stats_boost_boost() + _ = cached.modify { current in + var current = current + current["stats_boost_boost"] = image + return current + } + return image + } + } + public var stats_boost_giveaway: CGImage { + if let image = cached.with({ $0["stats_boost_giveaway"] }) { + return image + } else { + let image = _stats_boost_giveaway() + _ = cached.modify { current in + var current = current + current["stats_boost_giveaway"] = image + return current + } + return image + } + } + public var stats_boost_info: CGImage { + if let image = cached.with({ $0["stats_boost_info"] }) { + return image + } else { + let image = _stats_boost_info() + _ = cached.modify { current in + var current = current + current["stats_boost_info"] = image + return current + } + return image + } + } public var chat_quiz_explanation: CGImage { if let image = cached.with({ $0["chat_quiz_explanation"] }) { return image @@ -10728,6 +10767,9 @@ public final class TelegramIconsTheme { private let _profile_translate: ()->CGImage private let _profile_join_channel: ()->CGImage private let _profile_boost: ()->CGImage + private let _stats_boost_boost: ()->CGImage + private let _stats_boost_giveaway: ()->CGImage + private let _stats_boost_info: ()->CGImage private let _chat_quiz_explanation: ()->CGImage private let _chat_quiz_explanation_bubble_incoming: ()->CGImage private let _chat_quiz_explanation_bubble_outgoing: ()->CGImage @@ -11516,6 +11558,9 @@ public final class TelegramIconsTheme { profile_translate: @escaping()->CGImage, profile_join_channel: @escaping()->CGImage, profile_boost: @escaping()->CGImage, + stats_boost_boost: @escaping()->CGImage, + stats_boost_giveaway: @escaping()->CGImage, + stats_boost_info: @escaping()->CGImage, chat_quiz_explanation: @escaping()->CGImage, chat_quiz_explanation_bubble_incoming: @escaping()->CGImage, chat_quiz_explanation_bubble_outgoing: @escaping()->CGImage, @@ -12303,6 +12348,9 @@ public final class TelegramIconsTheme { self._profile_translate = profile_translate self._profile_join_channel = profile_join_channel self._profile_boost = profile_boost + self._stats_boost_boost = stats_boost_boost + self._stats_boost_giveaway = stats_boost_giveaway + self._stats_boost_info = stats_boost_info self._chat_quiz_explanation = chat_quiz_explanation self._chat_quiz_explanation_bubble_incoming = chat_quiz_explanation_bubble_incoming self._chat_quiz_explanation_bubble_outgoing = chat_quiz_explanation_bubble_outgoing diff --git a/tools/generate-images.swift b/tools/generate-images.swift index 28e0d4742..77eb976e2 100644 --- a/tools/generate-images.swift +++ b/tools/generate-images.swift @@ -528,6 +528,12 @@ func initialize() -> [String] { array.append("profile_translate") array.append("profile_join_channel") array.append("profile_boost") + + + array.append("stats_boost_boost") + array.append("stats_boost_giveaway") + array.append("stats_boost_info") + array.append("chat_quiz_explanation") array.append("chat_quiz_explanation_bubble_incoming") From 97a9b3dc36547bcf6b22b42f008a4f3b479c866a Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Mon, 5 Feb 2024 12:18:56 +0400 Subject: [PATCH 03/50] bug fixes --- .../ChannelVisibilityController.swift | 67 ++++++++++++------- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/Telegram-Mac/ChannelVisibilityController.swift b/Telegram-Mac/ChannelVisibilityController.swift index c5d407f76..e42154549 100644 --- a/Telegram-Mac/ChannelVisibilityController.swift +++ b/Telegram-Mac/ChannelVisibilityController.swift @@ -658,42 +658,61 @@ private func entries(arguments: Arguments, state: State) -> [ChannelVisibilityEn } if let peer = state.peer?.peer { + + + entries.append(.section(sectionId: sectionId)) sectionId += 1 if let channel = peer as? TelegramChannel, channel.isSupergroup { - let mode: CurrentChannelJoinToSend - if let value = state.joinToSend { - mode = value - } else { - if channel.flags.contains(.joinToSend) { - mode = .members - } else { - mode = .everyone - } - } - entries.append(.writeHeader(sectionId: sectionId, strings().channelVisibilityMessagesWho, .textTopItem)) - entries.append(.writeEveryone(sectionId: sectionId, mode == .everyone, .firstItem)) - entries.append(.writeOnlyMembers(sectionId: sectionId, mode == .members, .lastItem)) - if mode == .members { - entries.append(.section(sectionId: sectionId)) - sectionId += 1 + var isDiscussion = false + if let cachedData = state.cachedData?.data as? CachedChannelData, case let .known(peerId) = cachedData.linkedDiscussionPeerId, peerId != nil { + isDiscussion = true + } + + if (state.selectedType == .publicChannel || isDiscussion || (state.selectedType == nil && channel.addressName != nil)) { - let approve: Bool - if let value = state.approveMembers { - approve = value + let mode: CurrentChannelJoinToSend + if let value = state.joinToSend { + mode = value } else { - approve = channel.flags.contains(.requestToJoin) + if channel.flags.contains(.joinToSend) { + mode = .members + } else { + mode = .everyone + } + } + + if isDiscussion { + entries.append(.writeHeader(sectionId: sectionId, strings().channelVisibilityMessagesWho, .textTopItem)) + entries.append(.writeEveryone(sectionId: sectionId, mode == .everyone, .firstItem)) + entries.append(.writeOnlyMembers(sectionId: sectionId, mode == .members, .lastItem)) } - entries.append(.approveNewMembers(sectionId: sectionId, approve, .singleItem)) - entries.append(.approveNewMembersInfo(sectionId: sectionId, strings().channelVisibilityMessagesApproveInfo, .textBottomItem)) + + + if mode == .members || !isDiscussion { + if isDiscussion { + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + } + + let approve: Bool + if let value = state.approveMembers { + approve = value + } else { + approve = channel.flags.contains(.requestToJoin) + } + entries.append(.approveNewMembers(sectionId: sectionId, approve, .singleItem)) + entries.append(.approveNewMembersInfo(sectionId: sectionId, strings().channelVisibilityMessagesApproveInfo, .textBottomItem)) + } + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 } - entries.append(.section(sectionId: sectionId)) - sectionId += 1 } diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 21577649d..4e651e282 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259066 + 259068 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 605f71eb9..07afa2d1a 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259066 + 259068 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion From 7ba12ae890d23c97eb00b75dbc134d6c9473087e Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Mon, 5 Feb 2024 16:09:27 +0400 Subject: [PATCH 04/50] bug fixes --- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/PeerInfoController.swift | 7 ++++++- Telegram-Mac/UserInfoEntries.swift | 2 +- TelegramShare/Info.plist | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 4e651e282..3e2b8ea99 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259068 + 259070 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/PeerInfoController.swift b/Telegram-Mac/PeerInfoController.swift index b5ee0f2f8..620bd738e 100644 --- a/Telegram-Mac/PeerInfoController.swift +++ b/Telegram-Mac/PeerInfoController.swift @@ -21,7 +21,12 @@ class PeerInfoArguments { let isAd: Bool let pushViewController:(ViewController) -> Void - var peer: Peer? + var peer: Peer? { + didSet { + var bp = 0 + bp += 1 + } + } var effectivePeerId: PeerId { if let peer = peer as? TelegramSecretChat { diff --git a/Telegram-Mac/UserInfoEntries.swift b/Telegram-Mac/UserInfoEntries.swift index 12d79e459..d2236fef6 100644 --- a/Telegram-Mac/UserInfoEntries.swift +++ b/Telegram-Mac/UserInfoEntries.swift @@ -220,7 +220,7 @@ class UserInfoArguments : PeerInfoArguments { let peer = self.peer as? TelegramUser - let firstName: String = peer?.firstName ?? "" + let firstName: String = peerViewMainPeer(peerView)?.compactDisplayTitle ?? "" let lastName = isEditableBot ? (peerView.cachedData as? CachedUserData)?.about : peer?.lastName if editable { diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 07afa2d1a..8eb9210e6 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259068 + 259070 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion From 7ca4320a2d73d97b838f7c3638ede4275abd6529 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Sun, 11 Feb 2024 18:38:56 +0400 Subject: [PATCH 05/50] call screen dev --- Telegram-Mac/ChatListRowView.swift | 3 - Telegram-Mac/Info.plist | 2 +- Telegram-Mac/NetworkStatusManager.swift | 16 +- Telegram-Mac/PeersListController.swift | 3 + Telegram.xcodeproj/project.pbxproj | 8 - TelegramShare/Info.plist | 2 +- packages/PrivateCallScreen/Package.swift | 4 + .../Sources/PeerCallArguments.swift | 4 +- .../PeerCallRevealedSecretKeyView.swift | 130 ++++++++++++++ .../Sources/PeerCallScreen.swift | 158 ++++++++++++++---- .../Sources/PeerCallScreenView.swift | 135 +++++++++++++-- .../Sources/PeerCallSecretKeyView.swift | 98 +++++++++++ .../PrivateCallScreen/Sources/State.swift | 43 ++++- 13 files changed, 538 insertions(+), 68 deletions(-) create mode 100644 packages/PrivateCallScreen/Sources/PeerCallRevealedSecretKeyView.swift create mode 100644 packages/PrivateCallScreen/Sources/PeerCallSecretKeyView.swift diff --git a/Telegram-Mac/ChatListRowView.swift b/Telegram-Mac/ChatListRowView.swift index 6177409cb..289a94bd9 100644 --- a/Telegram-Mac/ChatListRowView.swift +++ b/Telegram-Mac/ChatListRowView.swift @@ -2089,9 +2089,6 @@ class ChatListRowView: TableRowView, ViewDisplayDelegate, RevealTableView { self.contentView.addSubview(current) self.tagsView = current - if animated { - current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) - } } current.update(items: tags.effective, item: item, animated: animated) } else if let view = self.tagsView { diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 96e6fcc27..64f97eae9 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259464 + 259532 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/NetworkStatusManager.swift b/Telegram-Mac/NetworkStatusManager.swift index 0d185ad9c..7d88d4124 100644 --- a/Telegram-Mac/NetworkStatusManager.swift +++ b/Telegram-Mac/NetworkStatusManager.swift @@ -94,7 +94,7 @@ private enum Status : Equatable { private final class ConnectingStatusView: View { private let textView: TextView = TextView() - private let visualEffect: NSVisualEffectView +// private let visualEffect: NSVisualEffectView private let container = View() private var progressView: InfiniteProgressView? private let imageView: ImageView @@ -104,18 +104,18 @@ private final class ConnectingStatusView: View { required init(frame frameRect: NSRect) { - self.visualEffect = NSVisualEffectView(frame: frameRect.size.bounds) + // self.visualEffect = NSVisualEffectView(frame: frameRect.size.bounds) self.imageView = ImageView(frame: frameRect.size.bounds) super.init(frame: frameRect) autoresizingMask = [.width, .height] addSubview(imageView) - addSubview(self.visualEffect) +// addSubview(self.visualEffect) addSubview(container) container.addSubview(textView) textView.userInteractionEnabled = false textView.isSelectable = false - visualEffect.state = .active - visualEffect.blendingMode = .withinWindow +// visualEffect.state = .active +// visualEffect.blendingMode = .withinWindow } @@ -133,7 +133,7 @@ private final class ConnectingStatusView: View { } self.container.center() self.imageView.center() - self.visualEffect.frame = bounds +// self.visualEffect.frame = bounds self.backgroundView?.frame = bounds updateAnimation() } @@ -201,7 +201,7 @@ private final class ConnectingStatusView: View { } if animated && self.backgroundView != nil { let current = self.backgroundView - backgroundView.layer?.animateScaleSpring(from: 0.1, to: 1, duration: 1.2, completion: { [weak current] _ in + backgroundView.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 1.2, completion: { [weak current] _ in current?.removeFromSuperview() }) } else { @@ -222,7 +222,7 @@ private final class ConnectingStatusView: View { // self.visualEffect.bgColor = .clear - visualEffect.material = theme.colors.isDark ? .dark : .light + // visualEffect.material = theme.colors.isDark ? .dark : .light if status.shouldAddProgress { diff --git a/Telegram-Mac/PeersListController.swift b/Telegram-Mac/PeersListController.swift index 1c0745074..78bbadb51 100644 --- a/Telegram-Mac/PeersListController.swift +++ b/Telegram-Mac/PeersListController.swift @@ -2531,6 +2531,9 @@ class PeersListController: TelegramGenericViewController, if case .forum(peerId, _, _) = current?.mode { navigationController?.back() } else { + if current?.mode.isForum == true { + navigationController?.back() + } ForumUI.open(peerId, context: context, threadId: threadId) } case .systemDeprecated, .sharedFolderUpdated, .reveal, .empty, .loading, .space, .suspicious, .savedMessageIndex: diff --git a/Telegram.xcodeproj/project.pbxproj b/Telegram.xcodeproj/project.pbxproj index be82051df..751a1486a 100644 --- a/Telegram.xcodeproj/project.pbxproj +++ b/Telegram.xcodeproj/project.pbxproj @@ -3568,13 +3568,6 @@ name = viewers; sourceTree = ""; }; - 27C1FF482B7499DF00B086E3 /* modern */ = { - isa = PBXGroup; - children = ( - ); - name = modern; - sourceTree = ""; - }; 27CE153E297966A6001F8CE8 /* categories */ = { isa = PBXGroup; children = ( @@ -5696,7 +5689,6 @@ D00B191A24E54B83006CCB87 /* ui */ = { isa = PBXGroup; children = ( - 27C1FF482B7499DF00B086E3 /* modern */, C2016F671EAE0A68003AF981 /* CallWindowController.swift */, D00B191B24E54BA3006CCB87 /* CallReceptionControl.swift */, D00B191D24E54F20006CCB87 /* CallStatusView.swift */, diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index e7b73ff24..581af7191 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259464 + 259532 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/PrivateCallScreen/Package.swift b/packages/PrivateCallScreen/Package.swift index b2cbb1b7e..a6547a421 100644 --- a/packages/PrivateCallScreen/Package.swift +++ b/packages/PrivateCallScreen/Package.swift @@ -21,6 +21,8 @@ let package = Package( .package(name: "MetalEngine", path: "../submodules/telegram-ios/submodules/MetalEngine"), .package(name: "TGUIKit", path: "../TGUIKit"), .package(name: "TelegramMedia", path: "../TelegramMedia"), + .package(name: "ColorPalette", path: "../ColorPalette"), + .package(name: "KeyboardKey", path: "../KeyboardKey"), .package(name: "Localization", path: "../Localization"), .package(name: "CallVideoLayer", path: "../CallVideoLayer"), ], @@ -36,6 +38,8 @@ let package = Package( .product(name: "TGUIKit", package: "TGUIKit", condition: nil), .product(name: "TelegramMedia", package: "TelegramMedia", condition: nil), .product(name: "Localization", package: "Localization", condition: nil), + .product(name: "KeyboardKey", package: "KeyboardKey", condition: nil), + .product(name: "ColorPalette", package: "ColorPalette", condition: nil), .product(name: "CallVideoLayer", package: "CallVideoLayer", condition: nil)], path: "Sources/", resources: [.copy("Resources/Assets.xcassets")]) diff --git a/packages/PrivateCallScreen/Sources/PeerCallArguments.swift b/packages/PrivateCallScreen/Sources/PeerCallArguments.swift index 7659deeb6..f7df72432 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallArguments.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallArguments.swift @@ -14,9 +14,11 @@ import AppKit internal final class Arguments { let external: PeerCallArguments let toggleAnim:()->Void - init(external: PeerCallArguments, toggleAnim:@escaping()->Void) { + let toggleSecretKey:()->Void + init(external: PeerCallArguments, toggleAnim:@escaping()->Void, toggleSecretKey:@escaping()->Void) { self.external = external self.toggleAnim = toggleAnim + self.toggleSecretKey = toggleSecretKey } } diff --git a/packages/PrivateCallScreen/Sources/PeerCallRevealedSecretKeyView.swift b/packages/PrivateCallScreen/Sources/PeerCallRevealedSecretKeyView.swift new file mode 100644 index 000000000..94910b98b --- /dev/null +++ b/packages/PrivateCallScreen/Sources/PeerCallRevealedSecretKeyView.swift @@ -0,0 +1,130 @@ +// +// File.swift +// +// +// Created by Mikhail Filimonov on 11.02.2024. +// + +import Foundation +import TGUIKit +import AppKit +import ColorPalette +import Localization + +internal final class PeerCallRevealedSecretKeyView : NSVisualEffectView, CallViewUpdater { + + + private let headerView = TextView() + private let textView = TextView() + private let ok = TextButton(frame: NSMakeRect(0, 0, 260, 40)) + + private var arguments: Arguments? + + + required override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(headerView) + addSubview(textView) + addSubview(ok) + + wantsLayer = true + state = .active + blendingMode = .withinWindow + material = .ultraDark + + layer?.cornerRadius = 10 + +// layer?.masksToBounds = false + + headerView.userInteractionEnabled = false + headerView.isSelectable = false + + textView.userInteractionEnabled = false + textView.isSelectable = false + //TODOLANG + let header = TextViewLayout(.initialize(string: "This call is end-to-end encrypted", color: .white, font: .medium(.title)), alignment: .center) + + header.measure(width: 300 - 40) + headerView.update(header) + + ok.autoSizeToFit = false + ok.scaleOnClick = true + + ok.layer?.cornerRadius = 10 + ok.set(background: darkPalette.grayIcon.withAlphaComponent(0.35), for: .Normal) + ok.set(font: .medium(.title), for: .Normal) + ok.set(text: L10n.modalOK, for: .Normal) + ok.set(color: NSColor.white, for: .Normal) + ok.sizeToFit(.zero, NSMakeSize(260, 40), thatFit: true) + + ok.set(handler: { [weak self] _ in + self?.arguments?.toggleSecretKey() + }, for: .Click) + +// let shadow = NSShadow() +// shadow.shadowBlurRadius = 4 +// shadow.shadowColor = NSColor.black.withAlphaComponent(0.6) +// shadow.shadowOffset = NSMakeSize(0, 0) +// self.shadow = shadow +// +// + + let layer = self.layer! + + if #available(macOS 10.15, *) { + layer.cornerCurve = .continuous + } + + layer.masksToBounds = false + layer.shadowColor = NSColor(white: 0.0, alpha: 1.0).cgColor + layer.shadowOffset = CGSize(width: 0.0, height: 0) + layer.shadowRadius = 10 + layer.shadowOpacity = 0.35 +//// layer.fillColor = presentation.colors.background.cgColor + + } + + override func updateLayer() { + super.updateLayer() + + let sublayers = self.layer?.sublayers ?? [] + + for sublayer in sublayers { + sublayer.cornerRadius = 10 + } + } + + override var isFlipped: Bool { + return true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateState(_ state: PeerCallState, arguments: Arguments, transition: ContainedViewLayoutTransition) { + + self.arguments = arguments + + let text = "If emoji on **\(state.compactTitle)** screen are the same, this call is 100% secure." + let textLayout = TextViewLayout(.initialize(string: text, color: darkPalette.grayIcon.withAlphaComponent(0.7), font: .normal(.text)).detectBold(with: .medium(.text)), alignment: .center) + textLayout.measure(width: 300 - 40) + + textView.update(textLayout) + + self.setFrameSize(NSMakeSize(300, 20 + headerView.frame.height + 20 + 40 + 20 + textLayout.layoutSize.height + 20 + ok.frame.height + 20)) + + updateLayout(size: self.frame.size, transition: transition) + } + + override func layout() { + super.layout() + self.updateLayout(size: self.frame.size, transition: .immediate) + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(view: headerView, frame: headerView.centerFrameX(y: 20)) + transition.updateFrame(view: ok, frame: ok.centerFrameX(y: size.height - ok.frame.height - 20)) + transition.updateFrame(view: textView, frame: textView.centerFrameX(y: size.height - textView.frame.height - 20 - 40 - 20)) + } +} diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift index c3453c1d1..b6188b5dd 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift @@ -14,7 +14,7 @@ import CallVideoLayer import TGUIKit import MetalEngine import AppKit - +import KeyboardKey protocol CallViewUpdater { func updateState(_ state: PeerCallState, arguments: Arguments, transition: ContainedViewLayoutTransition) @@ -73,39 +73,120 @@ public final class PeerCallScreen : ViewController { } statePromise.set(stateValue.modify (f)) } - + let arguments = Arguments(external: external, toggleAnim: { -// updateState { current in -// var current = current -// current.externalState = current.stateIndex + 1 -// if current.stateIndex > 2 { -// current.stateIndex = 0 -// } -// if let networkStatus = current.networkStatus { -// switch networkStatus { -// case .connecting: -// current.networkStatus = .calling -// case .calling: -// current.networkStatus = .failed -// case .failed: -// current.networkStatus = nil -// } -// } else { -// current.networkStatus = .connecting -// } -// current.networkSignal = current.networkSignal + 1 -// if current.networkSignal > 4 { -// current.networkSignal = 0 -// } -// return current -// } + // updateState { current in + // var current = current + // current.externalState = current.stateIndex + 1 + // if current.stateIndex > 2 { + // current.stateIndex = 0 + // } + // if let networkStatus = current.networkStatus { + // switch networkStatus { + // case .connecting: + // current.networkStatus = .calling + // case .calling: + // current.networkStatus = .failed + // case .failed: + // current.networkStatus = nil + // } + // } else { + // current.networkStatus = .connecting + // } + // current.networkSignal = current.networkSignal + 1 + // if current.networkSignal > 4 { + // current.networkSignal = 0 + // } + // return current + // } + }, toggleSecretKey: { + updateState { current in + var current = current + current.secretKeyViewState = current.secretKeyViewState.rev + return current + } }) + var first: Bool = true actionsDisposable.add((statePromise.get() |> deliverOnMainQueue).start(next: { [weak self] state in - self?.applyState(state, arguments: arguments, animated: true) + self?.applyState(state, arguments: arguments, animated: !first) + first = false + })) + + struct TooltipTuple : Equatable { + let lowSignal: Bool + let lowBattery: Bool + + var isEmpty: Bool { + return !lowSignal && !lowBattery + } + + func take(_ index: Int, state: PeerCallState) -> String { + + if index == 0 { + return "Weak network signal" + } + if index == 1 { + return "\(state.compactTitle)'s battery is low" + } + return "" + } + var indexes: [Int] { + var indexes: [Int] = [] + if lowSignal { + indexes.append(0) + } + if lowBattery { + indexes.append(1) + } + return indexes + } + } + + let statusTooltip: Signal = statePromise.get() |> map { + .init(lowSignal: $0.reception != nil && $0.reception! < 2, lowBattery: $0.externalState.remoteBatteryLevel == .low) + } |> distinctUntilChanged + + + let recursiveDisposable = MetaDisposable() + + actionsDisposable.add(recursiveDisposable) + + actionsDisposable.add(statusTooltip.start(next: { tooltip in + DispatchQueue.main.async { + if tooltip.isEmpty { + updateState { current in + var current = current + current.statusTooltip = nil + return current + } + recursiveDisposable.set(nil) + } else { + let recursive = Signal.single(Void()) |> then(.single(Void()) |> suspendAwareDelay(4, queue: .mainQueue()) |> restart) + + + let indexes: [Int] = tooltip.indexes + var index: Int = 0 + + + recursiveDisposable.set(recursive.start(next: { + updateState { current in + var current = current + current.statusTooltip = tooltip.take(indexes[index], state: current) + return current + } + + index += 1 + if index == indexes.count { + index = 0 + } + })) + } + + } })) let peer = external.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: external.peerId)) |> deliverOnMainQueue @@ -119,10 +200,31 @@ public final class PeerCallScreen : ViewController { return current } })) + + let invokeEsc:(NSEvent)->KeyHandlerResult = { [weak self] event in + guard let self else { + return .rejected + } + let keyIsShown = self.stateValue.with { $0.secretKeyViewState == .revealed } + if keyIsShown { + arguments.toggleSecretKey() + return .invoked + } else if event.keyCode == KeyboardKey.Space.rawValue { + arguments.toggleSecretKey() + return .invoked + } + + + return .rejected + } + + screen.set(handler: invokeEsc, with: self, for: .Escape) + screen.set(handler: invokeEsc, with: self, for: .Space) + } private func applyState(_ state: PeerCallState, arguments: Arguments, animated: Bool) { - let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeOut) : .immediate + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .spring) : .immediate genericView.updateState(state, arguments: arguments, transition: transition) genericView.updateLayout(size: self.frame.size, transition: transition) } diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift index df0d4c51e..0a2b123d5 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift @@ -31,6 +31,10 @@ private final class InfoHelpView : NSVisualEffectView { self.blendingMode = .withinWindow } + var string: String? { + return textView.textLayout?.attributedString.string + } + func set(string: String, hasShimm: Bool = false) { let layout = TextViewLayout(.initialize(string: string, color: NSColor.white, font: .medium(.text)), alignment: .center) layout.measure(width: .greatestFiniteMagnitude) @@ -76,7 +80,7 @@ private final class InfoHelpView : NSVisualEffectView { final class PeerCallScreenView : Control { private let backgroundLayer: CallBackgroundLayer = .init() private let photoView = PeerCallPhotoView(frame: NSMakeRect(0, 0, 120, 120)) - private let statusView: PeerCallStatusView = PeerCallStatusView(frame: NSMakeRect(0, 0, 300, 58)) + private let statusView = PeerCallStatusView(frame: NSMakeRect(0, 0, 300, 58)) private var arguments: Arguments? @@ -88,7 +92,10 @@ final class PeerCallScreenView : Control { private var endAction: PeerCallActionView? = PeerCallActionView() - private var weakSignal: InfoHelpView? + private var statusTooltip: InfoHelpView? + + private var secretView: SecretKeyView? + private var revealedKey: PeerCallRevealedSecretKeyView? required init(frame frameRect: NSRect) { super.init(frame: frameRect) @@ -136,6 +143,10 @@ final class PeerCallScreenView : Control { func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + guard let state = self.state else { + return + } + backgroundLayer.frame = size.bounds backgroundLayer.blurredLayer.frame = size.bounds @@ -149,8 +160,18 @@ final class PeerCallScreenView : Control { statusView.updateLayout(size: statusView.frame.size, transition: transition) - if let weakSignal { - transition.updateFrame(view: weakSignal, frame: weakSignal.centerFrameX(y: statusView.frame.maxY + 12)) + if let statusTooltip { + transition.updateFrame(view: statusTooltip, frame: statusTooltipFrame(view: statusTooltip, state: state)) + } + + if let secretView { + transition.updateFrame(view: secretView, frame: secretKeyFrame(view: secretView, state: state)) + secretView.updateLayout(size: secretView.frame.size, transition: transition) + } + + if let revealedKey { + transition.updateFrame(view: revealedKey, frame: revealedKeyFrame(view: revealedKey, state: state)) + revealedKey.updateLayout(size: revealedKey.frame.size, transition: transition) } let actions = [self.videoAction, self.screencastAction, self.muteAction, self.endAction].compactMap { $0 } @@ -173,30 +194,114 @@ final class PeerCallScreenView : Control { self.backgroundLayer.update(stateIndex: state.stateIndex, isEnergySavingEnabled: false, transition: transition) - if let reception = state.reception, reception < 2 { - let current: InfoHelpView + if let tooltip = state.statusTooltip { + if self.statusTooltip?.string != tooltip { + if let statusTooltip = self.statusTooltip { + performSubviewRemoval(statusTooltip, animated: transition.isAnimated, scale: true) + self.statusTooltip = nil + } + + let current: InfoHelpView + let isNew = true + current = InfoHelpView(frame: .zero) + self.addSubview(current, positioned: .below, relativeTo: subviews.first) + self.statusTooltip = current + current.set(string: tooltip, hasShimm: true) + + if isNew { + ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: statusTooltipFrame(view: current, state: state)) + if transition.isAnimated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + current.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) + } + } + } + } else if let statusTooltip = self.statusTooltip { + performSubviewRemoval(statusTooltip, animated: transition.isAnimated, scale: true) + self.statusTooltip = nil + } + + + + if state.secretKeyViewState == .revealed { + let current: PeerCallRevealedSecretKeyView let isNew: Bool - if let view = self.weakSignal { + if let view = self.revealedKey { current = view isNew = false } else { - current = InfoHelpView(frame: .zero) - self.addSubview(current) - self.weakSignal = current + current = PeerCallRevealedSecretKeyView(frame: NSMakeRect(0, 0, 300, 300)) + self.addSubview(current, positioned: .below, relativeTo: secretView) + self.revealedKey = current + isNew = true + } + current.updateState(state, arguments: arguments, transition: isNew ? .immediate : transition) + + if isNew { + ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: revealedKeyFrame(view: current, state: state)) + if transition.isAnimated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + current.layer?.animateScaleSpring(from: 0.8, to: 1, duration: 0.2, bounce: false) + } + } + } else if let revealedKey = self.revealedKey { + performSubviewRemoval(revealedKey, animated: transition.isAnimated, scale: false) + self.revealedKey = nil + } + + + if let _ = state.secretKey { + let current: SecretKeyView + let isNew: Bool + if let view = self.secretView { + current = view + isNew = false + } else { + current = SecretKeyView(frame: NSMakeRect(0, 0, 100, 25)) + self.addSubview(current, positioned: .above, relativeTo: revealedKey) + self.secretView = current isNew = true } - current.set(string: "Weak network signal", hasShimm: true) + current.updateState(state, arguments: arguments, transition: isNew ? .immediate : transition) if isNew { - ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: current.centerFrameX(y: statusView.frame.maxY + 12)) + ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: secretKeyFrame(view: current, state: state)) if transition.isAnimated { current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) current.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) } } - } else if let weakSignal = self.weakSignal { - performSubviewRemoval(weakSignal, animated: transition.isAnimated, scale: true) - self.weakSignal = nil + } else if let secretView = self.secretView { + performSubviewRemoval(secretView, animated: transition.isAnimated, scale: true) + self.secretView = nil + } + + } +} + + + + + +//RECT + +extension PeerCallScreenView { + func statusTooltipFrame(view: NSView, state: PeerCallState) -> NSRect { + return view.centerFrameX(y: statusView.frame.maxY + 12) + } + func secretKeyFrame(view: NSView, state: PeerCallState) -> NSRect { + if state.secretKeyViewState == .revealed { + var rect = focus(NSMakeSize(200, 50)) + rect.origin.y -= 30 + return rect + } else { + var rect = focus(NSMakeSize(100, 25)) + rect.origin.y = 16 + return rect } } + + func revealedKeyFrame(view: NSView, state: PeerCallState) -> NSRect { + return view.centerFrame() + } } diff --git a/packages/PrivateCallScreen/Sources/PeerCallSecretKeyView.swift b/packages/PrivateCallScreen/Sources/PeerCallSecretKeyView.swift new file mode 100644 index 000000000..57773eb97 --- /dev/null +++ b/packages/PrivateCallScreen/Sources/PeerCallSecretKeyView.swift @@ -0,0 +1,98 @@ +// +// File.swift +// +// +// Created by Mikhail Filimonov on 11.02.2024. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import AppKit + +fileprivate func generateSecretKey(_ emoji: NSAttributedString) -> Signal { + return Signal { subscriber in + + + + let node = TextNode.layoutText(maybeNode: nil, emoji, nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .center) + + if node.0.size == .zero { + subscriber.putCompletion() + return EmptyDisposable + } + + let image = generateImage(node.0.size, scale: System.backingScale, rotatedContext: { size, ctx in + ctx.clear(size.bounds) + node.1.draw(size.bounds, in: ctx, backingScaleFactor: System.backingScale, backgroundColor: .clear) + + }) + + subscriber.putNext(image) + subscriber.putCompletion() + + return ActionDisposable { + + } + } |> runOn(.concurrentBackgroundQueue()) +} + + + + +final class SecretKeyView : Control, CallViewUpdater { + + private let disposable = MetaDisposable() + + private let secretView: SimpleLayer = SimpleLayer() + + private var state: PeerCallState? + private var arguments: Arguments? + private var secretKey: String? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.layer = secretView + + scaleOnClick = true + + self.layer?.contentsGravity = .resizeAspect + + set(handler: { [weak self] _ in + self?.arguments?.toggleSecretKey() + }, for: .Click) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateState(_ state: PeerCallState, arguments: Arguments, transition: ContainedViewLayoutTransition) { + self.state = state + self.arguments = arguments + + let previousKey = self.secretKey + self.secretKey = state.secretKey + + if previousKey != secretKey, let secretKey = secretKey { + let signal = generateSecretKey(.initialize(string: secretKey, font: .normal(50))) |> deliverOnMainQueue + disposable.set(signal.startStandalone(next: { [weak self] image in + self?.secretView.contents = image + })) + } + } + + override func layout() { + super.layout() + self.updateLayout(size: self.frame.size, transition: .immediate) + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + + } + + deinit { + disposable.dispose() + } + +} diff --git a/packages/PrivateCallScreen/Sources/State.swift b/packages/PrivateCallScreen/Sources/State.swift index 5e22a143d..b818bec19 100644 --- a/packages/PrivateCallScreen/Sources/State.swift +++ b/packages/PrivateCallScreen/Sources/State.swift @@ -9,6 +9,8 @@ import Foundation import TelegramCore import Localization import Postbox +import QuartzCore +import CoreFoundation private extension EnginePeer { var compactDisplayTitle: String { @@ -77,8 +79,8 @@ public struct ExternalPeerCallState: Equatable { case ringing case requesting(ringing: Bool) case connecting - case active(duration: Double, reception: Int32?, emoji: String) - case reconnecting(duration: Double, reception: Int32?, emoji: String) + case active(startTime: Double, reception: Int32?, emoji: String) + case reconnecting(startTime: Double, reception: Int32?, emoji: String) case terminating case terminated(PeerCallSessionTerminationReason?) } @@ -183,10 +185,28 @@ extension ExternalPeerCallState.State { public struct PeerCallState : Equatable { + public enum SecretKeyViewState : Equatable { + case revealed + case concealed + + var rev: SecretKeyViewState { + switch self { + case .concealed: + return .revealed + case .revealed: + return .concealed + } + } + } + public var peer: EnginePeer? public var accountPeer: EnginePeer? - public var externalState: ExternalPeerCallState = .init(state: .waiting, videoState: .notAvailable, remoteVideoState: .inactive, isMuted: false, isOutgoingVideoPaused: true, remoteAspectRatio: 1.0, remoteAudioState: .active, remoteBatteryLevel: .normal, isScreenCapture: false) + public var secretKeyViewState: SecretKeyViewState = .concealed + + var statusTooltip: String? = nil + + public var externalState: ExternalPeerCallState = .init(state: .active(startTime: CFAbsoluteTimeGetCurrent(), reception: 1, emoji: "😁 🤷 😘 ❤️"), videoState: .notAvailable, remoteVideoState: .inactive, isMuted: false, isOutgoingVideoPaused: true, remoteAspectRatio: 1.0, remoteAudioState: .active, remoteBatteryLevel: .low, isScreenCapture: false) var status: PeerCallStatusValue { return self.externalState.state.statusText(accountPeer, externalState.videoState) @@ -198,6 +218,12 @@ public struct PeerCallState : Equatable { } return nil } + var compactTitle: String { + if let peer = peer?._asPeer() as? TelegramUser { + return peer.firstName ?? peer.lastName ?? "" + } + return "" + } var reception: Int32? { switch externalState.state { @@ -210,6 +236,17 @@ public struct PeerCallState : Equatable { } } + var secretKey: String? { + switch self.externalState.state { + case .active(_, _, let emoji): + return emoji + case .reconnecting(_, _, let emoji): + return emoji + default: + return nil + } + } + var stateIndex: Int { switch externalState.state { case .waiting: From 20e8eeb9ad6bd8bf275aa769be79d5aed01bb527 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 13 Feb 2024 12:01:54 -0400 Subject: [PATCH 06/50] 10.8 --- Telegram-Mac/Appearance.swift | 3 +- Telegram-Mac/ApplicationContext.swift | 27 +- .../BusinessAwayMessageController.swift | 420 ++++++++++++++++ Telegram-Mac/BusinessChatbotController.swift | 290 ++++++++++++ Telegram-Mac/BusinessHoursController.swift | 448 ++++++++++++++++++ Telegram-Mac/BusinessLocationController.swift | 327 +++++++++++++ Telegram-Mac/ChannelAdminController.swift | 1 + .../ChannelAdminsViewController.swift | 44 +- Telegram-Mac/ChannelInfoEntries.swift | 4 +- .../ChannelPermissionsController.swift | 6 +- Telegram-Mac/ChatListFilterController.swift | 6 +- .../ChatListFiltersListController.swift | 3 + Telegram-Mac/ChatListRowView.swift | 10 +- Telegram-Mac/ChatMessageMenuItems.swift | 8 +- Telegram-Mac/CoreExtension.swift | 7 + Telegram-Mac/DevicesContext.swift | 20 +- Telegram-Mac/EmojiesController.swift | 2 +- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/InputDataController.swift | 2 +- Telegram-Mac/InputDataControllerEntries.swift | 40 +- Telegram-Mac/LocationModalController.swift | 29 +- Telegram-Mac/LocationPreviewController.swift | 1 - Telegram-Mac/LocationSendCurrentItem.swift | 20 +- Telegram-Mac/PeersListController.swift | 2 +- Telegram-Mac/StoryChatContent.swift | 80 +++- Telegram-Mac/StoryControlsView.swift | 93 +++- Telegram-Mac/StoryInputView.swift | 11 +- Telegram-Mac/StoryListView.swift | 6 +- Telegram-Mac/StoryModalController.swift | 133 +++++- Telegram-Mac/en.lproj/Localizable.strings | Bin 829640 -> 829946 bytes Telegram.xcodeproj/project.pbxproj | 24 + TelegramShare/Info.plist | 2 +- .../ChatListFilterPreferences.swift | 2 +- .../Sources/Localization/Localizable.swift | 8 + packages/TGUIKit/Sources/ContextMenu.swift | 7 + .../Sources/TelegramIconsTheme.swift | 18 +- submodules/telegram-ios | 2 +- tools/generate-images.swift | 2 + 38 files changed, 2018 insertions(+), 92 deletions(-) create mode 100644 Telegram-Mac/BusinessAwayMessageController.swift create mode 100644 Telegram-Mac/BusinessChatbotController.swift create mode 100644 Telegram-Mac/BusinessHoursController.swift create mode 100644 Telegram-Mac/BusinessLocationController.swift diff --git a/Telegram-Mac/Appearance.swift b/Telegram-Mac/Appearance.swift index c49e45d23..754fe4507 100644 --- a/Telegram-Mac/Appearance.swift +++ b/Telegram-Mac/Appearance.swift @@ -2897,7 +2897,8 @@ private func generateIcons(from palette: ColorPalette, bubbled: Bool) -> Telegra channel_feature_voice_to_text: { NSImage(named: "Icon_ChannelFeature_VoiceToText")!.precomposed(palette.accent) }, chat_hidden_author: { NSImage(named: "Icon_AuthorHidden")!.precomposed(.white) }, chat_my_notes: { NSImage(named: "Icon_MyNotes")!.precomposed(.white) }, - premium_required_forward: { NSImage(named: "Icon_PremiumRequired_Forward")!.precomposed() } + premium_required_forward: { NSImage(named: "Icon_PremiumRequired_Forward")!.precomposed() }, + create_new_message_general: { NSImage(resource: .iconNewMessage).precomposed(palette.accent, flipVertical: true) } ) } diff --git a/Telegram-Mac/ApplicationContext.swift b/Telegram-Mac/ApplicationContext.swift index 3e9d6cbc3..15c4793ee 100644 --- a/Telegram-Mac/ApplicationContext.swift +++ b/Telegram-Mac/ApplicationContext.swift @@ -540,11 +540,28 @@ final class AuthorizedApplicationContext: NSObject, SplitViewDelegate { return .invoked }, with: self, for: .T, priority: .supreme, modifierFlags: [.command]) -// window.set(handler: { [weak self] _ -> KeyHandlerResult in -// showModal(with: GiveawayModalController(context: context, peerId: context.peerId), for: context.window) -// -// return .invoked -// }, with: self, for: .Y, priority: .supreme, modifierFlags: [.command]) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + context.bindings.rootNavigation().push(BusinessHoursController(context: context, peerId: context.peerId)) + + return .invoked + }, with: self, for: .Y, priority: .supreme, modifierFlags: [.command]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + context.bindings.rootNavigation().push(BusinessAwayMessageController(context: context, peerId: context.peerId)) + + return .invoked + }, with: self, for: .U, priority: .supreme, modifierFlags: [.command]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + context.bindings.rootNavigation().push(BusinessLocationController(context: context, peerId: context.peerId)) + + return .invoked + }, with: self, for: .I, priority: .supreme, modifierFlags: [.command]) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + context.bindings.rootNavigation().push(BusinessChatbotController(context: context, peerId: context.peerId)) + + return .invoked + }, with: self, for: .J, priority: .supreme, modifierFlags: [.command]) #endif diff --git a/Telegram-Mac/BusinessAwayMessageController.swift b/Telegram-Mac/BusinessAwayMessageController.swift new file mode 100644 index 000000000..366d905f4 --- /dev/null +++ b/Telegram-Mac/BusinessAwayMessageController.swift @@ -0,0 +1,420 @@ +// +// BusinessAwayMessageController.swift +// Telegram +// +// Created by Mikhail Filimonov on 12.02.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import Postbox +import Cocoa +import TGUIKit +import SwiftSignalKit + +class BusinessSelectChatsCallbackObject : ShareObject { + private let callback:([PeerId])->Signal + private let limitReachedText: String + init(_ context: AccountContext, defaultSelectedIds: Set, additionTopItems: ShareAdditionItems?, limit: Int?, limitReachedText: String, callback:@escaping([PeerId])->Signal, excludePeerIds: Set = Set()) { + self.callback = callback + self.limitReachedText = limitReachedText + super.init(context, excludePeerIds: excludePeerIds, defaultSelectedIds: defaultSelectedIds, additionTopItems: additionTopItems, limit: limit) + } + + override var selectTopics: Bool { + return false + } + + override var hasFolders: Bool { + return false + } + + override var hasCaptionView: Bool { + return false + } + override var blockCaptionView: Bool { + return true + } + + override func statusStyle(_ peer: Peer, presence: PeerStatusStringResult?, autoDeletion: Int32?) -> ControlStyle { + return ControlStyle(font: .normal(.text), foregroundColor: theme.colors.grayText) + } + +// override func statusString(_ peer: Peer, presence: PeerStatusStringResult?, autoDeletion: Int32?) -> String? { +// <#code#> +// } + + override func perform(to peerIds:[PeerId], threadId: MessageId?, comment: ChatTextInputState? = nil) -> Signal { + return callback(peerIds) |> castError(String.self) + } + override func limitReached() { + alert(for: context.window, info: limitReachedText) + } + override var searchPlaceholderKey: String { + return "ChatList.Add.Placeholder" + } + override var interactionOk: String { + return strings().chatListFilterAddDone + } + override var alwaysEnableDone: Bool { + return true + } + override func possibilityPerformTo(_ peer: Peer) -> Bool { + if peer is TelegramSecretChat { + return false + } + if excludePeerIds.contains(peer.id) { + return false + } + return peer.isUser && !peer.isBot + } + +} + + +private final class Arguments { + let context: AccountContext + let toggleEnabled:()->Void + let createMessage:()->Void + let toggleSchedule:(State.Schedule)->Void + let toggleRecepient:(State.Recepient)->Void + let selectScheduleStart:(Date, Date)->Void + let selectScheduleEnd:(Date, Date)->Void + let selectChats:()->Void + let removeIncluded:(PeerId)->Void + init(context: AccountContext, toggleEnabled:@escaping()->Void, createMessage:@escaping()->Void, toggleSchedule:@escaping(State.Schedule)->Void, toggleRecepient:@escaping(State.Recepient)->Void, selectScheduleStart:@escaping(Date, Date)->Void, selectScheduleEnd:@escaping(Date, Date)->Void, selectChats:@escaping()->Void, removeIncluded:@escaping(PeerId)->Void) { + self.context = context + self.toggleEnabled = toggleEnabled + self.createMessage = createMessage + self.toggleSchedule = toggleSchedule + self.toggleRecepient = toggleRecepient + self.selectScheduleStart = selectScheduleStart + self.selectScheduleEnd = selectScheduleEnd + self.selectChats = selectChats + self.removeIncluded = removeIncluded + } +} + +private struct State : Equatable { + + enum Recepient : Equatable { + case all + case selected + } + + enum Schedule : Equatable { + case alwaysSend + case outsideWorking + case custom(from: Date, to: Date) + + var isCustom: Bool { + if case .custom = self { + return true + } + return false + } + } + + var enabled: Bool = false + + var schedule: Schedule = .alwaysSend + var recepient: Recepient = .all + + var selectedIds: [PeerId] = [] + + var selectedPeers: [EnginePeer] = [] +} + +private let _id_header = InputDataIdentifier("_id_header") +private let _id_enabled = InputDataIdentifier("_id_enabled") + +private let _id_create_message = InputDataIdentifier("_id_create_message") +private let _id_message = InputDataIdentifier("_id_message") + +private let _id_schedule_always = InputDataIdentifier("_id_schedule_always") +private let _id_schedule_outside = InputDataIdentifier("_id_schedule_outside") +private let _id_schedule_custom = InputDataIdentifier("_id_schedule_custom") + + +private let _id_recepient_1x1 = InputDataIdentifier("_id_recepient_1x1") +private let _id_recepient_selected = InputDataIdentifier("_id_recepient_selected") + +private let _id_start_time = InputDataIdentifier("_id_start_time") +private let _id_end_time = InputDataIdentifier("_id_end_time") + + +private let _id_include_chats = InputDataIdentifier("_id_include_chats") +private let _id_exclude_chats = InputDataIdentifier("_id_exclude_chats") + +private func _id_peer(_ id: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_peer_\(id.toInt64())") +} + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_header, equatable: nil, comparable: nil, item: { initialSize, stableId in + return AnimatedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: LocalAnimatedSticker.fly_dollar, text: .initialize(string: "Automatically reply with a message when you are away.", color: theme.colors.listGrayText, font: .normal(.text))) + })) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_enabled, data: .init(name: "Send Away Message", color: theme.colors.text, type: .switchable(state.enabled), viewType: .singleItem, action: arguments.toggleEnabled))) + + // entries + + if state.enabled { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("AWAY MESSAGE"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_create_message, data: .init(name: "Create an Away Message", color: theme.colors.accent, icon: theme.icons.create_new_message_general, type: .next, viewType: .singleItem, action: arguments.createMessage))) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("SCHEDULE"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_schedule_always, data: .init(name: "Always Send", color: theme.colors.text, type: .selectable(state.schedule == .alwaysSend), viewType: .firstItem, action: { + arguments.toggleSchedule(.alwaysSend) + }))) + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_schedule_outside, data: .init(name: "Outside of Business Hours", color: theme.colors.text, type: .selectable(state.schedule == .outsideWorking), viewType: .innerItem, action: { + arguments.toggleSchedule(.outsideWorking) + }))) + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_schedule_custom, data: .init(name: "Custom Schedule", color: theme.colors.text, type: .selectable(state.schedule.isCustom), viewType: .lastItem, action: { + arguments.toggleSchedule(.custom(from: Date(), to: Date(timeIntervalSinceNow: TimeInterval(Int32.secondsInWeek)))) + + }))) + + switch state.schedule { + case let .custom(from, to): + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let fromString = stringForMediumDate(timestamp: Int32(from.timeIntervalSince1970)) + let toString = stringForMediumDate(timestamp: Int32(to.timeIntervalSince1970)) + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_start_time, data: .init(name: "Start Time", color: theme.colors.text, type: .nextContext(fromString), viewType: .firstItem, action: { + arguments.selectScheduleStart(from, to) + }))) + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_end_time, data: .init(name: "End Time", color: theme.colors.text, type: .nextContext(toString), viewType: .lastItem, action: { + arguments.selectScheduleEnd(from, to) + }))) + default: + break + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("RECIPIENTS"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_recepient_1x1, data: .init(name: "All 1-to-1 Chats Except...", color: theme.colors.text, type: .selectable(state.recepient == .all), viewType: .firstItem, action: { + arguments.toggleRecepient(.all) + }))) + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_recepient_selected, data: .init(name: "Only Selected Chats", color: theme.colors.text, type: .selectable(state.recepient == .selected), viewType: .lastItem, action: { + arguments.toggleRecepient(.selected) + }))) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + switch state.recepient { + case .all: + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("EXCLUDE CHATS"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_exclude_chats, data: .init(name: "Exclude Chats...", color: theme.colors.accent, icon: theme.icons.chat_filter_add, type: .none, viewType: state.selectedPeers.isEmpty ? .singleItem : .firstItem, action: arguments.selectChats))) + + case .selected: + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("INCLUDE CHATS"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_include_chats, data: .init(name: "Include Chats...", color: theme.colors.accent, icon: theme.icons.chat_filter_add, type: .none, viewType: state.selectedPeers.isEmpty ? .singleItem : .firstItem, action: arguments.selectChats))) + } + + struct Tuple : Equatable { + let peer: PeerEquatable + let viewType: GeneralViewType + let status: String? + } + var tuples: [Tuple] = [] + + var selectedPeers: [Peer] = [] + + let categories = state.selectedIds.filter { + $0.namespace._internalGetInt32Value() == ChatListFilterPeerCategories.Namespace + } + for category in categories { + let cat = ChatListFilterPeerCategories(rawValue: Int32(category.id._internalGetInt64Value())) + selectedPeers.append(TelegramFilterCategory(category: cat)) + } + + selectedPeers.append(contentsOf: state.selectedPeers.map { $0._asPeer() }) + + + for (i, peer) in selectedPeers.enumerated() { + var viewType: GeneralViewType = bestGeneralViewType(selectedPeers, for: i) + if i == 0 { + if i < selectedPeers.count - 1 { + viewType = .innerItem + } else { + viewType = .lastItem + } + } + let status: String? = nil + tuples.append(.init(peer: .init(peer), viewType: viewType, status: nil)) + } + + for tuple in tuples { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_peer(tuple.peer.id), equatable: .init(tuple), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: tuple.peer.peer, account: arguments.context.account, context: arguments.context, stableId: stableId, height: 44, photoSize: NSMakeSize(30, 30), status: tuple.status, inset: NSEdgeInsets(left: 20, right: 20), viewType: tuple.viewType, action: { + //arguments.openInfo(peer.id) + }, contextMenuItems: { + return .single([ContextMenuItem("Remove", handler: { + arguments.removeIncluded(tuple.peer.id) + }, itemMode: .destruct, itemImage: MenuAnimation.menu_delete.value)]) + }, highlightVerified: true) + })) + } + } + + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func BusinessAwayMessageController(context: AccountContext, peerId: PeerId) -> InputDataController { + + let actionsDisposable = DisposableSet() + + let initialState = State() + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let peers: Signal<[EnginePeer], NoError> = statePromise.get() |> map { $0.selectedIds } |> distinctUntilChanged |> mapToSignal { peerIds in + return context.account.postbox.transaction { transaction -> [EnginePeer] in + return peerIds.compactMap { + transaction.getPeer($0) + }.map { + .init($0) + } + } + } |> deliverOnMainQueue + + actionsDisposable.add(peers.start(next: { peers in + updateState { current in + var current = current + current.selectedPeers = peers + return current + } + })) + + let arguments = Arguments(context: context, toggleEnabled: { + updateState { current in + var current = current + current.enabled = !current.enabled + return current + } + }, createMessage: { + + }, toggleSchedule: { schedule in + updateState { current in + var current = current + current.schedule = schedule + return current + } + }, toggleRecepient: { recepient in + updateState { current in + var current = current + current.recepient = recepient + return current + } + }, selectScheduleStart: { from, to in + showModal(with: DateSelectorModalController(context: context, defaultDate: from, mode: .date(title: "Schedule Start", doneTitle: strings().modalDone), selectedAt: { updated in + updateState { current in + var current = current + current.schedule = .custom(from: updated, to: to) + return current + } + }), for: context.window) + }, selectScheduleEnd: { from, to in + showModal(with: DateSelectorModalController(context: context, defaultDate: to, mode: .date(title: "Schedule End", doneTitle: strings().modalDone), selectedAt: { updated in + updateState { current in + var current = current + current.schedule = .custom(from: from, to: updated) + return current + } + }), for: context.window) + }, selectChats: { + + var items: [ShareAdditionItem] = [] + + items.append(.init(peer: TelegramFilterCategory(category: .contacts), status: "")) + items.append(.init(peer: TelegramFilterCategory(category: .nonContacts), status: "")) + + let additionTopItems = ShareAdditionItems(items: items, topSeparator: "CHAT TYPES", bottomSeparator: "CHATS") + + + showModal(with: ShareModalController(BusinessSelectChatsCallbackObject(context, defaultSelectedIds: Set(), additionTopItems: additionTopItems, limit: 100, limitReachedText: "Limit reached", callback: { peerIds in + +// let categories = peerIds.filter { +// $0.namespace._internalGetInt32Value() == ChatListFilterPeerCategories.Namespace +// } +// let peerIds = Set(peerIds).subtracting(categories) + + updateState { current in + var current = current + current.selectedIds = Array(peerIds) + return current + } + + return .complete() + }, excludePeerIds: Set([context.peerId]))), for: context.window) + }, removeIncluded: { peerId in + updateState { current in + var current = current + current.selectedIds.removeAll(where: { $0 == peerId }) + return current + } + }) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments), grouping: false) + } + + let controller = InputDataController(dataSignal: signal, title: "Away Message", removeAfterDisappear: false, hasDone: false) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + return controller + +} diff --git a/Telegram-Mac/BusinessChatbotController.swift b/Telegram-Mac/BusinessChatbotController.swift new file mode 100644 index 000000000..1da216b53 --- /dev/null +++ b/Telegram-Mac/BusinessChatbotController.swift @@ -0,0 +1,290 @@ +// +// BusinessChatbotController.swift +// Telegram +// +// Created by Mikhail Filimonov on 13.02.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import Postbox +import Cocoa +import TGUIKit +import SwiftSignalKit + +private final class Arguments { + let context: AccountContext + let toggleAccess:(State.Access)->Void + let selectChats:()->Void + let toggleReplyAccess:()->Void + let removeIncluded:(PeerId)->Void + init(context: AccountContext, toggleAccess:@escaping(State.Access)->Void, selectChats:@escaping()->Void, toggleReplyAccess:@escaping()->Void, removeIncluded:@escaping(PeerId)->Void) { + self.context = context + self.toggleAccess = toggleAccess + self.selectChats = selectChats + self.toggleReplyAccess = toggleReplyAccess + self.removeIncluded = removeIncluded + } +} + +private struct State : Equatable { + enum Access : Equatable { + case all + case selected + } + var username: String? + var access: Access = .all + + var replyAccess: Bool = true + + + var selectedIds: [PeerId] = [] + + var selectedPeers: [EnginePeer] = [] + +} + + +private let _id_header = InputDataIdentifier("_id_header") +private let _id_input = InputDataIdentifier("_id_username") +private let _id_attached_bot = InputDataIdentifier("_id_attached_bot") + +private let _id_access_1x1 = InputDataIdentifier("_id_access_1x1") +private let _id_access_selected = InputDataIdentifier("_id_access_selected") + +private let _id_include_chats = InputDataIdentifier("_id_include_chats") +private let _id_exclude_chats = InputDataIdentifier("_id_exclude_chats") + +private let _id_reply_to_message = InputDataIdentifier("_id_reply_to_message") + +private let _id_remove = InputDataIdentifier("_id_remove") + + +private func _id_peer(_ id: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_peer_\(id.toInt64())") +} + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + let hText = "Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More >](learnmore)." + + let attr = parseMarkdownIntoAttributedString(hText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.listGrayText), bold: MarkdownAttributeSet(font: .bold(.text), textColor: theme.colors.listGrayText), link: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, contents) + })) + entries.append(.custom(sectionId: sectionId, index: 0, value: .none, identifier: _id_header, equatable: nil, comparable: nil, item: { initialSize, stableId in + return AnimatedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: LocalAnimatedSticker.fly_dollar, text: attr) + })) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.input(sectionId: sectionId, index: 0, value: .string(state.username), error: nil, identifier: _id_input, mode: .plain, data: .init(viewType: .singleItem, defaultText: ""), placeholder: nil, inputPlaceholder: "Bot Username", filter: { $0 }, limit: 60)) + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("CHATS ACCESSIBLE FOR THE BOT"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: 0, value: .none, error: nil, identifier: _id_access_1x1, data: .init(name: "All 1-to-1 Chats Except...", color: theme.colors.text, type: .selectable(state.access == .all), viewType: .firstItem, action: { + arguments.toggleAccess(.all) + }))) + + entries.append(.general(sectionId: sectionId, index: 0, value: .none, error: nil, identifier: _id_access_selected, data: .init(name: "Only Selected Chats", color: theme.colors.text, type: .selectable(state.access == .selected), viewType: .lastItem, action: { + arguments.toggleAccess(.selected) + }))) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + switch state.access { + case .all: + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("EXCLUDE CHATS"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: 0, value: .none, error: nil, identifier: _id_exclude_chats, data: .init(name: "Exclude Chats...", color: theme.colors.accent, icon: theme.icons.chat_filter_add, type: .none, viewType: state.selectedPeers.isEmpty ? .singleItem : .firstItem, action: arguments.selectChats))) + + case .selected: + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("INCLUDE CHATS"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: 0, value: .none, error: nil, identifier: _id_include_chats, data: .init(name: "Include Chats...", color: theme.colors.accent, icon: theme.icons.chat_filter_add, type: .none, viewType: state.selectedPeers.isEmpty ? .singleItem : .firstItem, action: arguments.selectChats))) + } + + + struct Tuple : Equatable { + let peer: PeerEquatable + let viewType: GeneralViewType + let status: String? + } + var tuples: [Tuple] = [] + + var selectedPeers: [Peer] = [] + + let categories = state.selectedIds.filter { + $0.namespace._internalGetInt32Value() == ChatListFilterPeerCategories.Namespace + } + for category in categories { + let cat = ChatListFilterPeerCategories(rawValue: Int32(category.id._internalGetInt64Value())) + selectedPeers.append(TelegramFilterCategory(category: cat)) + } + + selectedPeers.append(contentsOf: state.selectedPeers.map { $0._asPeer() }) + + + for (i, peer) in selectedPeers.enumerated() { + var viewType: GeneralViewType = bestGeneralViewType(selectedPeers, for: i) + if i == 0 { + if i < selectedPeers.count - 1 { + viewType = .innerItem + } else { + viewType = .lastItem + } + } + let status: String? = nil + tuples.append(.init(peer: .init(peer), viewType: viewType, status: nil)) + } + + for tuple in tuples { + entries.append(.custom(sectionId: sectionId, index: 0, value: .none, identifier: _id_peer(tuple.peer.id), equatable: .init(tuple), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: tuple.peer.peer, account: arguments.context.account, context: arguments.context, stableId: stableId, height: 44, photoSize: NSMakeSize(30, 30), status: tuple.status, inset: NSEdgeInsets(left: 20, right: 20), viewType: tuple.viewType, action: { + //arguments.openInfo(peer.id) + }, contextMenuItems: { + return .single([ContextMenuItem("Remove", handler: { + arguments.removeIncluded(tuple.peer.id) + }, itemMode: .destruct, itemImage: MenuAnimation.menu_delete.value)]) + }, highlightVerified: true) + })) + } + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("BOT PERMISSIONS"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: 0, value: .none, error: nil, identifier: _id_reply_to_message, data: .init(name: "Reply to Messages", color: theme.colors.text, type: .switchable(state.replyAccess), viewType: .singleItem, action: arguments.toggleReplyAccess))) + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("The bot will be able to view all new incoming messages, but not the messages that had been sent before you added the bot."), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: 0, value: .none, error: nil, identifier: _id_remove, data: .init(name: "Remove Bot", color: theme.colors.redUI, type: .none, viewType: .singleItem, action: { + + }))) + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + +func BusinessChatbotController(context: AccountContext, peerId: PeerId) -> InputDataController { + + let actionsDisposable = DisposableSet() + + let initialState = State() + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let peers: Signal<[EnginePeer], NoError> = statePromise.get() |> map { $0.selectedIds } |> distinctUntilChanged |> mapToSignal { peerIds in + return context.account.postbox.transaction { transaction -> [EnginePeer] in + return peerIds.compactMap { + transaction.getPeer($0) + }.map { + .init($0) + } + } + } |> deliverOnMainQueue + + actionsDisposable.add(peers.start(next: { peers in + updateState { current in + var current = current + current.selectedPeers = peers + return current + } + })) + + + let arguments = Arguments(context: context, toggleAccess: { value in + updateState { current in + var current = current + current.access = value + return current + } + }, selectChats: { + + var items: [ShareAdditionItem] = [] + + items.append(.init(peer: TelegramFilterCategory(category: .contacts), status: "")) + items.append(.init(peer: TelegramFilterCategory(category: .nonContacts), status: "")) + + let additionTopItems = ShareAdditionItems(items: items, topSeparator: "CHAT TYPES", bottomSeparator: "CHATS") + + + showModal(with: ShareModalController(BusinessSelectChatsCallbackObject(context, defaultSelectedIds: Set(), additionTopItems: additionTopItems, limit: 100, limitReachedText: "Limit reached", callback: { peerIds in + +// let categories = peerIds.filter { +// $0.namespace._internalGetInt32Value() == ChatListFilterPeerCategories.Namespace +// } +// let peerIds = Set(peerIds).subtracting(categories) + + updateState { current in + var current = current + current.selectedIds = Array(peerIds) + return current + } + + return .complete() + }, excludePeerIds: Set([context.peerId]))), for: context.window) + }, toggleReplyAccess: { + updateState { current in + var current = current + current.replyAccess = !current.replyAccess + return current + } + }, removeIncluded: { peerId in + updateState { current in + var current = current + current.selectedIds.removeAll(where: { $0 == peerId }) + return current + } + }) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: "Chat Bot", removeAfterDisappear: false, hasDone: false) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + return controller + +} diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift new file mode 100644 index 000000000..d1837f0eb --- /dev/null +++ b/Telegram-Mac/BusinessHoursController.swift @@ -0,0 +1,448 @@ +// +// BusinessHoursController.swift +// Telegram +// +// Created by Mikhail Filimonov on 12.02.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import Postbox +import Cocoa +import TGUIKit +import SwiftSignalKit + +private struct GMTZone : Equatable, Comparable { + static func < (lhs: GMTZone, rhs: GMTZone) -> Bool { + return lhs.hoursFromGMT < rhs.hoursFromGMT + } + let timeZone: TimeZone + + init(timeZone: TimeZone, abbreviation: String) { + self.timeZone = timeZone + self.hoursFromGMT = TimeInterval(timeZone.secondsFromGMT()) / 60.0 / 60.0 + self.abbreviation = abbreviation.replacingOccurrences(of: "_", with: " ") + } + let hoursFromGMT: TimeInterval + let abbreviation: String + + var text: String { + let gmtText = "\(hoursFromGMT)" + .replacingOccurrences(of: ".5", with: ":30") + .replacingOccurrences(of: ".0", with: "") + + if hoursFromGMT >= 0 { + return "GMT+\(gmtText), \(abbreviation)" + } else { + return "GMT\(gmtText), \(abbreviation)" + } + } +} + +private func getAllGMTTimeZones() -> [GMTZone] { + // Fetch all known timezone identifiers + let allTimeZoneIdentifiers = TimeZone.abbreviationDictionary.map { $0.value } + + let gmtTimeZones = allTimeZoneIdentifiers + + // Optionally, sort the array if you need the time zones in a specific order + let sortedGMTTimeZones = gmtTimeZones.uniqueElements + + return sortedGMTTimeZones.map { + GMTZone(timeZone: TimeZone(identifier: $0)!, abbreviation: $0) + }.sorted(by: <) +} + +private func formatHourToLocaleTime(hour: Int) -> String { + guard hour >= 0 && hour < 24 else { + print("Invalid hour. Please provide a value between 0 and 23.") + return "" + } + + // Create a DateComponents object with the hour set to the provided value + var components = DateComponents() + components.hour = hour + + // Use the current calendar to ensure the components are interpreted correctly + let calendar = Calendar.current + + // Optional: you might want to ensure you're using the current time zone + components.timeZone = TimeZone.current + + // Create a Date from components + guard let date = calendar.date(from: components) else { + print("Failed to create date from components.") + return "" + } + + // Create a DateFormatter and set its dateStyle to .none and timeStyle to .short + // This will ensure that the time is formatted according to the user's locale + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .short + + // Format the date to a string + let formattedTime = formatter.string(from: date) + + return formattedTime +} + +// Example usage +let formattedTime = formatHourToLocaleTime(hour: 3) + +private final class Arguments { + let context: AccountContext + let toggleEnabled:()->Void + let toggleDay:(State.Day)->Void + let enableDay:(State.Day)->Void + let addSpecific:(State.Day)->Void + let editSpefic:(State.Day, State.Hours.Hour)->Void + let removeSpefic:(State.Day, State.Hours.Hour)->Void + let selectTimezone:(GMTZone)->Void + init(context: AccountContext, toggleEnabled:@escaping()->Void, toggleDay:@escaping(State.Day)->Void, enableDay:@escaping(State.Day)->Void, addSpecific:@escaping(State.Day)->Void, removeSpefic:@escaping(State.Day, State.Hours.Hour)->Void, editSpefic:@escaping(State.Day, State.Hours.Hour)->Void, selectTimezone:@escaping(GMTZone)->Void) { + self.context = context + self.toggleEnabled = toggleEnabled + self.toggleDay = toggleDay + self.enableDay = enableDay + self.addSpecific = addSpecific + self.removeSpefic = removeSpefic + self.editSpefic = editSpefic + self.selectTimezone = selectTimezone + } +} + +private struct State : Equatable { + enum Day : Int32 { + case monday + case tuesday + case wednesday + case thrusday + case friday + case saturday + case sunday + + var title: String { + switch self { + case .monday: + return "Monday" + case .tuesday: + return "Tuesday" + case .wednesday: + return "Wednesday" + case .thrusday: + return "Thrusday" + case .friday: + return "Friday" + case .saturday: + return "Saturday" + case .sunday: + return "Sunday" + } + } + + static var all: [Day] { + return [.monday, .tuesday, .wednesday, .thrusday, .friday, .saturday, .sunday] + } + } + struct Hours: Equatable { + struct Hour : Equatable { + var from: Int + var to: Int + var uniqueId: Int64 + } + var list:[Hour] = [] + } + + var enabled: Bool = false + + var data:[Day: Hours] = [:] + + var timezone: GMTZone + + func openingHours(_ day: Day) -> String { + if let hours = data[day] { + if hours.list.isEmpty { + return "24 hours" + } else { + var text: String = "" + for (i, hour) in hours.list.enumerated() { + text += "\(formatHourToLocaleTime(hour: hour.from)) - \(formatHourToLocaleTime(hour: hour.to))" + if i < hours.list.count - 1 { + text += ", " + } + } + return text + } + } else { + return "Closed" + } + } +} + +private let _id_header = InputDataIdentifier("_id_header") +private let _id_enabled = InputDataIdentifier("_id_enabled") + +private func _id_day(_ day: State.Day) -> InputDataIdentifier { + return InputDataIdentifier("_id_day_\(day.rawValue)") +} +private let _id_timezone = InputDataIdentifier("_id_timezone") + +private let _id_add_specific = InputDataIdentifier("_id_add_specific") + +private func _id_opening_time(_ day: State.Hours.Hour) -> InputDataIdentifier { + return InputDataIdentifier("_id_opening_time\(day.uniqueId)") +} +private func _id_closing_time(_ day: State.Hours.Hour) -> InputDataIdentifier { + return InputDataIdentifier("_id_closing_time\(day.uniqueId)") +} +private func _id_remove_opening(_ day: State.Hours.Hour) -> InputDataIdentifier { + return InputDataIdentifier("_id_remove_opening\(day.uniqueId)") +} + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_header, equatable: nil, comparable: nil, item: { initialSize, stableId in + return AnimatedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: LocalAnimatedSticker.fly_dollar, text: .initialize(string: "Turn this on to show your opening hours schedule to your customers.", color: theme.colors.listGrayText, font: .normal(.text))) + })) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_enabled, data: .init(name: "Show Business Hours", color: theme.colors.text, type: .switchable(state.enabled), viewType: .singleItem, action: arguments.toggleEnabled))) + + // entries + + + + if state.enabled { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("BUSINESS HOURS"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + for (i, day) in State.Day.all.enumerated() { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_day(day), data: .init(name: day.title, color: theme.colors.text, type: .switchable(state.data[day] != nil), viewType: bestGeneralViewType(State.Day.all, for: i), description: state.openingHours(day), descTextColor: theme.colors.accent, action: { + arguments.toggleDay(day) + }, switchAction: { + arguments.enableDay(day) + }))) + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let zones: [ContextMenuItem] = getAllGMTTimeZones().map { zone in + return ContextMenuItem(zone.text, handler: { + arguments.selectTimezone(zone) + }, state: zone.abbreviation == state.timezone.abbreviation ? .on : nil) + } + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_timezone, data: .init(name: "Timezone", color: theme.colors.text, type: .contextSelector(state.timezone.text, zones), viewType: .singleItem))) + + + } + + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + + +private func dayEntries(_ state: State, day: State.Day, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_enabled, data: .init(name: "Open On This Day", color: theme.colors.text, type: .switchable(state.data[day] != nil), viewType: .singleItem, action: { + arguments.enableDay(day) + }))) + + // entries + + if let hours = state.data[day] { + let getHoursMenu:(State.Hours.Hour, Bool)->[ContextMenuItem] = { hour, from in + var items:[ContextMenuItem] = [] + + var start: Int = 0 + var end: Int = 24 + + if from { + start = 0 + end = hour.to + } else { + start = hour.from + end = 24 + } + + for i in start ..< end { + items.append(.init(formatHourToLocaleTime(hour: i), handler: { + arguments.editSpefic(day, .init(from: from ? i : hour.from, to: !from ? i : hour.to, uniqueId: hour.uniqueId)) + }, state: i == (from ? hour.from : hour.to) ? .on : nil)) + } + return items + } + + for hour in hours.list { + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_opening_time(hour), data: .init(name: "Opening Time", color: theme.colors.text, type: .contextSelector(formatHourToLocaleTime(hour: hour.from), getHoursMenu(hour, true)), viewType: .firstItem))) + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_closing_time(hour), data: .init(name: "Closing Time", color: theme.colors.text, type: .contextSelector(formatHourToLocaleTime(hour: hour.to), getHoursMenu(hour, false)), viewType: .innerItem))) + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_remove_opening(hour), data: .init(name: "Remove", color: theme.colors.redUI, type: .none, viewType: .lastItem, action: { + arguments.removeSpefic(day, hour) + }))) + + } + } + + + let count = state.data[day]?.list.count ?? 0 + + if count < 3 { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_add_specific, data: .init(name: "Add a Set of Hours", color: theme.colors.accent, type: .none, viewType: .singleItem, action: { + arguments.addSpecific(day) + }))) + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("Specify your working hours during the day."), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + } + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + + +func BusinessHoursController(context: AccountContext, peerId: PeerId) -> InputDataController { + + let actionsDisposable = DisposableSet() + + let timezone = GMTZone(timeZone: TimeZone.current, abbreviation: "") + + let effectiveTimezone = getAllGMTTimeZones().first(where: { $0.hoursFromGMT == timezone.hoursFromGMT })! + + let initialState = State(timezone: effectiveTimezone) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + var getArguments:(()->Arguments?)? = nil + + let arguments = Arguments(context: context, toggleEnabled: { + updateState { current in + var current = current + current.enabled = !current.enabled + return current + } + }, toggleDay: { day in + if let arguments = getArguments?() { + context.bindings.rootNavigation().push(BusinessdayHoursController(context: context, stateSignal: statePromise.get(), arguments: arguments, day: day)) + } + }, enableDay: { day in + updateState { current in + var current = current + if current.data[day] != nil { + current.data.removeValue(forKey: day) + } else { + current.data[day] = .init(list: []) + } + return current + } + }, addSpecific: { day in + updateState { current in + var current = current + var hours = current.data[day] ?? .init() + hours.list.append(.init(from: 9, to: 23, uniqueId: arc4random64())) + current.data[day] = hours + return current + } + }, removeSpefic: { day, hour in + updateState { current in + var current = current + var hours = current.data[day] ?? .init() + hours.list.removeAll(where: { $0.uniqueId == hour.uniqueId }) + current.data[day] = hours + return current + } + }, editSpefic: { day, hour in + updateState { current in + var current = current + var hours = current.data[day] ?? .init() + if let hourIndex = hours.list.firstIndex(where: { $0.uniqueId == hour.uniqueId }) { + hours.list[hourIndex] = hour + } + current.data[day] = hours + return current + } + }, selectTimezone: { timezone in + updateState { current in + var current = current + current.timezone = timezone + return current + } + }) + + getArguments = { [weak arguments] in + return arguments + } + + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments), grouping: false) + } + + let controller = InputDataController(dataSignal: signal, title: "Business Hours", removeAfterDisappear: false, hasDone: false) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + return controller + +} + + + + + + +private func BusinessdayHoursController(context: AccountContext, stateSignal: Signal, arguments: Arguments, day: State.Day) -> InputDataController { + + let signal = stateSignal |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: dayEntries(state, day: day, arguments: arguments), grouping: false) + } + + let controller = InputDataController(dataSignal: signal, title: day.title, removeAfterDisappear: false, hasDone: false) + + return controller + +} + diff --git a/Telegram-Mac/BusinessLocationController.swift b/Telegram-Mac/BusinessLocationController.swift new file mode 100644 index 000000000..a72c85fa8 --- /dev/null +++ b/Telegram-Mac/BusinessLocationController.swift @@ -0,0 +1,327 @@ +// +// BusinessLocationController.swift +// Telegram +// +// Created by Mikhail Filimonov on 12.02.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import Postbox +import Cocoa +import TGUIKit +import SwiftSignalKit +import MapKit + +private final class AnnotationView : MKAnnotationView { + private let locationPin = ImageView() + override init(annotation: MKAnnotation?, reuseIdentifier: String?) { + super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) + + + self.wantsLayer = true + + layer?.masksToBounds = false + + + + locationPin.image = darkAppearance.icons.locationMapPin + locationPin.sizeToFit() + + + frame = CGRect(x: 0, y: 0, width: 60, height: 60) + wantsLayer = true + + + addSubview(locationPin) + + + update() + } + override var annotation: MKAnnotation? { + didSet { + update() + } + } + + override func layout() { + super.layout() + locationPin.center() + locationPin.setFrameOrigin(NSMakePoint(locationPin.frame.minX, locationPin.frame.minY - locationPin.frame.height / 2)) + } + + private func update() { + + } + + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + static var reuseIdentifier: String { + return "peer" + } + +} + + + +private class MapRowItem: GeneralRowItem { + let context: AccountContext + fileprivate let location: State.Location + init(_ initialSize: NSSize, height: CGFloat, stableId: AnyHashable, context: AccountContext, location: State.Location, viewType: GeneralViewType) { + self.context = context + self.location = location + super.init(initialSize, height: height, stableId: stableId, viewType: viewType) + } + + deinit { + + } + + override func viewClass() -> AnyClass { + return MapRowItemView.self + } + +} + +private final class MapRowItemView : GeneralContainableRowView, MKMapViewDelegate { + private let mapView: MKMapView = MKMapView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(mapView) + mapView.register(AnnotationView.self, forAnnotationViewWithReuseIdentifier: AnnotationView.reuseIdentifier) + mapView.delegate = self + + mapView.showsZoomControls = false + mapView.showsUserLocation = false + + mapView.showsBuildings = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + mapView.delegate = nil + } + + override func layout() { + super.layout() + mapView.frame = bounds + } + + + func focusVenue() { + guard let item = item as? MapRowItem else { + return + } + let userLocation = item.location.coordinate + var region = MKCoordinateRegion() + var span = MKCoordinateSpan() + span.latitudeDelta = CLLocationDegrees(0.005) + span.longitudeDelta = CLLocationDegrees(0.005) + var location = CLLocationCoordinate2D() + location.latitude = userLocation.latitude + location.longitude = userLocation.longitude + region.span = span + region.center = location + mapView.setRegion(region, animated: true) + } + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + switch annotation { + case is State.Location: + return mapView.dequeueReusableAnnotationView(withIdentifier: AnnotationView.reuseIdentifier, for: annotation) + default: + return nil + } + } + + func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { + guard let item = item as? MapRowItem, let location = userLocation.location else { + return + } + } + + func mapViewDidStopLocatingUser(_ mapView: MKMapView) { + guard let item = item as? MapRowItem, let location = mapView.userLocation.location else { + return + } + } + + + func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + + } + + private var location: NSPoint? = nil + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + location = nil + + } + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + location = event.locationInWindow + } + + override func set(item: TableRowItem, animated: Bool = false) { + let previousItem = self.item + super.set(item: item, animated: animated) + + + guard let item = item as? MapRowItem else { + return + } + + mapView.appearance = theme.appearance + + focusVenue() + + if let previousItem = previousItem as? MapRowItem { + mapView.removeAnnotation(previousItem.location) + } + + mapView.addAnnotation(item.location) + + } +} + + +extension CLLocationCoordinate2D : Equatable { + public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { + return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude + } +} + +private final class Arguments { + let context: AccountContext + let setLocation:()->Void + init(context: AccountContext, setLocation:@escaping()->Void) { + self.context = context + self.setLocation = setLocation + } +} + +private struct State : Equatable { + class Location : NSObject, MKAnnotation { + var coordinate: CLLocationCoordinate2D + var venue: MapVenue? + init(coordinate: CLLocationCoordinate2D, venue: MapVenue?) { + self.coordinate = coordinate + self.venue = venue + } + } + var address: String? + var location: Location? +} + +private let _id_header = InputDataIdentifier("_id_header") +private let _id_input = InputDataIdentifier("_id_enabled") + +private let _id_map_enabled = InputDataIdentifier("_id_map_enabled") +private let _id_map_map = InputDataIdentifier("_id_map_enabled") + + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.custom(sectionId: sectionId, index: 0, value: .none, identifier: _id_header, equatable: nil, comparable: nil, item: { initialSize, stableId in + return AnimatedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: LocalAnimatedSticker.fly_dollar, text: .initialize(string: "Display the location of your business on your account.", color: theme.colors.listGrayText, font: .normal(.text))) + })) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.input(sectionId: sectionId, index: 0, value: .string(state.address), error: nil, identifier: _id_input, mode: .plain, data: .init(viewType: .singleItem), placeholder: nil, inputPlaceholder: "Enter Address", filter: { $0 }, limit: 256)) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: 0, value: .none, error: nil, identifier: _id_map_enabled, data: .init(name: "Set Location on Map", color: theme.colors.text, type: .switchable(state.location != nil), viewType: state.location != nil ? .firstItem : .singleItem, action: arguments.setLocation, autoswitch: false))) + + if let location = state.location { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_map_map, equatable: .init(state.location), comparable: nil, item: { initialSize, stableId in + return MapRowItem(initialSize, height: 200, stableId: stableId, context: arguments.context, location: location, viewType: .lastItem) + })) + } + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func BusinessLocationController(context: AccountContext, peerId: PeerId) -> InputDataController { + + let actionsDisposable = DisposableSet() + + let initialState = State() + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let chatInteraction = ChatInteraction(chatLocation: .peer(peerId), context: context) + + chatInteraction.sendLocation = { location, venue in + updateState { current in + var current = current + current.location = .init(coordinate: location, venue: venue) + return current + } + } + + let arguments = Arguments(context: context, setLocation: { + let value = stateValue.with { $0.location } + + if value != nil { + updateState { current in + var current = current + current.location = nil + return current + } + } else { + showModal(with: LocationModalController(chatInteraction, destination: .business), for: context.window) + } + }) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: "Location", removeAfterDisappear: false, hasDone: false) + + controller.updateDatas = { datas in + updateState { current in + var current = current + current.address = datas[_id_input]?.stringValue + return current + } + return .none + } + + controller.onDeinit = { + actionsDisposable.dispose() + } + + return controller + +} diff --git a/Telegram-Mac/ChannelAdminController.swift b/Telegram-Mac/ChannelAdminController.swift index e7d1f3aa2..0967477eb 100644 --- a/Telegram-Mac/ChannelAdminController.swift +++ b/Telegram-Mac/ChannelAdminController.swift @@ -395,6 +395,7 @@ private func channelAdminControllerEntries(state: ChannelAdminControllerState, a .direct(.canBanUsers), .direct(.canInviteUsers), .direct(.canPinMessages), + .sub(.stories, storiesRelatedFlags), .direct(.canManageCalls), .direct(.canBeAnonymous), .direct(.canAddAdmins) diff --git a/Telegram-Mac/ChannelAdminsViewController.swift b/Telegram-Mac/ChannelAdminsViewController.swift index 18aa8bbfa..d184b5478 100644 --- a/Telegram-Mac/ChannelAdminsViewController.swift +++ b/Telegram-Mac/ChannelAdminsViewController.swift @@ -20,13 +20,15 @@ fileprivate final class ChannelAdminsControllerArguments { let removeAdmin: (PeerId) -> Void let eventLogs:() -> Void let toggleAntispam: (Bool)->Void - init(context: AccountContext, addAdmin:@escaping()->Void, openAdmin:@escaping(RenderedChannelParticipant) -> Void, removeAdmin:@escaping(PeerId)->Void, eventLogs: @escaping()->Void, toggleAntispam:@escaping(Bool)->Void) { + let toggleSignatures:(Bool)->Void + init(context: AccountContext, addAdmin:@escaping()->Void, openAdmin:@escaping(RenderedChannelParticipant) -> Void, removeAdmin:@escaping(PeerId)->Void, eventLogs: @escaping()->Void, toggleAntispam:@escaping(Bool)->Void, toggleSignatures:@escaping(Bool)->Void) { self.context = context self.addAdmin = addAdmin self.openAdmin = openAdmin self.removeAdmin = removeAdmin self.eventLogs = eventLogs self.toggleAntispam = toggleAntispam + self.toggleSignatures = toggleSignatures } } @@ -54,6 +56,8 @@ fileprivate enum ChannelAdminsEntry : Identifiable, Comparable { case adminPeerItem(sectionId:Int32, Int32, RenderedChannelParticipant, ShortPeerDeleting?, GeneralViewType) case addAdmin(sectionId:Int32, GeneralViewType) case adminsInfo(sectionId:Int32, String, GeneralViewType) + case signMessages(sectionId: Int32, sign:Bool, viewType: GeneralViewType) + case signMessagesInfo(sectionId: Int32, viewType: GeneralViewType) case section(Int32) case loading var stableId: ChannelAdminsEntryStableId { @@ -72,6 +76,10 @@ fileprivate enum ChannelAdminsEntry : Identifiable, Comparable { return .index(5) case .adminsInfo: return .index(6) + case .signMessages: + return .index(7) + case .signMessagesInfo: + return .index(8) case let .section(sectionId): return .index((sectionId + 1) * 1000 - sectionId) case let .adminPeerItem(_, _, participant, _, _): @@ -96,6 +104,10 @@ fileprivate enum ChannelAdminsEntry : Identifiable, Comparable { return (sectionId * 1000) + stableId.index case let .adminsInfo(sectionId, _, _): return (sectionId * 1000) + stableId.index + case let .signMessages(sectionId, _, _): + return (sectionId * 1000) + stableId.index + case let .signMessagesInfo(sectionId, _): + return (sectionId * 1000) + stableId.index case let .adminPeerItem(sectionId, index, _, _, _): return (sectionId * 1000) + index + stableId.index case let .section(sectionId): @@ -231,8 +243,29 @@ private func channelAdminsControllerEntries(context: AccountContext, accountPeer if index > 0 { entries.append(.section(sectionId)) sectionId += 1 + } + + if !isGroup { + + let messagesShouldHaveSignatures:Bool + switch peer.info { + case let .broadcast(info): + messagesShouldHaveSignatures = info.flags.contains(.messagesShouldHaveSignatures) + default: + messagesShouldHaveSignatures = false + } + + if peer.hasPermission(.changeInfo) { + entries.append(.signMessages(sectionId: sectionId, sign:messagesShouldHaveSignatures, viewType: .singleItem)) + entries.append(.signMessagesInfo(sectionId: sectionId, viewType: .textBottomItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + + } } + } else if let peer = view.peers[view.peerId] as? TelegramGroup { entries.append(.adminsHeader(sectionId: sectionId, strings().adminsGroupAdmins, .textTopItem)) @@ -355,6 +388,13 @@ fileprivate func prepareTransition(left:[AppearanceWrapperEntry { self?.navigationController?.push(ChannelEventLogController(context, peerId: peerId)) }, toggleAntispam: { value in _ = showModalProgress(signal: context.engine.peers.toggleAntiSpamProtection(peerId: peerId, enabled: value), for: context.window).start() + }, toggleSignatures: { enabled in + _ = context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: peerId, enabled: enabled).startStandalone() }) let peerView = Promise() diff --git a/Telegram-Mac/ChannelInfoEntries.swift b/Telegram-Mac/ChannelInfoEntries.swift index a285a263a..518f78754 100644 --- a/Telegram-Mac/ChannelInfoEntries.swift +++ b/Telegram-Mac/ChannelInfoEntries.swift @@ -1267,8 +1267,8 @@ func channelInfoEntries(view: PeerView, arguments:PeerInfoArguments, mediaTabsDa } if channel.hasPermission(.changeInfo) { - entries.append(.signMessages(sectionId: .sign, sign: messagesShouldHaveSignatures, viewType: .singleItem)) - entries.append(.signDesc(sectionId: .sign, viewType: .textBottomItem)) +// entries.append(.signMessages(sectionId: .sign, sign: messagesShouldHaveSignatures, viewType: .singleItem)) +// entries.append(.signDesc(sectionId: .sign, viewType: .textBottomItem)) } if channel.flags.contains(.isCreator) { entries.append(.leave(sectionId: .destruct, isCreator: channel.flags.contains(.isCreator), viewType: .singleItem)) diff --git a/Telegram-Mac/ChannelPermissionsController.swift b/Telegram-Mac/ChannelPermissionsController.swift index b373f0196..edf2a0627 100644 --- a/Telegram-Mac/ChannelPermissionsController.swift +++ b/Telegram-Mac/ChannelPermissionsController.swift @@ -672,14 +672,18 @@ final class ChannelPermissionsController : TableViewController { let updateDefaultRightsDisposable = MetaDisposable() actionsDisposable.add(updateDefaultRightsDisposable) + var first: Bool = true actionsDisposable.add(context.account.viewTracker.peerView(peerId).start(next: { peerView in updateState { current in var current = current current.peer = PeerEquatable(peerView.peers[peerId]) current.cachedData = CachedDataEquatable(peerView.cachedData) - current.restrictBoosters = (peerView.cachedData as? CachedChannelData)?.boostsToUnrestrict + if first { + current.restrictBoosters = (peerView.cachedData as? CachedChannelData)?.boostsToUnrestrict + } return current } + first = false })) diff --git a/Telegram-Mac/ChatListFilterController.swift b/Telegram-Mac/ChatListFilterController.swift index d04208d21..9bcb1d4c5 100644 --- a/Telegram-Mac/ChatListFilterController.swift +++ b/Telegram-Mac/ChatListFilterController.swift @@ -575,10 +575,10 @@ private extension ChatListFilter { class SelectCallbackObject : ShareObject { private let callback:([PeerId])->Signal private let limitReachedText: String - init(_ context: AccountContext, defaultSelectedIds: Set, additionTopItems: ShareAdditionItems?, limit: Int?, limitReachedText: String, callback:@escaping([PeerId])->Signal) { + init(_ context: AccountContext, defaultSelectedIds: Set, additionTopItems: ShareAdditionItems?, limit: Int?, limitReachedText: String, callback:@escaping([PeerId])->Signal, excludePeerIds: Set = Set()) { self.callback = callback self.limitReachedText = limitReachedText - super.init(context, defaultSelectedIds: defaultSelectedIds, additionTopItems: additionTopItems, limit: limit) + super.init(context, excludePeerIds: excludePeerIds, defaultSelectedIds: defaultSelectedIds, additionTopItems: additionTopItems, limit: limit) } override var selectTopics: Bool { @@ -1445,7 +1445,7 @@ func ChatListFilterController(context: AccountContext, filter: ChatListFilter, i peers.append(peer) } } - return !peers.filter { peerCanBeSharedInFolder($0) }.isEmpty + return !peers.filter( { peerCanBeSharedInFolder($0) }).isEmpty } |> deliverOnMainQueue _ = combineLatest(folderLimits, canCreateLink).start(next: { limits, canCreateLink in diff --git a/Telegram-Mac/ChatListFiltersListController.swift b/Telegram-Mac/ChatListFiltersListController.swift index f4fa15eb2..3a7b779ae 100644 --- a/Telegram-Mac/ChatListFiltersListController.swift +++ b/Telegram-Mac/ChatListFiltersListController.swift @@ -154,9 +154,12 @@ private func chatListPresetEntries(filtersWithCounts: [(ChatListFilter, Int)], s sectionId += 1 //TODO:LANG + #if DEBUG entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_show_tags, data: .init(name: "Show Folders Tags", color: theme.colors.text, type: .switchable(showTags), viewType: .singleItem, action: arguments.toggleTags))) entries.append(.desc(sectionId: sectionId, index: index, text: .plain("Display folder names for each chat in the chat list."), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textBottomItem))) index += 1 + #endif + diff --git a/Telegram-Mac/ChatListRowView.swift b/Telegram-Mac/ChatListRowView.swift index 289a94bd9..4a6a17871 100644 --- a/Telegram-Mac/ChatListRowView.swift +++ b/Telegram-Mac/ChatListRowView.swift @@ -2927,6 +2927,14 @@ class ChatListRowView: TableRowView, ViewDisplayDelegate, RevealTableView { var mediaPreviewOffset = NSMakePoint(inset, displayNameView.frame.height + item.margin + 2 + offset) let contentImageSpacing: CGFloat = 2.0 + if tagsView != nil { + if let chatNameTextView = chatNameTextView { + mediaPreviewOffset.y -= displayNameView.frame.height + mediaPreviewOffset.x += chatNameTextView.frame.width + 3 + } + + } + for (message, _, mediaSize) in self.currentMediaPreviewSpecs { if let previewView = self.mediaPreviewViews[message.id] { previewView.frame = CGRect(origin: mediaPreviewOffset, size: mediaSize) @@ -2963,8 +2971,6 @@ class ChatListRowView: TableRowView, ViewDisplayDelegate, RevealTableView { if let messageTextView = messageTextView { if tagsView == nil || chatNameTextView == nil { - - messageTextView.setFrameOrigin(NSMakePoint(item.leftInset, displayHeight + item.margin + 1 + messageOffset)) } else if let chatNameTextView = chatNameTextView { let maxX = [chatNameTextView, forumTopicTextView].compactMap { $0 }.map { $0.frame.maxX + 3 }.max() diff --git a/Telegram-Mac/ChatMessageMenuItems.swift b/Telegram-Mac/ChatMessageMenuItems.swift index 39a0ad785..c75bddf8d 100644 --- a/Telegram-Mac/ChatMessageMenuItems.swift +++ b/Telegram-Mac/ChatMessageMenuItems.swift @@ -102,13 +102,15 @@ func chatMenuItemsData(for message: Message, textLayout: (TextViewLayout?, LinkT let storyMedia = message.media.first as? TelegramMediaStory let isMediaStory = storyMedia?.storyId.peerId == context.peerId ? false : storyMedia != nil + + let incoming: Bool = message.isIncoming(context.account, false) let isRead: Bool if case let .MessageEntry(_, _, _isRead, _, _, _, _) = entry { - isRead = _isRead + isRead = _isRead || incoming } else if case let .groupedPhotos(entries, _) = entry, case let .MessageEntry(_, _, _isRead, _, _, _, _) = entries.first { - isRead = _isRead + isRead = _isRead || incoming } else { - isRead = false + isRead = incoming } var file: TelegramMediaFile? = nil diff --git a/Telegram-Mac/CoreExtension.swift b/Telegram-Mac/CoreExtension.swift index 5fe1b5c96..6ecb723a5 100644 --- a/Telegram-Mac/CoreExtension.swift +++ b/Telegram-Mac/CoreExtension.swift @@ -3240,6 +3240,13 @@ func bigEmojiMessage(_ sharedContext: SharedAccountContext, message: Message) -> struct PeerEquatable: Equatable { let peer: Peer + + var peerId: PeerId { + return peer.id + } + var id: PeerId { + return peer.id + } init(peer: Peer) { self.peer = peer } diff --git a/Telegram-Mac/DevicesContext.swift b/Telegram-Mac/DevicesContext.swift index e2046e3cf..287b5db9d 100644 --- a/Telegram-Mac/DevicesContext.swift +++ b/Telegram-Mac/DevicesContext.swift @@ -149,16 +149,16 @@ final class DevicesContext : NSObject { init(_ accountManager: AccountManager ) { super.init() - - var prop : CMIOObjectPropertyAddress = CMIOObjectPropertyAddress( - mSelector: CMIOObjectPropertySelector(kCMIOHardwarePropertyAllowScreenCaptureDevices), - mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal), - mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementMaster)) - - var allow: UInt32 = 1 - CMIOObjectSetPropertyData(CMIOObjectID(kCMIOObjectSystemObject), - &prop, 0, nil, - UInt32(sizeof(allow)), &allow ); +// +// var prop : CMIOObjectPropertyAddress = CMIOObjectPropertyAddress( +// mSelector: CMIOObjectPropertySelector(kCMIOHardwarePropertyAllowScreenCaptureDevices), +// mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal), +// mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementMaster)) +// +// var allow: UInt32 = 1 +// CMIOObjectSetPropertyData(CMIOObjectID(kCMIOObjectSystemObject), +// &prop, 0, nil, +// UInt32(sizeof(allow)), &allow ); diff --git a/Telegram-Mac/EmojiesController.swift b/Telegram-Mac/EmojiesController.swift index 2e9980d27..d1826cd0c 100644 --- a/Telegram-Mac/EmojiesController.swift +++ b/Telegram-Mac/EmojiesController.swift @@ -2495,7 +2495,7 @@ final class EmojiesController : TelegramGenericViewController map { $0 as? CachedChannelData } |> map { $0?.emojiPack } groupEmojiPack = emojiPack |> mapToSignal { info in diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 64f97eae9..6fcf4f7f2 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259532 + 259648 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/InputDataController.swift b/Telegram-Mac/InputDataController.swift index 650bd0e35..4ef0843c8 100644 --- a/Telegram-Mac/InputDataController.swift +++ b/Telegram-Mac/InputDataController.swift @@ -307,7 +307,7 @@ func prepareInputDataTransition(left:[AppearanceWrapperEntry], r } }) if !cancelled.with({ $0 }) { - subscriber.putNext(TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated:animated, state: .none(nil), animateVisibleOnly: !animateEverything, searchState: searchState)) + subscriber.putNext(TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated:animated, state: .none(nil), grouping: grouping, animateVisibleOnly: !animateEverything, searchState: searchState)) subscriber.putCompletion() } } diff --git a/Telegram-Mac/InputDataControllerEntries.swift b/Telegram-Mac/InputDataControllerEntries.swift index 77e287086..b3be02afc 100644 --- a/Telegram-Mac/InputDataControllerEntries.swift +++ b/Telegram-Mac/InputDataControllerEntries.swift @@ -240,7 +240,8 @@ final class InputDataGeneralData : Equatable { let disableBorder: Bool let descTextColor: NSColor? let afterNameImage: CGImage? - init(name: String, color: NSColor, icon: CGImage? = nil, type: GeneralInteractedType = .none, viewType: GeneralViewType = .legacy, enabled: Bool = true, description: String? = nil, descTextColor: NSColor? = nil, justUpdate: Int64? = nil, action: (()->Void)? = nil, switchAction: (()->Void)? = nil, disabledAction: (()->Void)? = nil, menuItems:(()->[ContextMenuItem])? = nil, descClick: (()->Void)? = nil, theme: GeneralRowItem.Theme? = nil, disableBorder: Bool = false, nameAttributed: NSAttributedString? = nil, afterNameImage: CGImage? = nil) { + let autoswitch: Bool + init(name: String, color: NSColor, icon: CGImage? = nil, type: GeneralInteractedType = .none, viewType: GeneralViewType = .legacy, enabled: Bool = true, description: String? = nil, descTextColor: NSColor? = nil, justUpdate: Int64? = nil, action: (()->Void)? = nil, switchAction: (()->Void)? = nil, disabledAction: (()->Void)? = nil, menuItems:(()->[ContextMenuItem])? = nil, descClick: (()->Void)? = nil, theme: GeneralRowItem.Theme? = nil, disableBorder: Bool = false, nameAttributed: NSAttributedString? = nil, afterNameImage: CGImage? = nil, autoswitch: Bool = true) { self.name = name self.color = color self.icon = icon @@ -259,10 +260,11 @@ final class InputDataGeneralData : Equatable { self.nameAttributed = nameAttributed self.descTextColor = descTextColor self.afterNameImage = afterNameImage + self.autoswitch = autoswitch } static func ==(lhs: InputDataGeneralData, rhs: InputDataGeneralData) -> Bool { - return lhs.name == rhs.name && lhs.icon === rhs.icon && lhs.color.hexString == rhs.color.hexString && lhs.type == rhs.type && lhs.description == rhs.description && lhs.viewType == rhs.viewType && lhs.enabled == rhs.enabled && lhs.justUpdate == rhs.justUpdate && lhs.theme == rhs.theme && lhs.disableBorder == rhs.disableBorder && lhs.nameAttributed == rhs.nameAttributed && lhs.descTextColor == rhs.descTextColor && lhs.afterNameImage == rhs.afterNameImage + return lhs.name == rhs.name && lhs.icon === rhs.icon && lhs.color.hexString == rhs.color.hexString && lhs.type == rhs.type && lhs.description == rhs.description && lhs.viewType == rhs.viewType && lhs.enabled == rhs.enabled && lhs.justUpdate == rhs.justUpdate && lhs.theme == rhs.theme && lhs.disableBorder == rhs.disableBorder && lhs.nameAttributed == rhs.nameAttributed && lhs.descTextColor == rhs.descTextColor && lhs.afterNameImage == rhs.afterNameImage && lhs.autoswitch == rhs.autoswitch } } @@ -500,7 +502,7 @@ enum InputDataEntry : Identifiable, Comparable { case let .general(_, _, value, error, identifier, data): return GeneralInteractedRowItem(initialSize, stableId: stableId, name: data.name, nameAttributed: data.nameAttributed, icon: data.icon, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: data.color), description: data.description, descTextColor: data.descTextColor ?? data.theme?.grayTextColor ?? theme.colors.text, type: data.type, viewType: data.viewType, action: { data.action != nil ? data.action?() : arguments.select((identifier, value)) - }, enabled: data.enabled, switchAppearance: data.theme?.switchAppearance ?? switchViewAppearance, error: error, disabledAction: data.disabledAction ?? {}, menuItems: data.menuItems, customTheme: data.theme, disableBorder: data.disableBorder, switchAction: data.switchAction, descClick: data.descClick, afterNameImage: data.afterNameImage) + }, enabled: data.enabled, switchAppearance: data.theme?.switchAppearance ?? switchViewAppearance, error: error, autoswitch: data.autoswitch, disabledAction: data.disabledAction ?? {}, menuItems: data.menuItems, customTheme: data.theme, disableBorder: data.disableBorder, switchAction: data.switchAction, descClick: data.descClick, afterNameImage: data.afterNameImage) case let .dateSelector(_, _, value, error, _, placeholder): return InputDataDateRowItem(initialSize, stableId: stableId, value: value, error: error, updated: arguments.dataUpdated, placeholder: placeholder) case let .input(_, _, value, error, _, mode, data, placeholder, inputPlaceholder, filter, limit: limit): @@ -528,38 +530,38 @@ func <(lhs: InputDataEntry, rhs: InputDataEntry) -> Bool { func ==(lhs: InputDataEntry, rhs: InputDataEntry) -> Bool { switch lhs { - case let .desc(sectionId, index, text, data): - if case .desc(sectionId, index, text, data) = rhs { + case let .desc(_, index, text, data): + if case .desc(_, index, text, data) = rhs { return true } else { return false } - case let .input(sectionId, index, lhsValue, lhsError, identifier, mode, data, placeholder, inputPlaceholder, _, limit): - if case .input(sectionId, index, let rhsValue, let rhsError, identifier, mode, data, placeholder, inputPlaceholder, _, limit) = rhs { + case let .input(_, index, lhsValue, lhsError, identifier, mode, data, placeholder, inputPlaceholder, _, limit): + if case .input(_, index, let rhsValue, let rhsError, identifier, mode, data, placeholder, inputPlaceholder, _, limit) = rhs { return lhsValue == rhsValue && lhsError == rhsError } else { return false } - case let .general(sectionId, index, lhsValue, lhsError, identifier, data): - if case .general(sectionId, index, let rhsValue, let rhsError, identifier, data) = rhs { + case let .general(_, index, lhsValue, lhsError, identifier, data): + if case .general(_, index, let rhsValue, let rhsError, identifier, data) = rhs { return lhsValue == rhsValue && lhsError == rhsError } else { return false } - case let .selector(sectionId, index, lhsValue, lhsError, identifier, placeholder, viewType, lhsValues): - if case .selector(sectionId, index, let rhsValue, let rhsError, identifier, placeholder, viewType, let rhsValues) = rhs { + case let .selector(_, index, lhsValue, lhsError, identifier, placeholder, viewType, lhsValues): + if case .selector(_, index, let rhsValue, let rhsError, identifier, placeholder, viewType, let rhsValues) = rhs { return lhsValues == rhsValues && lhsValue == rhsValue && lhsError == rhsError } else { return false } - case let .dateSelector(sectionId, index, lhsValue, lhsError, identifier, placeholder): - if case .dateSelector(sectionId, index, let rhsValue, let rhsError, identifier, placeholder) = rhs { + case let .dateSelector(_, index, lhsValue, lhsError, identifier, placeholder): + if case .dateSelector(_, index, let rhsValue, let rhsError, identifier, placeholder) = rhs { return lhsValue == rhsValue && lhsError == rhsError } else { return false } - case let .dataSelector(sectionId, index, lhsValue, lhsError, identifier, placeholder, description, lhsIcon, _): - if case .dataSelector(sectionId, index, let rhsValue, let rhsError, identifier, placeholder, description, let rhsIcon, _) = rhs { + case let .dataSelector(_, index, lhsValue, lhsError, identifier, placeholder, description, lhsIcon, _): + if case .dataSelector(_, index, let rhsValue, let rhsError, identifier, placeholder, description, let rhsIcon, _) = rhs { return lhsValue == rhsValue && lhsError == rhsError && lhsIcon == rhsIcon } else { return false @@ -570,14 +572,14 @@ func ==(lhs: InputDataEntry, rhs: InputDataEntry) -> Bool { } else { return false } - case let .search(sectionId, index, value, identifier, _): - if case .search(sectionId, index, value, identifier, _) = rhs { + case let .search(_, index, value, identifier, _): + if case .search(_, index, value, identifier, _) = rhs { return true } else { return false } - case let .sectionId(id, type): - if case .sectionId(id, type) = rhs { + case let .sectionId(_, type): + if case .sectionId(_, type) = rhs { return true } else { return false diff --git a/Telegram-Mac/LocationModalController.swift b/Telegram-Mac/LocationModalController.swift index 7318542ad..4fa639792 100644 --- a/Telegram-Mac/LocationModalController.swift +++ b/Telegram-Mac/LocationModalController.swift @@ -143,15 +143,16 @@ private final class LocationMapView : View { headerTextView.center() } - fileprivate func updateExpandState(_ state: LocationViewState, loading: Bool, hasVenues: Bool, animated: Bool, toggleExpand:@escaping(LocationViewState)->Void) { + fileprivate func updateExpandState(_ state: LocationViewState, loading: Bool, hasVenues: Bool, animated: Bool, toggleExpand:@escaping(LocationViewState)->Void, destination: SelectLocationDestination) { loadingView.isHidden = !loading && hasVenues expandButton.isHidden = loading || !hasVenues - hasExpand = (loading || hasVenues) + hasExpand = (loading || hasVenues) && destination == .chat self.state = state let duration: Double = 0.3 let timingFunction: CAMediaTimingFunctionName = CAMediaTimingFunctionName.spring + CATransaction.begin() let mapY: CGFloat switch state { @@ -252,11 +253,13 @@ private final class LocationMapView : View { private final class MapItemsArguments { let context: AccountContext + let destination: SelectLocationDestination let sendCurrent:()->Void let sendVenue:(TelegramMediaMap)->Void let searchVenues:(String)->Void - init(context: AccountContext, sendCurrent:@escaping()->Void, sendVenue:@escaping(TelegramMediaMap)->Void, searchVenues: @escaping(String)->Void) { + init(context: AccountContext, destination: SelectLocationDestination, sendCurrent:@escaping()->Void, sendVenue:@escaping(TelegramMediaMap)->Void, searchVenues: @escaping(String)->Void) { self.context = context + self.destination = destination self.sendCurrent = sendCurrent self.sendVenue = sendVenue self.searchVenues = searchVenues @@ -269,9 +272,6 @@ private enum MapItemEntryId : Hashable { case nearby(Int32) case search case searchEmptyId - var hashValue: Int { - return 0 - } } private enum MapItemEntry : TableItemListNodeEntry { @@ -328,7 +328,7 @@ private enum MapItemEntry : TableItemListNodeEntry { arguments.searchVenues(state.request) }), inset: NSEdgeInsets(left: 10,right: 10, top: 10, bottom: 10)) case let .currentLocation(_, state): - return LocationSendCurrentItem(initialSize, stableId: stableId, state: state, action: { + return LocationSendCurrentItem(initialSize, stableId: stableId, state: state, destination: arguments.destination, action: { arguments.sendCurrent() }) case let .searchEmpty(_, loading): @@ -455,7 +455,14 @@ private class MapDelegate : NSObject, MKMapViewDelegate { } } +enum SelectLocationDestination { + case chat + case business +} + class LocationModalController: ModalViewController { + + private let chatInteraction: ChatInteraction private let delegate: MapDelegate = MapDelegate() @@ -463,8 +470,10 @@ class LocationModalController: ModalViewController { private let sendDisposable = MetaDisposable() private let requestDisposable = MetaDisposable() private let statePromise:Promise = Promise() - init(_ chatInteraction: ChatInteraction) { + private let destination: SelectLocationDestination + init(_ chatInteraction: ChatInteraction, destination: SelectLocationDestination = .chat) { self.chatInteraction = chatInteraction + self.destination = destination super.init(frame: NSMakeRect(0, 0, 360, 380)) } @@ -619,7 +628,7 @@ class LocationModalController: ModalViewController { let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) let initialSize = self.atomicSize - let arguments = MapItemsArguments(context: context, sendCurrent: { [weak self] in + let arguments = MapItemsArguments(context: context, destination: self.destination, sendCurrent: { [weak self] in self?.sendLocation() }, sendVenue: { [weak self] venue in self?.sendLocation(venue) @@ -673,7 +682,7 @@ class LocationModalController: ModalViewController { self?.genericView.tableView.clipView.scroll(to: NSMakePoint(0, 0), animated: false) search.set(.single("")) state.set(viewState) - }) + }, destination: self.destination) self.readyOnce() })) diff --git a/Telegram-Mac/LocationPreviewController.swift b/Telegram-Mac/LocationPreviewController.swift index cadccc899..b4b669e6e 100644 --- a/Telegram-Mac/LocationPreviewController.swift +++ b/Telegram-Mac/LocationPreviewController.swift @@ -166,7 +166,6 @@ private final class AnnotationView : MKAnnotationView { } } -@available(macOS 10.13, *) private class MapRowItem: GeneralRowItem { let context: AccountContext let presentation: TelegramPresentationTheme diff --git a/Telegram-Mac/LocationSendCurrentItem.swift b/Telegram-Mac/LocationSendCurrentItem.swift index 615540014..2e12650bc 100644 --- a/Telegram-Mac/LocationSendCurrentItem.swift +++ b/Telegram-Mac/LocationSendCurrentItem.swift @@ -17,8 +17,10 @@ enum LocationSelectCurrentState : Equatable { class LocationSendCurrentItem: GeneralRowItem { fileprivate let statusLayout: TextViewLayout fileprivate let state: LocationSelectCurrentState - init(_ initialSize: NSSize, stableId: AnyHashable, state: LocationSelectCurrentState, action:@escaping()->Void) { + fileprivate let destination: SelectLocationDestination + init(_ initialSize: NSSize, stableId: AnyHashable, state: LocationSelectCurrentState, destination: SelectLocationDestination, action:@escaping()->Void) { self.state = state + self.destination = destination let text: String switch state { case let .accurate(location, _): @@ -87,9 +89,19 @@ private final class LocationSendCurrentView : TableRowView { let text: String switch item.state { case .accurate: - text = strings().locationSendMyLocation + switch item.destination { + case .chat: + text = strings().locationSendMyLocation + case .business: + text = strings().locationSetLocation + } case .selected: - text = strings().locationSendThisLocation + switch item.destination { + case .chat: + text = strings().locationSendThisLocation + case .business: + text = strings().locationSetLocation + } } button.set(text: text, for: .Normal) _ = button.sizeToFit() @@ -97,7 +109,7 @@ private final class LocationSendCurrentView : TableRowView { statusView.update(item.statusLayout) iconView.image = theme.icons.locationPin - _ = iconView.sizeToFit() + iconView.sizeToFit() } override func draw(_ layer: CALayer, in ctx: CGContext) { diff --git a/Telegram-Mac/PeersListController.swift b/Telegram-Mac/PeersListController.swift index 78bbadb51..31cfb4184 100644 --- a/Telegram-Mac/PeersListController.swift +++ b/Telegram-Mac/PeersListController.swift @@ -1945,7 +1945,7 @@ class PeersListController: TelegramGenericViewController, } switch state.state { case .Focus: - assert(self?.searchController == nil) + //assert(self?.searchController == nil) self?.showSearchController(animated: animated) case .None: diff --git a/Telegram-Mac/StoryChatContent.swift b/Telegram-Mac/StoryChatContent.swift index 5c16eb7a1..21226fd09 100644 --- a/Telegram-Mac/StoryChatContent.swift +++ b/Telegram-Mac/StoryChatContent.swift @@ -107,13 +107,20 @@ final class StoryContentContextState { let canViewStats: Bool let premiumRequired: Bool let preferHighQualityStories: Bool + let slowModeTimeout: Int32? + let slowModeValidUntilTimestamp: Int32? + let canAvoidRestrictions: Bool + init( isMuted: Bool, areVoiceMessagesAvailable: Bool, presence: EnginePeer.Presence?, canViewStats: Bool, premiumRequired: Bool, - preferHighQualityStories: Bool + preferHighQualityStories: Bool, + slowModeTimeout: Int32?, + slowModeValidUntilTimestamp: Int32?, + canAvoidRestrictions: Bool ) { self.isMuted = isMuted self.areVoiceMessagesAvailable = areVoiceMessagesAvailable @@ -121,6 +128,9 @@ final class StoryContentContextState { self.canViewStats = canViewStats self.premiumRequired = premiumRequired self.preferHighQualityStories = preferHighQualityStories + self.slowModeTimeout = slowModeTimeout + self.slowModeValidUntilTimestamp = slowModeValidUntilTimestamp + self.canAvoidRestrictions = canAvoidRestrictions } } @@ -311,6 +321,11 @@ final class StoryContentContextImpl: StoryContentContext { forwardInfoStories.updateValue(nil, forKey: storyId) } } + if let peerId = itemValue.authorId { + if let peer = transaction.getPeer(peerId) { + peers[peer.id] = peer + } + } for entity in itemValue.entities { if case let .CustomEmoji(_, fileId) = entity.type { let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) @@ -407,16 +422,24 @@ final class StoryContentContextImpl: StoryContentContext { presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: false, premiumRequired: cachedUserData.flags.contains(.premiumRequired), - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + slowModeTimeout: nil, + slowModeValidUntilTimestamp: nil, + canAvoidRestrictions: true ) } else if let cachedChannelData = cachedPeerDataView.cachedPeerData as? CachedChannelData { + let boostsToUnrestrict = cachedChannelData.boostsToUnrestrict + let appliedBoosts = cachedChannelData.appliedBoosts ?? 0 additionalPeerData = StoryContentContextState.AdditionalPeerData( isMuted: true, areVoiceMessagesAvailable: true, presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: cachedChannelData.flags.contains(.canViewStats), premiumRequired: false, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + slowModeTimeout: cachedChannelData.slowModeTimeout, + slowModeValidUntilTimestamp: cachedChannelData.slowModeValidUntilTimestamp, + canAvoidRestrictions: boostsToUnrestrict != nil ? boostsToUnrestrict! <= appliedBoosts : false ) } else { additionalPeerData = StoryContentContextState.AdditionalPeerData( @@ -425,7 +448,10 @@ final class StoryContentContextImpl: StoryContentContext { presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: false, premiumRequired: false, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + slowModeTimeout: nil, + slowModeValidUntilTimestamp: nil, + canAvoidRestrictions: true ) } } @@ -436,7 +462,10 @@ final class StoryContentContextImpl: StoryContentContext { presence: peerPresence.flatMap { EnginePeer.Presence($0) }, canViewStats: false, premiumRequired: false, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + slowModeTimeout: nil, + slowModeValidUntilTimestamp: nil, + canAvoidRestrictions: true ) } let state = stateView.value?.get(Stories.PeerState.self) @@ -492,7 +521,8 @@ final class StoryContentContextImpl: StoryContentContext { isEdited: item.isEdited, isMy: item.isMy, myReaction: item.myReaction, - forwardInfo: forwardInfo + forwardInfo: forwardInfo, + author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } ) } var totalCount = peerStoryItemsView.items.count @@ -528,7 +558,8 @@ final class StoryContentContextImpl: StoryContentContext { isEdited: false, isMy: true, myReaction: nil, - forwardInfo: pendingForwardsInfo[item.randomId] + forwardInfo: pendingForwardsInfo[item.randomId], + author: nil )) totalCount += 1 } @@ -1342,7 +1373,10 @@ final class SingleStoryContentContextImpl: StoryContentContext { TelegramEngine.EngineData.Item.Peer.CanViewStats(id: storyId.peerId), TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: storyId.peerId), TelegramEngine.EngineData.Item.NotificationSettings.Global(), - TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: storyId.peerId) + TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: storyId.peerId), + TelegramEngine.EngineData.Item.Peer.SlowmodeTimeout(id: storyId.peerId), + TelegramEngine.EngineData.Item.Peer.SlowmodeValidUntilTimeout(id: storyId.peerId), + TelegramEngine.EngineData.Item.Peer.CanAvoidGroupRestrictions(id: storyId.peerId) ), item |> mapToSignal { item -> Signal<(Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?]), NoError> in return context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer], [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?]) in @@ -1371,6 +1405,11 @@ final class SingleStoryContentContextImpl: StoryContentContext { stories.updateValue(nil, forKey: storyId) } } + if let peerId = item.authorId { + if let peer = transaction.getPeer(peerId) { + peers[peer.id] = peer + } + } for entity in item.entities { if case let .CustomEmoji(_, fileId) = entity.type { let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) @@ -1407,7 +1446,7 @@ final class SingleStoryContentContextImpl: StoryContentContext { return } - let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, premiumRequired) = data + let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, premiumRequired, slowmodeTimeout, slowmodeValidUntilTimeout, canAvoidGroupRestrictions) = data let (item, peers, allEntityFiles, forwardInfoStories) = itemAndPeers guard let peer = peer else { @@ -1422,7 +1461,10 @@ final class SingleStoryContentContextImpl: StoryContentContext { presence: presence, canViewStats: canViewStats, premiumRequired: false, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + slowModeTimeout: slowmodeTimeout, + slowModeValidUntilTimestamp: slowmodeValidUntilTimeout, + canAvoidRestrictions: canAvoidGroupRestrictions ) for (storyId, story) in forwardInfoStories { @@ -1496,7 +1538,8 @@ final class SingleStoryContentContextImpl: StoryContentContext { isEdited: itemValue.isEdited, isMy: itemValue.isMy, myReaction: itemValue.myReaction, - forwardInfo: itemValue.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) } + forwardInfo: itemValue.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, + author: itemValue.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) } ) let mainItem = StoryContentItem( @@ -1618,7 +1661,10 @@ final class PeerStoryListContentContextImpl: StoryContentContext { TelegramEngine.EngineData.Item.Peer.CanViewStats(id: peerId), TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId), TelegramEngine.EngineData.Item.NotificationSettings.Global(), - TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: peerId) + TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: peerId), + TelegramEngine.EngineData.Item.Peer.SlowmodeTimeout(id: peerId), + TelegramEngine.EngineData.Item.Peer.SlowmodeValidUntilTimeout(id: peerId), + TelegramEngine.EngineData.Item.Peer.CanAvoidGroupRestrictions(id: peerId) ), listContext.state, self.focusedIdUpdated.get(), @@ -1629,7 +1675,7 @@ final class PeerStoryListContentContextImpl: StoryContentContext { return } - let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, premiumRequired) = data + let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, premiumRequired, slowmodeTimeout, slowmodeValidUntilTimeout, canAvoidGroupRestrictions) = data guard let peer = peer else { return @@ -1643,7 +1689,10 @@ final class PeerStoryListContentContextImpl: StoryContentContext { presence: presence, canViewStats: canViewStats, premiumRequired: premiumRequired, - preferHighQualityStories: preferHighQualityStories + preferHighQualityStories: preferHighQualityStories, + slowModeTimeout: slowmodeTimeout, + slowModeValidUntilTimestamp: slowmodeValidUntilTimeout, + canAvoidRestrictions: canAvoidGroupRestrictions ) self.listState = state @@ -2372,7 +2421,8 @@ private func getCachedStory(storyId: StoryId, transaction: Transaction) -> Engin isEdited: item.isEdited, isMy: item.isMy, myReaction: item.myReaction, - forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) } + forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) } ) } else { return nil diff --git a/Telegram-Mac/StoryControlsView.swift b/Telegram-Mac/StoryControlsView.swift index ca45beb6d..fa5203cb8 100644 --- a/Telegram-Mac/StoryControlsView.swift +++ b/Telegram-Mac/StoryControlsView.swift @@ -85,6 +85,56 @@ private final class RepostView : View { } } +private final class AuthorView : View { + private let avatarView: AvatarControl = AvatarControl(font: .avatar(4)) + private let nameView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + avatarView.setFrameSize(NSMakeSize(12, 12)) + addSubview(avatarView) + addSubview(nameView) + + avatarView.userInteractionEnabled = false + nameView.userInteractionEnabled = false + nameView.isSelectable = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func set(peer: EnginePeer, context: AccountContext, animated: Bool) { + avatarView.setPeer(account: context.account, peer: peer._asPeer()) + + + let layout = TextViewLayout(.initialize(string: peer._asPeer().compactDisplayTitle, color: NSColor.white.withAlphaComponent(0.8), font: .medium(.small))) + layout.measure(width: 100) + + self.nameView.update(layout) + + let size = NSMakeSize(avatarView.frame.width + 3 + self.nameView.frame.width, 16) + + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.2, curve: .easeOut) + } else { + transition = .immediate + } + self.setFrameSize(size) + self.updateLayout(size: size, transition: transition) + } + + override func layout() { + super.layout() + self.updateLayout(size: self.frame.size, transition: .immediate) + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(view: avatarView, frame: avatarView.centerFrameY(x: 0)) + transition.updateFrame(view: nameView, frame: self.nameView.centerFrameY(x: avatarView.frame.maxX + 3)) + } +} + final class StoryControlsView : Control { private let avatar = AvatarControl(font: .avatar(13)) private let textView = TextView() @@ -104,6 +154,7 @@ final class StoryControlsView : Control { private let shadowView = ShadowView() private var repostView: RepostView? + private var authorView: AuthorView? required init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(shadowView) @@ -249,6 +300,7 @@ final class StoryControlsView : Control { current.centerY(x: 0) } if let peerId = peerId { + dateContainer.removeAllHandlers() dateContainer.set(handler: { [weak arguments] _ in arguments?.openChat(peerId, nil, nil) }, for: .Click) @@ -259,6 +311,35 @@ final class StoryControlsView : Control { performSubviewRemoval(view, animated: animated) self.repostView = nil } + + if let author = story.storyItem.author { + + date.append(string: " \(strings().bullet) ", color: color, font: .medium(.small)) + + let current: AuthorView + var isNew = false + if let view = self.authorView { + current = view + } else { + current = AuthorView(frame: .zero) + self.authorView = current + dateContainer.addSubview(current) + isNew = true + } + current.set(peer: author, context: context, animated: animated) + if isNew { + current.centerY(x: 0) + } + dateContainer.removeAllHandlers() + dateContainer.set(handler: { [weak arguments] _ in + arguments?.openChat(author.id, nil, nil) + }, for: .Click) + + } else if let view = authorView { + performSubviewRemoval(view, animated: animated) + self.authorView = nil + } + if story.storyItem.expirationTimestamp < context.timestamp { date.append(string: stringForFullDate(timestamp: story.storyItem.timestamp), color: color, font: .medium(.small)) } else { @@ -278,14 +359,14 @@ final class StoryControlsView : Control { more.isHidden = context.peerId == groupId - let textWidth = frame.width - 24 - avatar.frame.width - 20 - (muted.isHidden ? 0 : 20) - (more.isHidden ? 0 : 20) - (privacy.isHidden ? 0 : 20) - (repostView != nil ? repostView!.frame.width + 3 : 0) + let textWidth = frame.width - 24 - avatar.frame.width - 20 - (muted.isHidden ? 0 : 20) - (more.isHidden ? 0 : 20) - (privacy.isHidden ? 0 : 20) - (repostView != nil ? repostView!.frame.width + 3 : 0) - (authorView != nil ? authorView!.frame.width + 3 : 0) let dateLayout = TextViewLayout(date, maximumNumberOfLines: 1) dateLayout.measure(width: textWidth) - dateContainer.userInteractionEnabled = self.repostView != nil + dateContainer.userInteractionEnabled = self.repostView != nil || self.authorView != nil dateContainer.scaleOnClick = true dateView.userInteractionEnabled = false @@ -339,11 +420,19 @@ final class StoryControlsView : Control { repostView.updateLayout(size: repostView.frame.size, transition: transition) } + if let authorView = self.authorView { + dateContainerSize.width += authorView.frame.width + 3 + transition.updateFrame(view: authorView, frame: authorView.centerFrameY(x: 0)) + authorView.updateLayout(size: authorView.frame.size, transition: transition) + } + transition.updateFrame(view: dateContainer, frame: CGRect(origin: NSMakePoint(avatar.frame.maxX + 10, avatar.frame.maxY - dateView.frame.height), size: dateContainerSize)) if let view = repostView { transition.updateFrame(view: dateView, frame: dateView.centerFrameY(x: view.frame.maxX + 3)) + } else if let view = authorView { + transition.updateFrame(view: dateView, frame: dateView.centerFrameY(x: view.frame.maxX + 3)) } else { transition.updateFrame(view: dateView, frame: dateView.centerFrameY(x: 0)) } diff --git a/Telegram-Mac/StoryInputView.swift b/Telegram-Mac/StoryInputView.swift index 2b44f0109..1d0d7fc9a 100644 --- a/Telegram-Mac/StoryInputView.swift +++ b/Telegram-Mac/StoryInputView.swift @@ -411,7 +411,10 @@ final class StoryInputView : Control, StoryInput { return } let text: String - if let cooldown = arguments.interaction.presentation.stealthMode.activeUntilTimestamp { + if let slowmode = arguments.interaction.presentation.slowMode, let timeout = slowmode.timeout { + let timer = smartTimeleftText(Int(timeout)) + text = strings().storySlowModePlaceholder(timer) + } else if let cooldown = arguments.interaction.presentation.stealthMode.activeUntilTimestamp { stealthDisposable.set(delaySignal(0.3).start(completed: { [weak self] in self?.updatePlaceholder() })) @@ -420,7 +423,11 @@ final class StoryInputView : Control, StoryInput { text = strings().storyStealthModePlaceholder(timer) } else { stealthDisposable.set(nil) - text = strings().storyInputPlaceholder + if arguments.interaction.presentation.entryId?.namespace == Namespaces.Peer.CloudChannel { + text = strings().storyInputGroupPlaceholder + } else { + text = strings().storyInputPlaceholder + } } textView.placeholder = text } diff --git a/Telegram-Mac/StoryListView.swift b/Telegram-Mac/StoryListView.swift index f10c04502..c65238770 100644 --- a/Telegram-Mac/StoryListView.swift +++ b/Telegram-Mac/StoryListView.swift @@ -1617,7 +1617,11 @@ final class StoryListView : Control, Notifable { let aspect = StoryLayoutView.size.aspectFitted(maxSize) if entry.peer._asPeer() is TelegramChannel { - self.inputView = StoryChannelInputView(frame: NSMakeRect(0, 0, aspect.width, 50)) + if entry.peer._asPeer().isSupergroup { + self.inputView = StoryInputView(frame: NSMakeRect(0, 0, aspect.width, 50)) + } else { + self.inputView = StoryChannelInputView(frame: NSMakeRect(0, 0, aspect.width, 50)) + } } else if entry.peer.isService { self.inputView = StoryNoReplyInput(frame: NSMakeRect(0, 0, aspect.width, 50)) } else if entry.additionalPeerData.premiumRequired && !arguments.context.isPremium { diff --git a/Telegram-Mac/StoryModalController.swift b/Telegram-Mac/StoryModalController.swift index 56f773033..96221f725 100644 --- a/Telegram-Mac/StoryModalController.swift +++ b/Telegram-Mac/StoryModalController.swift @@ -156,7 +156,8 @@ final class StoryInteraction : InterfaceObserver { var closed: Bool = false var wideInput: Bool { - return (inputInFocus || hasPopover) && self.entryId?.namespace == Namespaces.Peer.CloudUser + let accept = self.entryId?.namespace == Namespaces.Peer.CloudUser || self.entryId?.namespace == Namespaces.Peer.CloudChannel + return (inputInFocus || hasPopover) && accept } var isAreaActivated: Bool = false @@ -168,6 +169,7 @@ final class StoryInteraction : InterfaceObserver { var inputRecording: ChatRecordingState? var recordType: RecordingStateSettings = FastSettings.recordingState var stealthMode: Stories.StealthModeState = .init(activeUntilTimestamp: nil, cooldownUntilTimestamp: nil) + var slowMode: SlowMode? var canViewStats: Bool = false var reactions: AvailableReactions? var isPaused: Bool { @@ -1963,7 +1965,7 @@ private final class StoryViewController: Control, Notifable { if let peerId = current.id, let story = current.story { if peerId == arguments?.context.peerId { arguments?.showViewers(story) - } else if let peer = self.storyContext?.stateValue?.slice?.peer._asPeer() as? TelegramChannel { + } else if self.storyContext?.stateValue?.slice?.peer._asPeer() is TelegramChannel { arguments?.showViewers(story) } NSHapticFeedbackManager.defaultPerformer.perform(.levelChange, performanceTime: .default) @@ -2041,6 +2043,7 @@ final class StoryModalController : ModalViewController, Notifable { private let actionsDisposable = DisposableSet() private let updatesDisposable = MetaDisposable() private let inputSwapDisposable = MetaDisposable() + private let slowModeDisposable = MetaDisposable() private var overlayTimer: SwiftSignalKit.Timer? private var arguments: StoryArguments? @@ -2124,6 +2127,40 @@ final class StoryModalController : ModalViewController, Notifable { } } } + + if let until = value.slowMode?.validUntil, until > self.context.timestamp { + let signal = Signal.single(Void()) |> then(.single(Void()) |> delay(0.2, queue: .mainQueue()) |> restart) + slowModeDisposable.set(signal.start(next: { [weak self] in + if let `self` = self { + if until < self.context.timestamp { + self.arguments?.interaction.update { current in + var current = current + current.slowMode = value.slowMode?.withUpdatedTimeout(nil) + return current + } + } else { + self.arguments?.interaction.update { current in + var current = current + current.slowMode = value.slowMode?.withUpdatedTimeout(until - self.context.timestamp) + return current + } + } + } + })) + + } else { + self.slowModeDisposable.set(nil) + if let slowMode = value.slowMode, slowMode.timeout != nil { + DispatchQueue.main.async { [weak self] in + self?.arguments?.interaction.update { current in + var current = current + current.slowMode = value.slowMode?.withUpdatedTimeout(nil) + return current + } + } + } + } + } if let value = value as? StoryInteraction.State, let oldValue = oldValue as? StoryInteraction.State { self.chatInteraction.update({ @@ -2391,9 +2428,26 @@ final class StoryModalController : ModalViewController, Notifable { }) } + let canAvoidGroupRestrictions:()->Bool = { [weak self] in + return self?.genericView.storyContext?.stateValue?.slice?.additionalPeerData.canAvoidRestrictions == true + } + let sendText: (ChatTextInputState, PeerId, Int32, StoryViewController.TooptipView.Source)->Void = { [weak self] input, peerId, id, source in + + if let timeout = self?.arguments?.interaction.presentation.slowMode?.timeout { + self?.genericView.showTooltip(.tooltip(slowModeTooltipText(timeout), MenuAnimation.menu_clear_history)) + return + } + + let restrictionText = permissionText(from: self?.genericView.storyContext?.stateValue?.slice?.peer._asPeer(), for: .banSendText) + + if let restrictionText, !canAvoidGroupRestrictions() { + self?.genericView.showTooltip(.tooltip(restrictionText, MenuAnimation.menu_clear_history)) + return + } + beforeCompletion() _ = Sender.enqueue(input: input, context: context, peerId: peerId, replyId: nil, threadId: nil, replyStoryId: .init(peerId: peerId, id: id), sendAsPeerId: nil).start(completed: { afterCompletion() @@ -2836,6 +2890,8 @@ final class StoryModalController : ModalViewController, Notifable { StoryModalController.ShowSingleStory(context: context, storyId: storyId, initialId: nil) }) + + self.arguments = arguments genericView.setArguments(arguments) @@ -2847,6 +2903,19 @@ final class StoryModalController : ModalViewController, Notifable { guard let interactions = self?.interactions else { return } + + if let timeout = self?.arguments?.interaction.presentation.slowMode?.timeout { + self?.genericView.showTooltip(.tooltip(slowModeTooltipText(timeout), MenuAnimation.menu_clear_history)) + return + } + + let restrictionText = permissionText(from: self?.genericView.storyContext?.stateValue?.slice?.peer._asPeer(), for: .banSendFiles) + + if let restrictionText, !canAvoidGroupRestrictions() { + self?.genericView.showTooltip(.tooltip(restrictionText, MenuAnimation.menu_clear_history)) + return + } + if let peerId = interactions.presentation.entryId, let id = interactions.presentation.storyId { beforeCompletion() _ = Sender.enqueue(media: file, context: context, peerId: peerId, replyId: nil, threadId: nil, replyStoryId: .init(peerId: peerId, id: id)).start(completed: { @@ -2891,6 +2960,19 @@ final class StoryModalController : ModalViewController, Notifable { guard let interactions = self?.interactions else { return } + + if let timeout = self?.arguments?.interaction.presentation.slowMode?.timeout { + self?.genericView.showTooltip(.tooltip(slowModeTooltipText(timeout), MenuAnimation.menu_clear_history)) + return + } + + let restrictionText = permissionText(from: self?.genericView.storyContext?.stateValue?.slice?.peer._asPeer(), for: .banSendMedia) + + if let restrictionText, !canAvoidGroupRestrictions() { + self?.genericView.showTooltip(.tooltip(restrictionText, MenuAnimation.menu_clear_history)) + return + } + if let peerId = interactions.presentation.entryId, let id = interactions.presentation.storyId { beforeCompletion() _ = Sender.enqueue(media: medias, caption: caption, context: context, peerId: peerId, replyId: nil, threadId: nil, replyStoryId: .init(peerId: peerId, id: id), isCollage: isCollage, additionText: additionText, silent: silent, atDate: atDate, isSpoiler: isSpoiler).start(completed: { @@ -2901,6 +2983,19 @@ final class StoryModalController : ModalViewController, Notifable { } chatInteraction.sendMedia = { [weak self] container in + + if let timeout = self?.arguments?.interaction.presentation.slowMode?.timeout { + self?.genericView.showTooltip(.tooltip(slowModeTooltipText(timeout), MenuAnimation.menu_clear_history)) + return + } + + let restrictionText = permissionText(from: self?.genericView.storyContext?.stateValue?.slice?.peer._asPeer(), for: .banSendMedia) + + if let restrictionText, !canAvoidGroupRestrictions() { + self?.genericView.showTooltip(.tooltip(restrictionText, MenuAnimation.menu_clear_history)) + return + } + if let peerId = interactions.presentation.entryId, let id = interactions.presentation.storyId { beforeCompletion() _ = Sender.enqueue(media: container, context: context, peerId: peerId, replyId: nil, threadId: nil, replyStoryId: .init(peerId: peerId, id: id)).start(completed: { @@ -2989,6 +3084,9 @@ final class StoryModalController : ModalViewController, Notifable { }) })) + + + actionsDisposable.add(context.reactions.stateValue.start(next: { [weak self] reactions in self?.interactions.update({ current in var current = current @@ -3007,6 +3105,36 @@ final class StoryModalController : ModalViewController, Notifable { if ready { self?.readyOnce() } + if let chatInteraction = self?.chatInteraction { + chatInteraction.update({ current in + var current = current + current = current.updatedPeer { _ in + return state.slice?.peer._asPeer() + } + + if let slowmode = state.slice?.additionalPeerData.slowModeTimeout, state.slice?.additionalPeerData.canAvoidRestrictions == false { + let slowmodeValidUntilTimeout = state.slice?.additionalPeerData.slowModeValidUntilTimestamp + current = current.updateSlowMode({ value in + var value = value ?? SlowMode() + value = value.withUpdatedValidUntil(slowmodeValidUntilTimeout) + if let timeout = slowmodeValidUntilTimeout { + if timeout > context.timestamp { + value = value.withUpdatedTimeout(timeout - context.timestamp) + } else { + value = value.withUpdatedTimeout(nil) + } + } + return value + }) + } else { + current = current.updateSlowMode({ _ in + nil + }) + } + return current + }) + self?.entertainment.update(with: chatInteraction) + } } })) @@ -3316,6 +3444,7 @@ final class StoryModalController : ModalViewController, Notifable { updatesDisposable.dispose() inputSwapDisposable.dispose() actionsDisposable.dispose() + slowModeDisposable.dispose() } override var containerBackground: NSColor { diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index 920c25ec55263897a51a337682f7e51be940836f..714a407f068d14b00d178538ced9b4a040591bc4 100644 GIT binary patch delta 177 zcmX?c(dgG@qlOm77N!>FEiBhOrf>0Nkzg#D9M~i~z2P$#$Mh+lEDF;vykTUU9&?mS zVEO`Y7M}KR9xOo23dC$c%nrmHK+L)Qn+Mm4NKSW#B8Gg1Qig)*hUtvT(;dDrvrMnr z&&I{Cz!1z(0+cFZsASM%(3`%ojZ1R+orPRH+xIQvdLcADU=u6Lbi1`&9E^I?A1-GV aogTN9OJRDzIu^F}uB}|#yS8%MeFgxD_(8$| delta 101 zcmex$+33VYqlOm77N!>FEiBhOrgt6V5}D@1!qXn+$pXZzK+Fcj>_E%`#GKp1Jh@Ip zZtq#l^+IU+flzj~X||j^)AP1-*-ify!Ywj=Ul1GHbPr2To_4?OT-*J&bFcXX0G661 AqyPW_ diff --git a/Telegram.xcodeproj/project.pbxproj b/Telegram.xcodeproj/project.pbxproj index 751a1486a..770f73ace 100644 --- a/Telegram.xcodeproj/project.pbxproj +++ b/Telegram.xcodeproj/project.pbxproj @@ -462,6 +462,7 @@ 27A697AE273D3FE900B33415 /* Spotlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A697AD273D3FE900B33415 /* Spotlight.swift */; }; 27A710B728B8C8F100E65838 /* custom_reaction.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27A710B628B8C8F100E65838 /* custom_reaction.tgs */; }; 27A710B928B9009500E65838 /* QuickReactionRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A710B828B9009500E65838 /* QuickReactionRowItem.swift */; }; + 27A734492B7A7BB600AB4FD5 /* BusinessHoursController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A734482B7A7BB600AB4FD5 /* BusinessHoursController.swift */; }; 27A7D37D2600F58A00A67737 /* voip_group_unmuted.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 27A7D37B2600F58900A67737 /* voip_group_unmuted.mp3 */; }; 27A7D37E2600F58A00A67737 /* voip_group_recording_started.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 27A7D37C2600F58A00A67737 /* voip_group_recording_started.mp3 */; }; 27A9904E257A8047009044DB /* Pop.wav in Resources */ = {isa = PBXBuildFile; fileRef = 27A9904C257A8044009044DB /* Pop.wav */; }; @@ -688,6 +689,9 @@ 27E070F2278D9A970026BFA1 /* menu_camera.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27E070F1278D9A970026BFA1 /* menu_camera.tgs */; }; 27E070F4278D9C0A0026BFA1 /* menu_location.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27E070F3278D9C0A0026BFA1 /* menu_location.tgs */; }; 27E070F6278D9C120026BFA1 /* menu_poll.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27E070F5278D9C120026BFA1 /* menu_poll.tgs */; }; + 27E1F2422B7AD47F00DCF241 /* BusinessAwayMessageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E1F2412B7AD47F00DCF241 /* BusinessAwayMessageController.swift */; }; + 27E1F2442B7B007B00DCF241 /* BusinessLocationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E1F2432B7B007B00DCF241 /* BusinessLocationController.swift */; }; + 27E1F2462B7B876800DCF241 /* BusinessChatbotController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E1F2452B7B876800DCF241 /* BusinessChatbotController.swift */; }; 27E5C3BD2739761300C52BA0 /* MurMurHash32 in Frameworks */ = {isa = PBXBuildFile; productRef = 27E5C3BC2739761300C52BA0 /* MurMurHash32 */; }; 27E60C1129ED35E200E10E39 /* menu_online.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27E60C1029ED35E200E10E39 /* menu_online.tgs */; }; 27E6DB0A2689C71E003D6164 /* MetalFunctions.metal in Sources */ = {isa = PBXBuildFile; fileRef = 27E6DB092689C71E003D6164 /* MetalFunctions.metal */; }; @@ -1926,6 +1930,7 @@ 27A697AD273D3FE900B33415 /* Spotlight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Spotlight.swift; sourceTree = ""; }; 27A710B628B8C8F100E65838 /* custom_reaction.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = custom_reaction.tgs; sourceTree = ""; }; 27A710B828B9009500E65838 /* QuickReactionRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickReactionRowItem.swift; sourceTree = ""; }; + 27A734482B7A7BB600AB4FD5 /* BusinessHoursController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusinessHoursController.swift; sourceTree = ""; }; 27A7D37B2600F58900A67737 /* voip_group_unmuted.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = voip_group_unmuted.mp3; sourceTree = ""; }; 27A7D37C2600F58A00A67737 /* voip_group_recording_started.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = voip_group_recording_started.mp3; sourceTree = ""; }; 27A99049257A7435009044DB /* SoundEffectPlayQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundEffectPlayQueue.swift; sourceTree = ""; }; @@ -2133,6 +2138,9 @@ 27E070F1278D9A970026BFA1 /* menu_camera.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = menu_camera.tgs; sourceTree = ""; }; 27E070F3278D9C0A0026BFA1 /* menu_location.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = menu_location.tgs; sourceTree = ""; }; 27E070F5278D9C120026BFA1 /* menu_poll.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = menu_poll.tgs; sourceTree = ""; }; + 27E1F2412B7AD47F00DCF241 /* BusinessAwayMessageController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusinessAwayMessageController.swift; sourceTree = ""; }; + 27E1F2432B7B007B00DCF241 /* BusinessLocationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusinessLocationController.swift; sourceTree = ""; }; + 27E1F2452B7B876800DCF241 /* BusinessChatbotController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusinessChatbotController.swift; sourceTree = ""; }; 27E59979261B3F5800228411 /* CurrencyFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyFormatter.swift; sourceTree = ""; }; 27E5997C261B3FA800228411 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 27E5997F261B404000228411 /* CurrencyLocale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyLocale.swift; sourceTree = ""; }; @@ -3480,6 +3488,17 @@ name = profile; sourceTree = ""; }; + 27A734472B7A7B9800AB4FD5 /* business */ = { + isa = PBXGroup; + children = ( + 27A734482B7A7BB600AB4FD5 /* BusinessHoursController.swift */, + 27E1F2412B7AD47F00DCF241 /* BusinessAwayMessageController.swift */, + 27E1F2432B7B007B00DCF241 /* BusinessLocationController.swift */, + 27E1F2452B7B876800DCF241 /* BusinessChatbotController.swift */, + ); + name = business; + sourceTree = ""; + }; 27AAE00F28E2DADE004DA38D /* forum */ = { isa = PBXGroup; children = ( @@ -4536,6 +4555,7 @@ C2271DD01DAF6DD9001792B6 /* settings-controllers */ = { isa = PBXGroup; children = ( + 27A734472B7A7B9800AB4FD5 /* business */, 277E228B28EEC4B000001689 /* username */, 27D29AA327F306A300D4BC64 /* notifications */, 2728991A26CBCD8F00F4D288 /* items */, @@ -6840,6 +6860,7 @@ D032AFA42578172400E67215 /* PushToTalk.swift in Sources */, 27A6975E273D086400B33415 /* PushToTalkRowItem.swift in Sources */, 27EBC7B0275795DE00C6A820 /* ChatReactionsView.swift in Sources */, + 27E1F2462B7B876800DCF241 /* BusinessChatbotController.swift in Sources */, 27A6975D273D085F00B33415 /* GroupCallSettingsController.swift in Sources */, 272C7DF02AA07EDD00F89190 /* StoryLayoutView.swift in Sources */, 27A6975C273D085A00B33415 /* GroupCallController.swift in Sources */, @@ -6995,6 +7016,7 @@ C218FF9C1F4204C400DD7D35 /* InstantPageChannelView.swift in Sources */, 27EFF4BF27BBF2D600006834 /* Auth_NextView.swift in Sources */, 277C209027A2DA060070B3BF /* SliderContextMenuItem.swift in Sources */, + 27E1F2442B7B007B00DCF241 /* BusinessLocationController.swift in Sources */, 277E228028E4C27600001689 /* ForwardChatListController.swift in Sources */, D07C6D7D234698C600468B1A /* DynamicHeightRowItem.swift in Sources */, A73884352580E501002E8424 /* CGChatListIndicator.swift in Sources */, @@ -7349,6 +7371,7 @@ C224675D1FA884E300F03E27 /* ChatGroupedItem.swift in Sources */, 27D6044B295732BC00588C21 /* StorageUsageClearedItem.swift in Sources */, C2271DD71DAF80D5001792B6 /* PeerMediaController.swift in Sources */, + 27E1F2422B7AD47F00DCF241 /* BusinessAwayMessageController.swift in Sources */, 2764A5AA25DFB5B300F9A20D /* ReportDetailsController.swift in Sources */, A7F2830823954EF800742C20 /* CachedInstantPages.swift in Sources */, 277DFE6F27848AAB00F78396 /* ContextPeerMenuItem.swift in Sources */, @@ -7426,6 +7449,7 @@ 279A1E2825E945A2007D48E7 /* PaymentsReceiptController.swift in Sources */, 270340602823D45300D0156C /* PremiumLimitController.swift in Sources */, 276553602B68BB7B0007F2FB /* GroupEmojiPackController.swift in Sources */, + 27A734492B7A7BB600AB4FD5 /* BusinessHoursController.swift in Sources */, 271FB884289014CE001C25DC /* CustomEmojiController.swift in Sources */, C2271F591D9D46CA00424F7B /* ShortPeerRowView.swift in Sources */, D0530D4224E69459003273BC /* CallTooltipView.swift in Sources */, diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 581af7191..19d50efee 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259532 + 259648 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/InAppSettings/Sources/InAppSettings/ChatListFilterPreferences.swift b/packages/InAppSettings/Sources/InAppSettings/ChatListFilterPreferences.swift index 82eb4b7c3..a1e2c3459 100644 --- a/packages/InAppSettings/Sources/InAppSettings/ChatListFilterPreferences.swift +++ b/packages/InAppSettings/Sources/InAppSettings/ChatListFilterPreferences.swift @@ -42,7 +42,7 @@ public extension ChatListFilter { id = tempId } } - return .filter(id: id, title: "", emoticon: nil, data: ChatListFilterData(isShared: false, hasSharedLinks: false, categories: [], excludeMuted: false, excludeRead: false, excludeArchived: false, includePeers: ChatListFilterIncludePeers(), excludePeers: [])) + return .filter(id: id, title: "", emoticon: nil, data: ChatListFilterData(isShared: false, hasSharedLinks: false, categories: [], excludeMuted: false, excludeRead: false, excludeArchived: false, includePeers: ChatListFilterIncludePeers(), excludePeers: [], color: nil)) } } diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index 74b434e77..f3c0371df 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -8715,6 +8715,8 @@ public final class L10n { public static var locationSendTitle: String { return L10n.tr("Localizable", "Location.Send.Title") } /// Unknown Location public static var locationSendThisLocationUnknown: String { return L10n.tr("Localizable", "Location.Send.ThisLocation.Unknown") } + /// Set This Location + public static var locationSetLocation: String { return L10n.tr("Localizable", "Location.Set.Location") } /// %@ away public static func locationPreviewDistanceAway(_ p1: String) -> String { return L10n.tr("Localizable", "LocationPreview.DistanceAway", p1) @@ -14465,6 +14467,8 @@ public final class L10n { public static var storyInputFile: String { return L10n.tr("Localizable", "Story.Input.File") } /// Reply Privately... public static var storyInputPlaceholder: String { return L10n.tr("Localizable", "Story.Input.Placeholder") } + /// Comment Story... + public static var storyInputGroupPlaceholder: String { return L10n.tr("Localizable", "Story.Input.Group.Placeholder") } /// This story is not supported by your version of Telegram. Please update the app to the latest version. public static var storyMediaUnsupported: String { return L10n.tr("Localizable", "Story.Media.Unsupported") } /// Copy Link @@ -14539,6 +14543,10 @@ public final class L10n { public static var storyNoReplyInputNoReply: String { return L10n.tr("Localizable", "Story.NoReplyInput.NoReply") } /// Telegram moderators will review your report. Thank you! public static var storyReportSuccessText: String { return L10n.tr("Localizable", "Story.Report.SuccessText") } + /// Slow Mode - %@ + public static func storySlowModePlaceholder(_ p1: String) -> String { + return L10n.tr("Localizable", "Story.SlowMode.Placeholder", p1) + } /// Stealth Mode - %@ public static func storyStealthModePlaceholder(_ p1: String) -> String { return L10n.tr("Localizable", "Story.StealthMode.Placeholder", p1) diff --git a/packages/TGUIKit/Sources/ContextMenu.swift b/packages/TGUIKit/Sources/ContextMenu.swift index 3471f2446..6fdf214ed 100644 --- a/packages/TGUIKit/Sources/ContextMenu.swift +++ b/packages/TGUIKit/Sources/ContextMenu.swift @@ -35,6 +35,13 @@ open class ContextMenuItem : NSMenuItem { } return _id! } + + open override func isEqual(_ object: Any?) -> Bool { + if let object = object as? ContextMenuItem { + return object.title == self.title && object.state == self.state + } + return false + } public enum KeyEquiavalent: String { case none = "" diff --git a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift index da1269f3e..cb0639afd 100644 --- a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift +++ b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift @@ -10262,6 +10262,19 @@ public final class TelegramIconsTheme { return image } } + public var create_new_message_general: CGImage { + if let image = cached.with({ $0["create_new_message_general"] }) { + return image + } else { + let image = _create_new_message_general() + _ = cached.modify { current in + var current = current + current["create_new_message_general"] = image + return current + } + return image + } + } private let _dialogMuteImage: ()->CGImage private let _dialogMuteImageSelected: ()->CGImage @@ -11052,6 +11065,7 @@ public final class TelegramIconsTheme { private let _chat_hidden_author: ()->CGImage private let _chat_my_notes: ()->CGImage private let _premium_required_forward: ()->CGImage + private let _create_new_message_general: ()->CGImage public init( dialogMuteImage: @escaping()->CGImage, @@ -11842,7 +11856,8 @@ public final class TelegramIconsTheme { channel_feature_voice_to_text: @escaping()->CGImage, chat_hidden_author: @escaping()->CGImage, chat_my_notes: @escaping()->CGImage, - premium_required_forward: @escaping()->CGImage + premium_required_forward: @escaping()->CGImage, + create_new_message_general: @escaping()->CGImage ) { self._dialogMuteImage = dialogMuteImage self._dialogMuteImageSelected = dialogMuteImageSelected @@ -12633,5 +12648,6 @@ public final class TelegramIconsTheme { self._chat_hidden_author = chat_hidden_author self._chat_my_notes = chat_my_notes self._premium_required_forward = premium_required_forward + self._create_new_message_general = create_new_message_general } } \ No newline at end of file diff --git a/submodules/telegram-ios b/submodules/telegram-ios index 0fce018db..2e7d6be64 160000 --- a/submodules/telegram-ios +++ b/submodules/telegram-ios @@ -1 +1 @@ -Subproject commit 0fce018dbfe68ebdd1616f0c35c3c02ca842d90a +Subproject commit 2e7d6be64ca1fd90498825a9d2edf02a34363f1e diff --git a/tools/generate-images.swift b/tools/generate-images.swift index 77eb976e2..f96783b3a 100644 --- a/tools/generate-images.swift +++ b/tools/generate-images.swift @@ -926,6 +926,8 @@ func initialize() -> [String] { array.append("chat_my_notes") array.append("premium_required_forward") + + array.append("create_new_message_general") return array } From 51a2478bb2b9d436cbc72fee16df62be8f76037d Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 13 Feb 2024 12:02:48 -0400 Subject: [PATCH 07/50] - bugfixes --- Telegram-Mac/BusinessAwayMessageController.swift | 5 +++++ Telegram-Mac/BusinessChatbotController.swift | 4 ++++ Telegram-Mac/BusinessHoursController.swift | 4 ++++ Telegram-Mac/BusinessLocationController.swift | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/Telegram-Mac/BusinessAwayMessageController.swift b/Telegram-Mac/BusinessAwayMessageController.swift index 366d905f4..c6cdfd4f8 100644 --- a/Telegram-Mac/BusinessAwayMessageController.swift +++ b/Telegram-Mac/BusinessAwayMessageController.swift @@ -13,6 +13,8 @@ import Cocoa import TGUIKit import SwiftSignalKit +#if DEBUG + class BusinessSelectChatsCallbackObject : ShareObject { private let callback:([PeerId])->Signal private let limitReachedText: String @@ -418,3 +420,6 @@ func BusinessAwayMessageController(context: AccountContext, peerId: PeerId) -> I return controller } + + +#endif diff --git a/Telegram-Mac/BusinessChatbotController.swift b/Telegram-Mac/BusinessChatbotController.swift index 1da216b53..697c17d55 100644 --- a/Telegram-Mac/BusinessChatbotController.swift +++ b/Telegram-Mac/BusinessChatbotController.swift @@ -13,6 +13,8 @@ import Cocoa import TGUIKit import SwiftSignalKit +#if DEBUG + private final class Arguments { let context: AccountContext let toggleAccess:(State.Access)->Void @@ -288,3 +290,5 @@ func BusinessChatbotController(context: AccountContext, peerId: PeerId) -> Input return controller } + +#endif diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index d1837f0eb..7deccf1eb 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -13,6 +13,8 @@ import Cocoa import TGUIKit import SwiftSignalKit +#if DEBUG + private struct GMTZone : Equatable, Comparable { static func < (lhs: GMTZone, rhs: GMTZone) -> Bool { return lhs.hoursFromGMT < rhs.hoursFromGMT @@ -446,3 +448,5 @@ private func BusinessdayHoursController(context: AccountContext, stateSignal: Si } + +#endif diff --git a/Telegram-Mac/BusinessLocationController.swift b/Telegram-Mac/BusinessLocationController.swift index a72c85fa8..f889c7948 100644 --- a/Telegram-Mac/BusinessLocationController.swift +++ b/Telegram-Mac/BusinessLocationController.swift @@ -14,6 +14,8 @@ import TGUIKit import SwiftSignalKit import MapKit +#if DEBUG + private final class AnnotationView : MKAnnotationView { private let locationPin = ImageView() override init(annotation: MKAnnotation?, reuseIdentifier: String?) { @@ -325,3 +327,5 @@ func BusinessLocationController(context: AccountContext, peerId: PeerId) -> Inpu return controller } + +#endif From 1d47aea267886e9ba5f7b6f02607dc7f0562f6ae Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 13 Feb 2024 12:06:59 -0400 Subject: [PATCH 08/50] - bugfixes --- Telegram-Mac/ChatListFilterController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram-Mac/ChatListFilterController.swift b/Telegram-Mac/ChatListFilterController.swift index 9bcb1d4c5..ecb55b777 100644 --- a/Telegram-Mac/ChatListFilterController.swift +++ b/Telegram-Mac/ChatListFilterController.swift @@ -950,7 +950,7 @@ private func chatListFilterEntries(state: State, includePeers: [Peer], excludePe entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 - + #if DEBUG if "".isEmpty { let colors = [theme.colors.peerColors(0).top, @@ -982,7 +982,7 @@ private func chatListFilterEntries(state: State, includePeers: [Peer], excludePe entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 } - + #endif if true { entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().chatListFilterInviteLinkHeader), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textTopItem))) From 3e8b6af027d3a58f2d0071694eaab6515301e3a7 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 14 Feb 2024 08:28:59 -0400 Subject: [PATCH 09/50] - bugfixes --- Telegram-Mac/ChatMessageMenuItems.swift | 2 +- Telegram-Mac/GroupCallParticipantRowItem.swift | 3 +-- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/PopularPeersRowItem.swift | 5 ++++- Telegram-Mac/ShareModalController.swift | 3 +++ TelegramShare/Info.plist | 2 +- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Telegram-Mac/ChatMessageMenuItems.swift b/Telegram-Mac/ChatMessageMenuItems.swift index c75bddf8d..9ffeba705 100644 --- a/Telegram-Mac/ChatMessageMenuItems.swift +++ b/Telegram-Mac/ChatMessageMenuItems.swift @@ -418,7 +418,7 @@ func chatMenuItems(for message: Message, entry: ChatHistoryEntry?, textLayout: ( } if data.chatMode.threadId == nil, let peer = peer, peer.isSupergroup { - if let attr = data.message.threadAttr, attr.count > 0 { + if let attr = data.message.threadAttr, attr.count > 0, mode != .scheduled { var messageId: MessageId = message.id var modeIsReplies = true if let source = message.sourceReference { diff --git a/Telegram-Mac/GroupCallParticipantRowItem.swift b/Telegram-Mac/GroupCallParticipantRowItem.swift index 0ff9dc88d..d06ad4516 100644 --- a/Telegram-Mac/GroupCallParticipantRowItem.swift +++ b/Telegram-Mac/GroupCallParticipantRowItem.swift @@ -710,8 +710,7 @@ private final class HorizontalContainerView : GeneralContainableRowView, GroupCa for item in items { menu.addItem(item) } - NSMenu.popUpContextMenu(menu, with: event, for: button) - // AppMenu.show(menu: menu, event: event, for: button) + AppMenu.show(menu: menu, event: event, for: button) } }) } diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 6fcf4f7f2..802b927ec 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259648 + 259658 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/PopularPeersRowItem.swift b/Telegram-Mac/PopularPeersRowItem.swift index b0c51ad07..1acfba346 100644 --- a/Telegram-Mac/PopularPeersRowItem.swift +++ b/Telegram-Mac/PopularPeersRowItem.swift @@ -118,6 +118,8 @@ private final class PopularPeerItemView : HorizontalRowView { item.actionHandler(item.type) }, for: .Click) + self.container.layer?.masksToBounds = false + self.layer?.masksToBounds = false } // @@ -187,9 +189,10 @@ private final class PopularPeerItemView : HorizontalRowView { } } let layout = TextViewLayout(.initialize(string: text, color: theme.colors.text, font: .normal(11)), maximumNumberOfLines: 1) - layout.measure(width: frame.width - 15) + layout.measure(width: frame.width - 2) textView.update(layout) + self.needsLayout = true } diff --git a/Telegram-Mac/ShareModalController.swift b/Telegram-Mac/ShareModalController.swift index 95a58f586..1e4fa2669 100644 --- a/Telegram-Mac/ShareModalController.swift +++ b/Telegram-Mac/ShareModalController.swift @@ -1065,6 +1065,9 @@ final class ReplyForwardMessageObject : ShareObject { override func possibilityPerformTo(_ peer: Peer) -> Bool { let canSend = peer.canSendMessage(media: message.media.first) + if peer.id.namespace == Namespaces.Peer.SecretChat { + return false + } return !excludePeerIds.contains(peer.id) && canSend } diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 19d50efee..b938b8380 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259648 + 259658 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion From 56d149c7abdd21e1d4ccf232ef4a9e90407957d8 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 15 Feb 2024 12:20:02 -0400 Subject: [PATCH 10/50] - bugfixes --- Telegram-Mac/ApplicationContext.swift | 5 + .../BoostChannelModalController.swift | 8 +- Telegram-Mac/BusinessChatbotController.swift | 256 ++++++++- Telegram-Mac/BusinessHoursController.swift | 70 ++- Telegram-Mac/BusinessLocationController.swift | 39 +- .../BusinessQuickReplyController.swift | 488 ++++++++++++++++++ Telegram-Mac/CallWindowController.swift | 2 +- Telegram-Mac/ChatController.swift | 5 +- Telegram-Mac/ChatListRowItem.swift | 3 + Telegram-Mac/EmptyChatViewController.swift | 6 +- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/InputDataController.swift | 2 +- Telegram-Mac/LocationModalController.swift | 28 +- Telegram-Mac/SVideoController.swift | 2 +- Telegram-Mac/en.lproj/Localizable.strings | Bin 829946 -> 835882 bytes Telegram.xcodeproj/project.pbxproj | 4 + TelegramShare/Info.plist | 2 +- .../Sources/Localization/Localizable.swift | 120 ++++- packages/TGUIKit/Sources/TableRowView.swift | 2 +- packages/TGUIKit/Sources/TableView.swift | 2 +- 20 files changed, 993 insertions(+), 53 deletions(-) create mode 100644 Telegram-Mac/BusinessQuickReplyController.swift diff --git a/Telegram-Mac/ApplicationContext.swift b/Telegram-Mac/ApplicationContext.swift index 15c4793ee..553a4f87f 100644 --- a/Telegram-Mac/ApplicationContext.swift +++ b/Telegram-Mac/ApplicationContext.swift @@ -562,6 +562,11 @@ final class AuthorizedApplicationContext: NSObject, SplitViewDelegate { return .invoked }, with: self, for: .J, priority: .supreme, modifierFlags: [.command]) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + context.bindings.rootNavigation().push(BusinessQuickReplyController(context: context, peerId: context.peerId)) + + return .invoked + }, with: self, for: .R, priority: .supreme, modifierFlags: [.command]) #endif diff --git a/Telegram-Mac/BoostChannelModalController.swift b/Telegram-Mac/BoostChannelModalController.swift index 6fa9b5bf3..a9075b55b 100644 --- a/Telegram-Mac/BoostChannelModalController.swift +++ b/Telegram-Mac/BoostChannelModalController.swift @@ -472,17 +472,17 @@ private final class BoostRowItem : TableRowItem { case let .unblockText(count): if count > state.boostedByMe { if state.status.nextLevelBoosts == nil { - string = strings().channelBoostUnblockTextGroupFull("\(count - state.boostedByMe)") + string = strings().channelBoostUnblockTextGroupFullCountable(Int(count - state.boostedByMe)) } else { - string = strings().channelBoostUnblockTextGroup("\(count - state.boostedByMe)", state.peer.peer.displayTitle) + string = strings().channelBoostUnblockTextGroupCountable(Int(count - state.boostedByMe), state.peer.peer.displayTitle) } } case let .unblockSlowmode(count): if count > state.boostedByMe { if state.status.nextLevelBoosts == nil { - string = strings().channelBoostUnblockSlowmodeGroupFull("\(count - state.boostedByMe)") + string = strings().channelBoostUnblockSlowmodeGroupFullCountable(Int(state.boostedByMe)) } else { - string = strings().channelBoostUnblockSlowmodeGroup("\(count - state.boostedByMe)", state.peer.peer.displayTitle) + string = strings().channelBoostUnblockSlowmodeGroupCountable(Int(count - state.boostedByMe), state.peer.peer.displayTitle) } } default: diff --git a/Telegram-Mac/BusinessChatbotController.swift b/Telegram-Mac/BusinessChatbotController.swift index 697c17d55..05d68cc26 100644 --- a/Telegram-Mac/BusinessChatbotController.swift +++ b/Telegram-Mac/BusinessChatbotController.swift @@ -15,18 +15,163 @@ import SwiftSignalKit #if DEBUG +private final class BusinessBotRowItem : GeneralRowItem { + let bot: EnginePeer + let context: AccountContext + let titleLayout: TextViewLayout + let statusLayout: TextViewLayout + let selected: Bool + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, bot: EnginePeer, selected: Bool, viewType: GeneralViewType, action: @escaping()->Void) { + self.bot = bot + self.context = context + self.selected = selected + titleLayout = .init(.initialize(string: bot._asPeer().displayTitle, color: theme.colors.text, font: .medium(.text))) + statusLayout = .init(.initialize(string: "bot", color: theme.colors.grayText, font: .normal(.text))) + super.init(initialSize, stableId: stableId, viewType: viewType, action: action) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + titleLayout.measure(width: blockWidth - 30 - 70) + statusLayout.measure(width: blockWidth - 30 - 70) + return true + } + + override var height: CGFloat { + return 44 + } + + override func viewClass() -> AnyClass { + return BusinessBotRowView.self + } +} + +private final class BusinessBotRowView: GeneralContainableRowView { + private let avatar = AvatarControl(font: .avatar(10)) + private let titleView = TextView() + private let statusView = TextView() + private var add: TextButton? + private var remove: ImageButton? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + avatar.setFrameSize(NSMakeSize(30, 30)) + addSubview(avatar) + addSubview(titleView) + addSubview(statusView) + + avatar.userInteractionEnabled = false + + titleView.userInteractionEnabled = false + titleView.isSelectable = false + + statusView.userInteractionEnabled = false + statusView.isSelectable = false + + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? BusinessBotRowItem else { + return + } + + avatar.setPeer(account: item.context.account, peer: item.bot._asPeer()) + titleView.update(item.titleLayout) + statusView.update(item.statusLayout) + + if item.selected { + if let view = self.add { + performSubviewRemoval(view, animated: animated) + self.add = nil + } + let current: ImageButton + if let view = self.remove { + current = view + } else { + current = ImageButton() + current.scaleOnClick = true + addSubview(current) + self.remove = current + + current.set(handler: { [weak self] _ in + if let item = self?.item as? GeneralRowItem { + item.action() + } + }, for: .Click) + } + current.set(image: theme.icons.stickersRemove, for: .Normal) + current.sizeToFit(.zero, NSMakeSize(24, 24), thatFit: true) + } else { + if let view = self.remove { + performSubviewRemoval(view, animated: animated) + self.remove = nil + } + let current: TextButton + if let view = self.add { + current = view + } else { + current = TextButton() + current.scaleOnClick = true + addSubview(current) + self.add = current + + current.set(handler: { [weak self] _ in + if let item = self?.item as? GeneralRowItem { + item.action() + } + }, for: .Click) + } + current.set(font: .medium(.text), for: .Normal) + current.set(color: theme.colors.underSelectedColor, for: .Normal) + current.set(background: theme.colors.accent, for: .Normal) + current.set(text: "ADD", for: .Normal) + current.sizeToFit(NSMakeSize(10, 6)) + current.layer?.cornerRadius = current.frame.height / 2 + } + + needsLayout = true + } + + + override func layout() { + super.layout() + guard let item = item as? BusinessBotRowItem else { + return + } + avatar.centerY(x: item.viewType.innerInset.left) + titleView.setFrameOrigin(NSMakePoint(avatar.frame.maxX + 10, 7)) + statusView.setFrameOrigin(NSMakePoint(avatar.frame.maxX + 10, containerView.frame.height - statusView.frame.height - 5)) + + if let add { + add.centerY(x: containerView.frame.width - add.frame.width - item.viewType.innerInset.left) + } + if let remove { + remove.centerY(x: containerView.frame.width - remove.frame.width - item.viewType.innerInset.left) + } + } +} + private final class Arguments { let context: AccountContext let toggleAccess:(State.Access)->Void let selectChats:()->Void let toggleReplyAccess:()->Void let removeIncluded:(PeerId)->Void - init(context: AccountContext, toggleAccess:@escaping(State.Access)->Void, selectChats:@escaping()->Void, toggleReplyAccess:@escaping()->Void, removeIncluded:@escaping(PeerId)->Void) { + let setBot:(EnginePeer?)->Void + init(context: AccountContext, toggleAccess:@escaping(State.Access)->Void, selectChats:@escaping()->Void, toggleReplyAccess:@escaping()->Void, removeIncluded:@escaping(PeerId)->Void, setBot:@escaping(EnginePeer?)->Void) { self.context = context self.toggleAccess = toggleAccess self.selectChats = selectChats self.toggleReplyAccess = toggleReplyAccess self.removeIncluded = removeIncluded + self.setBot = setBot } } @@ -35,9 +180,16 @@ private struct State : Equatable { case all case selected } + enum BotsResult : Equatable { + case found([EnginePeer]) + case loading + } var username: String? var access: Access = .all + var botsResult: BotsResult? = nil + var bot: EnginePeer? = nil + var replyAccess: Bool = true @@ -61,11 +213,15 @@ private let _id_exclude_chats = InputDataIdentifier("_id_exclude_chats") private let _id_reply_to_message = InputDataIdentifier("_id_reply_to_message") private let _id_remove = InputDataIdentifier("_id_remove") - +private let _id_loading = InputDataIdentifier("_id_loading") private func _id_peer(_ id: PeerId) -> InputDataIdentifier { return InputDataIdentifier("_id_peer_\(id.toInt64())") } +private func _id_bot(_ id: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_bot_\(id.toInt64())") +} + private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { var entries:[InputDataEntry] = [] @@ -89,8 +245,58 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 + if let bot = state.bot { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_input, equatable: .init(state), comparable: nil, item: { initialSize, stableId in + return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .firstItem, text: "https://t.me/\(bot.addressName ?? "")", font: .normal(.text), color: theme.colors.text) + })) + } else { + entries.append(.input(sectionId: sectionId, index: 0, value: .string(state.username), error: nil, identifier: _id_input, mode: .plain, data: .init(viewType: state.botsResult == nil ? .singleItem : .firstItem, defaultText: ""), placeholder: nil, inputPlaceholder: "Bot Username", filter: { $0 }, limit: 60)) + } - entries.append(.input(sectionId: sectionId, index: 0, value: .string(state.username), error: nil, identifier: _id_input, mode: .plain, data: .init(viewType: .singleItem, defaultText: ""), placeholder: nil, inputPlaceholder: "Bot Username", filter: { $0 }, limit: 60)) + if let result = state.botsResult { + switch result { + case .loading: + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_loading, equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralLoadingRowItem(initialSize, stableId: stableId, viewType: .lastItem) + })) + case let .found(peers): + struct Tuple : Equatable { + let peer: EnginePeer + let viewType: GeneralViewType + let selected: Bool + } + var tuples: [Tuple] = [] + + if peers.isEmpty { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_bot(.init(0)), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .lastItem, text: "No bots found", font: .normal(.text), color: theme.colors.grayText, centerViewAlignment: true) + })) + } else { + for (i, peer) in peers.enumerated() { + var viewType: GeneralViewType = bestGeneralViewType(peers, for: i) + if i == 0 { + if i < peers.count - 1 { + viewType = .innerItem + } else { + viewType = .lastItem + } + } + tuples.append(.init(peer: peer, viewType: viewType, selected: state.bot?.id == peer.id)) + } + for tuple in tuples { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_bot(tuple.peer.id), equatable: .init(tuple), comparable: nil, item: { initialSize, stableId in + return BusinessBotRowItem(initialSize, stableId: stableId, context: arguments.context, bot: tuple.peer, selected: tuple.selected, viewType: tuple.viewType, action: { + if tuple.selected { + arguments.setBot(nil) + } else { + arguments.setBot(tuple.peer) + } + }) + })) + } + } + } + } entries.append(.sectionId(sectionId, type: .normal)) @@ -275,6 +481,18 @@ func BusinessChatbotController(context: AccountContext, peerId: PeerId) -> Input current.selectedIds.removeAll(where: { $0 == peerId }) return current } + }, setBot: { bot in + updateState { current in + var current = current + current.bot = bot + current.username = nil + if let bot = bot { + current.botsResult = .found([bot]) + } else { + current.botsResult = nil + } + return current + } }) let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in @@ -283,6 +501,38 @@ func BusinessChatbotController(context: AccountContext, peerId: PeerId) -> Input let controller = InputDataController(dataSignal: signal, title: "Chat Bot", removeAfterDisappear: false, hasDone: false) + + controller.updateDatas = { datas in + updateState { current in + var current = current + current.username = datas[_id_input]?.stringValue + return current + } + return .none + } + + let usernameUpdate: Signal = statePromise.get() |> filter { $0.bot == nil } + |> map { $0.username } + |> distinctUntilChanged + |> mapToSignal { username in + if let username = username, !username.isEmpty { + return .single(.loading) |> then(context.engine.contacts.searchRemotePeers(query: username) |> map { + return .found(($0.0 + $0.1).prefix(5).filter { $0.peer.isBot }.map { EnginePeer($0.peer) }) + }) + } else { + return .single(nil) + } + } + + actionsDisposable.add(usernameUpdate.startStandalone(next: { result in + updateState { current in + var current = current + current.botsResult = result + return current + } + })) + + controller.onDeinit = { actionsDisposable.dispose() } diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index 7deccf1eb..6fd9314bf 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -90,6 +90,36 @@ private func formatHourToLocaleTime(hour: Int) -> String { return formattedTime } +private func formatMinutesToLocaleTime(minutes: Int) -> String { + + // Create a DateComponents object with the hour set to the provided value + var components = DateComponents() + components.minute = minutes + + // Use the current calendar to ensure the components are interpreted correctly + let calendar = Calendar.current + + // Optional: you might want to ensure you're using the current time zone + components.timeZone = TimeZone.current + + // Create a Date from components + guard let date = calendar.date(from: components) else { + print("Failed to create date from components.") + return "" + } + + // Create a DateFormatter and set its dateStyle to .none and timeStyle to .short + // This will ensure that the time is formatted according to the user's locale + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .short + + // Format the date to a string + let formattedTime = formatter.string(from: date) + + return formattedTime +} + // Example usage let formattedTime = formatHourToLocaleTime(hour: 3) @@ -269,6 +299,8 @@ private func dayEntries(_ state: State, day: State.Day, arguments: Arguments) -> var sectionId:Int32 = 0 var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 @@ -293,14 +325,23 @@ private func dayEntries(_ state: State, day: State.Day, arguments: Arguments) -> end = 24 } + let minutes = ContextMenu() + for j in 0 ..< 60 { + minutes.addItem(ContextMenuItem(formatMinutesToLocaleTime(minutes: j))) + } + for i in start ..< end { - items.append(.init(formatHourToLocaleTime(hour: i), handler: { + let item = ContextMenuItem(formatHourToLocaleTime(hour: i), handler: { arguments.editSpefic(day, .init(from: from ? i : hour.from, to: !from ? i : hour.to, uniqueId: hour.uniqueId)) - }, state: i == (from ? hour.from : hour.to) ? .on : nil)) + }, state: i == (from ? hour.from : hour.to) ? .on : nil) + + + item.submenu = minutes + items.append(item) } return items } - + for hour in hours.list { entries.append(.sectionId(sectionId, type: .normal)) @@ -317,20 +358,19 @@ private func dayEntries(_ state: State, day: State.Day, arguments: Arguments) -> } } - + let count = state.data[day]?.list.count ?? 0 - - if count < 3 { - entries.append(.sectionId(sectionId, type: .normal)) - sectionId += 1 - - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_add_specific, data: .init(name: "Add a Set of Hours", color: theme.colors.accent, type: .none, viewType: .singleItem, action: { - arguments.addSpecific(day) - }))) - entries.append(.desc(sectionId: sectionId, index: index, text: .plain("Specify your working hours during the day."), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) - index += 1 - } + sectionId = 1000 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_add_specific, data: .init(name: "Add a Set of Hours", color: theme.colors.accent, type: .none, viewType: .singleItem, action: { + arguments.addSpecific(day) + }))) + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("Specify your working hours during the day."), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) entries.append(.sectionId(sectionId, type: .normal)) diff --git a/Telegram-Mac/BusinessLocationController.swift b/Telegram-Mac/BusinessLocationController.swift index f889c7948..71ff20a62 100644 --- a/Telegram-Mac/BusinessLocationController.swift +++ b/Telegram-Mac/BusinessLocationController.swift @@ -74,10 +74,10 @@ private final class AnnotationView : MKAnnotationView { private class MapRowItem: GeneralRowItem { let context: AccountContext fileprivate let location: State.Location - init(_ initialSize: NSSize, height: CGFloat, stableId: AnyHashable, context: AccountContext, location: State.Location, viewType: GeneralViewType) { + init(_ initialSize: NSSize, height: CGFloat, stableId: AnyHashable, context: AccountContext, location: State.Location, viewType: GeneralViewType, action: @escaping()->Void) { self.context = context self.location = location - super.init(initialSize, height: height, stableId: stableId, viewType: viewType) + super.init(initialSize, height: height, stableId: stableId, viewType: viewType, action: action) } deinit { @@ -92,6 +92,7 @@ private class MapRowItem: GeneralRowItem { private final class MapRowItemView : GeneralContainableRowView, MKMapViewDelegate { private let mapView: MKMapView = MKMapView() + private let overlay = Control() required init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(mapView) @@ -101,7 +102,17 @@ private final class MapRowItemView : GeneralContainableRowView, MKMapViewDelegat mapView.showsZoomControls = false mapView.showsUserLocation = false + mapView.isZoomEnabled = false + mapView.isScrollEnabled = false + mapView.showsBuildings = false + addSubview(overlay) + + overlay.set(handler: { [weak self] _ in + if let item = self?.item as? GeneralRowItem { + item.action() + } + }, for: .Click) } required init?(coder: NSCoder) { @@ -115,6 +126,7 @@ private final class MapRowItemView : GeneralContainableRowView, MKMapViewDelegat override func layout() { super.layout() mapView.frame = bounds + overlay.frame = bounds } @@ -204,9 +216,11 @@ extension CLLocationCoordinate2D : Equatable { private final class Arguments { let context: AccountContext let setLocation:()->Void - init(context: AccountContext, setLocation:@escaping()->Void) { + let openMap:()->Void + init(context: AccountContext, setLocation:@escaping()->Void, openMap:@escaping()->Void) { self.context = context self.setLocation = setLocation + self.openMap = openMap } } @@ -257,7 +271,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { if let location = state.location { entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_map_map, equatable: .init(state.location), comparable: nil, item: { initialSize, stableId in - return MapRowItem(initialSize, height: 200, stableId: stableId, context: arguments.context, location: location, viewType: .lastItem) + return MapRowItem(initialSize, height: 200, stableId: stableId, context: arguments.context, location: location, viewType: .lastItem, action: arguments.openMap) })) } @@ -284,6 +298,19 @@ func BusinessLocationController(context: AccountContext, peerId: PeerId) -> Inpu let chatInteraction = ChatInteraction(chatLocation: .peer(peerId), context: context) chatInteraction.sendLocation = { location, venue in + + let signal = reverseGeocodeLocation(latitude: location.latitude, longitude: location.longitude) |> deliverOnMainQueue + + if stateValue.with({ $0.address == nil || $0.address!.isEmpty }) { + _ = signal.startStandalone(next: { value in + updateState { current in + var current = current + current.address = value?.fullAddress + return current + } + }) + } + updateState { current in var current = current current.location = .init(coordinate: location, venue: venue) @@ -301,8 +328,10 @@ func BusinessLocationController(context: AccountContext, peerId: PeerId) -> Inpu return current } } else { - showModal(with: LocationModalController(chatInteraction, destination: .business), for: context.window) + showModal(with: LocationModalController(chatInteraction, destination: .business(value?.coordinate)), for: context.window) } + }, openMap: { + showModal(with: LocationModalController(chatInteraction, destination: .business(stateValue.with { $0.location?.coordinate })), for: context.window) }) let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in diff --git a/Telegram-Mac/BusinessQuickReplyController.swift b/Telegram-Mac/BusinessQuickReplyController.swift new file mode 100644 index 000000000..bcc5c3d80 --- /dev/null +++ b/Telegram-Mac/BusinessQuickReplyController.swift @@ -0,0 +1,488 @@ +// +// BusinessQuickReplyController.swift +// Telegram +// +// Created by Mikhail Filimonov on 15.02.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import Postbox +import Cocoa +import TGUIKit +import SwiftSignalKit + +#if DEBUG + +private final class QuickReplyRowItem : GeneralRowItem { + + let reply: State.Reply + let context: AccountContext + let textLayout: TextViewLayout + let editing: Bool + let open: (State.Reply)->Void + let editName: (State.Reply)->Void + let remove: (State.Reply)->Void + init(_ initialSize: NSSize, stableId: AnyHashable, reply: State.Reply, context: AccountContext, editing: Bool, viewType: GeneralViewType, open: @escaping(State.Reply)->Void, editName: @escaping(State.Reply)->Void, remove: @escaping(State.Reply)->Void) { + self.reply = reply + self.context = context + self.editing = editing + self.editName = editName + self.open = open + self.remove = remove + let attr = NSMutableAttributedString() + attr.append(string: "/\(reply.name)", color: theme.colors.text, font: .normal(.text)) + attr.append(string: " ", color: theme.colors.text, font: .normal(.text)) + attr.append(string: reply.messages[0], color: theme.colors.grayText, font: .normal(.text)) + + self.textLayout = TextViewLayout(attr) + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + return .single([ContextMenuItem("Edit Name", handler: { [weak self] in + if let self { + self.editName(self.reply) + } + }, itemImage: MenuAnimation.menu_edit.value)]) + } + + private var textWidth: CGFloat { + var width = blockWidth + width -= (leftInset + viewType.innerInset.left) + + width -= 30 // photo + width -= viewType.innerInset.left // photo + + + if editing { + width -= 30 //left control + width -= 30 // sort control + } + + return width + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + + textLayout.measure(width: textWidth) + + return true + } + + override func viewClass() -> AnyClass { + return QuickReplyRowItemView.self + } + + override var height: CGFloat { + return 44 + } + + var leftInset: CGFloat { + return 20 + } +} + +private final class QuickReplyRowItemView: GeneralContainableRowView { + private let textView = TextView() + private let imageView = ImageView(frame: NSMakeRect(0, 0, 30, 30)) + private let container = View() + + private var remove: ImageButton? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(container) + container.addSubview(imageView) + container.addSubview(textView) + + imageView.layer?.backgroundColor = theme.colors.grayIcon.cgColor + imageView.layer?.cornerRadius = imageView.frame.height / 2 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? QuickReplyRowItem else { + return + } + + textView.update(item.textLayout) + + if item.editing { + let current: ImageButton + var isNew = false + if let view = self.remove { + current = view + } else { + current = ImageButton() + current.scaleOnClick = true + current.autohighlight = false + addSubview(current) + self.remove = current + + current.set(handler: { [weak self] _ in + if let item = self?.item as? QuickReplyRowItem { + item.remove(item.reply) + } + }, for: .Click) + + isNew = true + } + current.set(image: theme.icons.deleteItem, for: .Normal) + current.sizeToFit(.zero, NSMakeSize(24, 24), thatFit: true) + + if isNew { + current.centerY(x: item.leftInset) + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + } else if let view = remove { + performSubviewRemoval(view, animated: animated) + self.remove = nil + } + + self.updateLayout(size: frame.size, transition: animated ? .animated(duration: 0.2, curve: .easeOut) : .immediate) + } + + override func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + super.updateLayout(size: size, transition: transition) + + guard let item = item as? QuickReplyRowItem else { + return + } + + if let remove = remove { + transition.updateFrame(view: remove, frame: remove.centerFrameY(x: item.leftInset)) + } + + let contentInset = item.editing ? item.leftInset * 2 + 18 : 16 + + let containerRect = NSMakeRect(contentInset, 0, containerView.frame.width - contentInset, containerView.frame.height) + + transition.updateFrame(view: container, frame: containerRect) + + transition.updateFrame(view: imageView, frame: imageView.centerFrameY(x: 0)) + transition.updateFrame(view: textView, frame: textView.centerFrameY(x: imageView.frame.maxX + 10)) + } +} + + +private final class Arguments { + let context: AccountContext + let add:()->Void + let edit:(State.Reply)->Void + let editName:(State.Reply)->Void + let remove:(State.Reply)->Void + init(context: AccountContext, add:@escaping()->Void, edit:@escaping(State.Reply)->Void, remove:@escaping(State.Reply)->Void, editName:@escaping(State.Reply)->Void) { + self.context = context + self.add = add + self.edit = edit + self.remove = remove + self.editName = editName + } +} + +private struct State : Equatable { + struct Reply : Equatable { + var name: String + var messages: [String] + var id: Int64 + } + + var replies: [Reply] = [] + + var editing: Bool = false + + var creatingName: String? + var input_error: InputDataValueError? +} + + +private let _id_header = InputDataIdentifier("_id_header") +private let _id_add = InputDataIdentifier("_id_add") +private func _id_reply(_ id: Int64) -> InputDataIdentifier { + return .init("_id_reply_\(id)") +} + + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let headerAttr = NSMutableAttributedString() + _ = headerAttr.append(string: "Set up shortcuts with rich text and media to respond to messages faster.", color: theme.colors.listGrayText, font: .normal(.text)) + + entries.append(.custom(sectionId: sectionId, index: 0, value: .none, identifier: _id_header, equatable: nil, comparable: nil, item: { initialSize, stableId in + return AnimatedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: LocalAnimatedSticker.fly_dollar, text: headerAttr) + })) + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: 0, value: .none, error: nil, identifier: _id_add, data: .init(name: "Add Quick Reply", color: theme.colors.accent, icon: theme.icons.stickersAddFeatured, type: .none, viewType: state.replies.isEmpty ? .singleItem : .firstItem, action: { + arguments.add() + }))) + + struct Tuple : Equatable { + var reply: State.Reply + var viewType: GeneralViewType + var editing: Bool + } + var tuples: [Tuple] = [] + + for (i, reply) in state.replies.enumerated() { + var viewType: GeneralViewType = bestGeneralViewType(state.replies, for: i) + if i == 0 { + if i < state.replies.count - 1 { + viewType = .innerItem + } else { + viewType = .lastItem + } + } + tuples.append(.init(reply: reply, viewType: viewType, editing: state.editing)) + } + + for tuple in tuples { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_reply(tuple.reply.id), equatable: .init(tuple), comparable: nil, item: { initialSize, stableId in + return QuickReplyRowItem(initialSize, stableId: stableId, reply: tuple.reply, context: arguments.context, editing: tuple.editing, viewType: tuple.viewType, open: arguments.edit, editName: arguments.editName, remove: arguments.remove) + })) + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + + +func BusinessQuickReplyController(context: AccountContext, peerId: PeerId) -> InputDataController { + + let actionsDisposable = DisposableSet() + + let initialState = State() + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context, add: { + showModal(with: BusinessAddQuickReply(context: context, stateSignal: statePromise.get(), stateValue: stateValue, updateState: updateState, reply: nil), for: context.window) + }, edit: { reply in + + }, remove: { reply in + updateState { current in + var current = current + current.replies.removeAll(where: { $0.id == reply.id }) + return current + } + }, editName: { reply in + showModal(with: BusinessAddQuickReply(context: context, stateSignal: statePromise.get(), stateValue: stateValue, updateState: updateState, reply: reply), for: context.window) + }) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: "Quick Replies", removeAfterDisappear: false) + + controller.updateDoneValue = { data in + return { f in + if !stateValue.with({ $0.editing }) { + f(.enabled(strings().navigationEdit)) + } else { + f(.enabled(strings().navigationDone)) + } + } + } + controller.validateData = { _ in + updateState { current in + var current = current + current.editing = !current.editing + return current + } + return .none + } + + + controller.afterTransaction = { controller in + var range: NSRange = NSMakeRange(NSNotFound, 0) + + controller.tableView.enumerateItems(with: { item in + if let item = item as? QuickReplyRowItem { + if item.editing { + if range.location == NSNotFound { + range.location = item.index + } + range.length += 1 + } else { + return false + } + } + return true + }) + + if range.location != NSNotFound { + controller.tableView.resortController = .init(resortRange: range, start: { _ in + + }, resort: { _ in }, complete: { from, to in + let fromValue = from - range.location + let toValue = to - range.location + var replies = stateValue.with { $0.replies } + replies.move(at: fromValue, to: toValue) +// _ = nextTransactionNonAnimated.swap(true) + updateState { current in + var current = current + current.replies = replies + return current + } + }) + } else { + controller.tableView.resortController = nil + } + } + + controller.onDeinit = { + actionsDisposable.dispose() + } + + return controller + +} + + + +private let _id_input = InputDataIdentifier("_id_input") + +private func newReplyEntries(_ state: State) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + // + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("Add a shortcut for your quick reply."), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.input(sectionId: sectionId, index: 0, value: .string(state.creatingName), error: state.input_error, identifier: _id_input, mode: .plain, data: .init(viewType: .singleItem, defaultText: ""), placeholder: nil, inputPlaceholder: "Enter name...", filter: { $0.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) }, limit: 40)) + + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +private func BusinessAddQuickReply(context: AccountContext, stateSignal: Signal, stateValue: Atomic, updateState: @escaping((State) -> State) -> Void, reply: State.Reply?) -> InputDataModalController { + + var close:(()->Void)? = nil + + let actionsDisposable = DisposableSet() + + updateState { current in + var current = current + current.creatingName = reply?.name + return current + } + + let signal = stateSignal |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: newReplyEntries(state)) + } + + let controller = InputDataController(dataSignal: signal, title: "New Quick Reply") + + controller.onDeinit = { + actionsDisposable.dispose() + } + + let modalInteractions = ModalInteractions(acceptTitle: strings().modalDone, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + controller.updateDatas = { data in + updateState { current in + var current = current + current.creatingName = data[_id_input]?.stringValue + current.input_error = nil + return current + } + return .none + } + + controller.validateData = { data in + + let value = data[_id_input]?.stringValue + + let replies = stateValue.with { $0.replies } + let contains = replies.contains(where: { $0.name == value }) + + if contains, reply?.name != value { + updateState { current in + var current = current + current.input_error = .init(description: "Shortcut with that name already exists.", target: .data) + return current + } + return .fail(.fields([_id_input : .shake])) + } + + if value?.isEmpty == true { + return .fail(.fields([_id_input : .shake])) + } + + updateState { current in + var current = current + if let input = current.creatingName, !input.isEmpty { + if let index = current.replies.firstIndex(where: { $0.id == reply?.id }) { + current.replies[index].name = input + } else { + current.replies.append(.init(name: input, messages: ["text"], id: arc4random64())) + } + } + return current + } + close?() + return .none + } + + close = { [weak modalController] in + modalController?.modal?.close() + } + + + return modalController + + +} + +#endif + +/* + */ diff --git a/Telegram-Mac/CallWindowController.swift b/Telegram-Mac/CallWindowController.swift index fe7be6c3a..3434f4d1a 100644 --- a/Telegram-Mac/CallWindowController.swift +++ b/Telegram-Mac/CallWindowController.swift @@ -1230,7 +1230,7 @@ class PhoneCallWindowController { @objc open func windowDidResignKey() { keyStateDisposable.set((session.state |> deliverOnMainQueue).start(next: { [weak self] state in if let strongSelf = self { - if case .active = state.state, !strongSelf.session.isVideo, !strongSelf.window.isKeyWindow { + if case .active = state.state, !strongSelf.session.isVideo, !strongSelf.session.isScreenCapture, !strongSelf.window.isKeyWindow { switch state.videoState { case .active, .paused: break diff --git a/Telegram-Mac/ChatController.swift b/Telegram-Mac/ChatController.swift index 20a859b72..8e37a9238 100644 --- a/Telegram-Mac/ChatController.swift +++ b/Telegram-Mac/ChatController.swift @@ -6540,8 +6540,9 @@ class ChatController: EditableViewController, Notifable, Tab } return true }) - self.dustLayerView = ApplyDustAnimations(for: foundItemViews, superview: self.dustLayerView) - + if dustLayerView == nil { + self.dustLayerView = ApplyDustAnimations(for: foundItemViews, superview: self.dustLayerView) + } } CATransaction.commit() } diff --git a/Telegram-Mac/ChatListRowItem.swift b/Telegram-Mac/ChatListRowItem.swift index 69fbdfcc7..6d2180f50 100644 --- a/Telegram-Mac/ChatListRowItem.swift +++ b/Telegram-Mac/ChatListRowItem.swift @@ -1224,6 +1224,9 @@ class ChatListRowItem: TableRowItem { if let topicsLayout = forumTopicNameLayout, let _ = tags { w += topicsLayout.layoutSize.width + 5 } + if let _ = tags, !contentImageSpecs.isEmpty { + w += CGFloat(contentImageSpecs.count) * 16 + } return (max(200, size.width) - margin * 3) - w - (chatNameLayout != nil ? textLeftCutout : 0) } diff --git a/Telegram-Mac/EmptyChatViewController.swift b/Telegram-Mac/EmptyChatViewController.swift index 67788078c..a9ddbaa10 100644 --- a/Telegram-Mac/EmptyChatViewController.swift +++ b/Telegram-Mac/EmptyChatViewController.swift @@ -238,9 +238,9 @@ class EmptyChatViewController: TelegramGenericViewController { }, for: .Click) } - override func firstResponder() -> NSResponder? { - return context.bindings.mainController().chatList.firstResponder() - } +// override func firstResponder() -> NSResponder? { +// return context.bindings.mainController().chatList.firstResponder() +// } override var window: Window? { return context.window diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 802b927ec..0fa50f182 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259658 + 259736 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/InputDataController.swift b/Telegram-Mac/InputDataController.swift index 4ef0843c8..b406b136c 100644 --- a/Telegram-Mac/InputDataController.swift +++ b/Telegram-Mac/InputDataController.swift @@ -307,7 +307,7 @@ func prepareInputDataTransition(left:[AppearanceWrapperEntry], r } }) if !cancelled.with({ $0 }) { - subscriber.putNext(TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated:animated, state: .none(nil), grouping: grouping, animateVisibleOnly: !animateEverything, searchState: searchState)) + subscriber.putNext(TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated:animated, state: .none(nil), grouping: true, animateVisibleOnly: !animateEverything, searchState: searchState)) subscriber.putCompletion() } } diff --git a/Telegram-Mac/LocationModalController.swift b/Telegram-Mac/LocationModalController.swift index 4fa639792..4ac3261ec 100644 --- a/Telegram-Mac/LocationModalController.swift +++ b/Telegram-Mac/LocationModalController.swift @@ -453,11 +453,25 @@ private class MapDelegate : NSObject, MKMapViewDelegate { mapView.setRegion(region, animated: animated) animated = true } + + func focusVenue(mapView: MKMapView, _ location: CLLocationCoordinate2D) { + let userLocation = location + var region = MKCoordinateRegion() + var span = MKCoordinateSpan() + span.latitudeDelta = CLLocationDegrees(0.005) + span.longitudeDelta = CLLocationDegrees(0.005) + var location = CLLocationCoordinate2D() + location.latitude = userLocation.latitude + location.longitude = userLocation.longitude + region.span = span + region.center = location + mapView.setRegion(region, animated: true) + } } -enum SelectLocationDestination { +enum SelectLocationDestination : Equatable { case chat - case business + case business(CLLocationCoordinate2D?) } class LocationModalController: ModalViewController { @@ -561,6 +575,16 @@ class LocationModalController: ModalViewController { delegate.focusUserLocation(genericView.mapView) } + switch destination { + case let .business(coordinate): + if let coordinate = coordinate { + delegate.focusVenue(mapView: genericView.mapView, coordinate) + self.delegate.isPinRaised = true + } + default: + break + } + var handleRegion: Bool = true delegate.willChangeRegion = { [weak self] in diff --git a/Telegram-Mac/SVideoController.swift b/Telegram-Mac/SVideoController.swift index 75300beb2..ef8342d1b 100644 --- a/Telegram-Mac/SVideoController.swift +++ b/Telegram-Mac/SVideoController.swift @@ -136,7 +136,7 @@ class SVideoController: GenericViewController, PictureInPictureContr NSCursor.unhide() hideOnIdleDisposable.set((Signal.complete() |> delay(1.0, queue: Queue.mainQueue())).start(completed: { [weak self] in guard let `self` = self else {return} - let hide = !self.genericView.isInMenu + let hide = !self.genericView.isInMenu && !self.genericView.insideControls self.hideControls.set(hide) if !self.pictureInPicture, !self.isPaused, hide { NSCursor.hide() diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index 714a407f068d14b00d178538ced9b4a040591bc4..e20467725564308310ea146d628f122e11b12fad 100644 GIT binary patch delta 1105 zcmex$*=SXhaYGAZ3sVbo3rh=Y3tJ0&3r7oQ3)d8GzUgO5Ig}N1PhrpkLIs8rhD?TBhScc?PjHG)-@wSNH+2TL$@H!V%zRVtapz3W zD&r8E{6?N<`UGZfDaMrPiA>z;Sd__~W+z6OT`D&*$|A}+M2N6d?+m*bL1lWA*Bxe? zF2KSK4FNAkZm#Jp6&wcBCw$}*frJWJETjUvr6$vP7`Z*Bhnz+7AF!s<_` z^dogfe9|rF*s*#as@v=i8&+?dO)v0Z7n=UyKQOWdn7C!8KS<(6jekCfI4}{Qo8JkG z@d$_#lxWBBZ6OnPC7K2-2>{(pERm0{1WUxDE5Q=!a3y-vS50FSLkfWcX7tF01_4+C zON@t3?>oT728wYlNI*ao;*EP?Xt-k4h9wnXHy=xSz^)KW%D}D=OBxBCevgq`W%`FN hTs+ekuy6}a-*b$KYxs#RPvGR1n$9K2 z$ur%8o7->t2N7gX> xko89ia#u~iAc3MEs3L57fd{+LbO%|W{yoQ-xTe3k!pPS?L7IE}1ZkdGO9A1_I7$Ej diff --git a/Telegram.xcodeproj/project.pbxproj b/Telegram.xcodeproj/project.pbxproj index 770f73ace..a70493743 100644 --- a/Telegram.xcodeproj/project.pbxproj +++ b/Telegram.xcodeproj/project.pbxproj @@ -387,6 +387,7 @@ 27916F2D287D77660046DB44 /* objects.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27916F25287D77640046DB44 /* objects.tgs */; }; 27916F2E287D77660046DB44 /* activity.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27916F26287D77650046DB44 /* activity.tgs */; }; 27916F2F287D77660046DB44 /* animals.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27916F27287D77660046DB44 /* animals.tgs */; }; + 279338872B7E2B070096A292 /* BusinessQuickReplyController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279338862B7E2B070096A292 /* BusinessQuickReplyController.swift */; }; 2795578E2AB86A65007EC996 /* libsharpyuv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2795578D2AB86A5C007EC996 /* libsharpyuv.a */; }; 2796FEEF273E503200DA653C /* ChatInputSendAsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2796FEEE273E503100DA653C /* ChatInputSendAsView.swift */; }; 2796FEF227423A8600DA653C /* EmojiSuggestions in Frameworks */ = {isa = PBXBuildFile; productRef = 2796FEF127423A8600DA653C /* EmojiSuggestions */; }; @@ -1886,6 +1887,7 @@ 27916F25287D77640046DB44 /* objects.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = objects.tgs; sourceTree = ""; }; 27916F26287D77650046DB44 /* activity.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = activity.tgs; sourceTree = ""; }; 27916F27287D77660046DB44 /* animals.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = animals.tgs; sourceTree = ""; }; + 279338862B7E2B070096A292 /* BusinessQuickReplyController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusinessQuickReplyController.swift; sourceTree = ""; }; 2795578D2AB86A5C007EC996 /* libsharpyuv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libsharpyuv.a; path = "core-xprojects/libwebp/build/libwebp/lib/libsharpyuv.a"; sourceTree = ""; }; 2796FEEE273E503100DA653C /* ChatInputSendAsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputSendAsView.swift; sourceTree = ""; }; 2796FEF02742390000DA653C /* EmojiSuggestions */ = {isa = PBXFileReference; lastKnownFileType = folder; name = EmojiSuggestions; path = packages/EmojiSuggestions; sourceTree = ""; }; @@ -3495,6 +3497,7 @@ 27E1F2412B7AD47F00DCF241 /* BusinessAwayMessageController.swift */, 27E1F2432B7B007B00DCF241 /* BusinessLocationController.swift */, 27E1F2452B7B876800DCF241 /* BusinessChatbotController.swift */, + 279338862B7E2B070096A292 /* BusinessQuickReplyController.swift */, ); name = business; sourceTree = ""; @@ -6973,6 +6976,7 @@ 27E0509A2796B6EE00D5AFC3 /* ChatSendAsMenuItem.swift in Sources */, C2271DA01DACC7F7001792B6 /* ChatListMessageRowView.swift in Sources */, 27F67417297682880014DCB9 /* ChatLiveTranslateContext.swift in Sources */, + 279338872B7E2B070096A292 /* BusinessQuickReplyController.swift in Sources */, 27F3BB8E2B4C399400BC37C2 /* PremiumShowStatusController.swift in Sources */, C219E1DB1D8884290042F0C8 /* ChatHistoryEntry.swift in Sources */, 9F0367F0227208E000456348 /* QRCode.swift in Sources */, diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index b938b8380..1da38ad3c 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259658 + 259736 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index f3c0371df..3bde16a2a 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -2391,21 +2391,117 @@ public final class L10n { public static var channelBoostTitleChannel: String { return L10n.tr("Localizable", "ChannelBoost.Title.Channel") } /// Boost Group public static var channelBoostTitleGroup: String { return L10n.tr("Localizable", "ChannelBoost.Title.Group") } - /// Boost the group **%1$@** times to remove slow mode restrictions. Your boosts will help **%2$@** to unlock new features. - public static func channelBoostUnblockSlowmodeGroup(_ p1: String, _ p2: String) -> String { - return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group", p1, p2) + /// %d %@ + public static func channelBoostUnblockSlowmodeGroupCountable(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group_countable", p1, p2) + } + /// Boost the group **%d** times to remove slow mode restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockSlowmodeGroupFew(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group_few", p1, p2) + } + /// Boost the group **%d** times to remove slow mode restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockSlowmodeGroupMany(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group_many", p1, p2) + } + /// Boost the group **%d** time to remove slow mode restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockSlowmodeGroupOne(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group_one", p1, p2) + } + /// Boost the group **%d** times to remove slow mode restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockSlowmodeGroupOther(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group_other", p1, p2) + } + /// Boost the group **%d** times to remove slow mode restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockSlowmodeGroupTwo(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group_two", p1, p2) + } + /// Boost the group **%d** times to remove slow mode restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockSlowmodeGroupZero(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group_zero", p1, p2) + } + /// %d + public static func channelBoostUnblockSlowmodeGroupFullCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group.Full_countable", p1) + } + /// Boost the group **%d** times to remove slow mode restrictions. + public static func channelBoostUnblockSlowmodeGroupFullFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group.Full_few", p1) + } + /// Boost the group **%d** times to remove slow mode restrictions. + public static func channelBoostUnblockSlowmodeGroupFullMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group.Full_many", p1) + } + /// Boost the group **%d** time to remove slow mode restrictions. + public static func channelBoostUnblockSlowmodeGroupFullOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group.Full_one", p1) + } + /// Boost the group **%d** times to remove slow mode restrictions. + public static func channelBoostUnblockSlowmodeGroupFullOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group.Full_other", p1) + } + /// Boost the group **%d** times to remove slow mode restrictions. + public static func channelBoostUnblockSlowmodeGroupFullTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group.Full_two", p1) + } + /// Boost the group **%d** times to remove slow mode restrictions. + public static func channelBoostUnblockSlowmodeGroupFullZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group.Full_zero", p1) + } + /// %d %@ + public static func channelBoostUnblockTextGroupCountable(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group_countable", p1, p2) + } + /// Boost the group **%d** times to remove messaging restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockTextGroupFew(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group_few", p1, p2) + } + /// Boost the group **%d** times to remove messaging restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockTextGroupMany(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group_many", p1, p2) + } + /// Boost the group **%d** times to remove messaging restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockTextGroupOne(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group_one", p1, p2) + } + /// Boost the group **%d** times to remove messaging restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockTextGroupOther(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group_other", p1, p2) + } + /// Boost the group **%d** times to remove messaging restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockTextGroupTwo(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group_two", p1, p2) + } + /// Boost the group **%d** time to remove messaging restrictions. Your boosts will help **%@** to unlock new features. + public static func channelBoostUnblockTextGroupZero(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group_zero", p1, p2) + } + /// %d + public static func channelBoostUnblockTextGroupFullCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group.Full_countable", p1) + } + /// Boost the group **%d** times to remove messaging restrictions. + public static func channelBoostUnblockTextGroupFullFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group.Full_few", p1) + } + /// Boost the group **%d** times to remove messaging restrictions. + public static func channelBoostUnblockTextGroupFullMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group.Full_many", p1) + } + /// Boost the group **%d** time to remove messaging restrictions. + public static func channelBoostUnblockTextGroupFullOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group.Full_one", p1) } - /// Boost the group **%1$@** times to remove slow mode restrictions. - public static func channelBoostUnblockSlowmodeGroupFull(_ p1: String) -> String { - return L10n.tr("Localizable", "ChannelBoost.UnblockSlowmode.Group.Full", p1) + /// Boost the group **%d** times to remove messaging restrictions. + public static func channelBoostUnblockTextGroupFullOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group.Full_other", p1) } - /// Boost the group **%1$@** times to remove messaging restrictions. Your boosts will help **%2$@** to unlock new features. - public static func channelBoostUnblockTextGroup(_ p1: String, _ p2: String) -> String { - return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group", p1, p2) + /// Boost the group **%d** times to remove messaging restrictions. + public static func channelBoostUnblockTextGroupFullTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group.Full_two", p1) } - /// Boost the group **%1$@** times to remove messaging restrictions. - public static func channelBoostUnblockTextGroupFull(_ p1: String) -> String { - return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group.Full", p1) + /// Boost the group **%d** times to remove messaging restrictions. + public static func channelBoostUnblockTextGroupFullZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelBoost.UnblockText.Group.Full_zero", p1) } /// Your channel needs %1$@ to get access to features.\n\nAsk your **Premium** subscribers to boost your channel with this link: public static func channelBoostZeroLevelTextChannel(_ p1: String) -> String { diff --git a/packages/TGUIKit/Sources/TableRowView.swift b/packages/TGUIKit/Sources/TableRowView.swift index 112b1c4b6..c509a36cc 100644 --- a/packages/TGUIKit/Sources/TableRowView.swift +++ b/packages/TGUIKit/Sources/TableRowView.swift @@ -49,7 +49,7 @@ open class TableRowView: NSTableRowView, CALayerDelegate { } - open private(set) weak var item:TableRowItem? + open private(set) var item:TableRowItem? private let menuDisposable = MetaDisposable() // var selected:Bool? diff --git a/packages/TGUIKit/Sources/TableView.swift b/packages/TGUIKit/Sources/TableView.swift index ab9323f24..dc0c9ef29 100644 --- a/packages/TGUIKit/Sources/TableView.swift +++ b/packages/TGUIKit/Sources/TableView.swift @@ -1883,7 +1883,7 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele } public func insert(item:TableRowItem, at:Int = 0, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Bool { - assert(self.item(stableId:item.stableId) == nil, "inserting existing row inTable: \(self.item(stableId:item.stableId)!.className), new: \(item.className), stableId: \(item.stableId)") + // assert(self.item(stableId:item.stableId) == nil, "inserting existing row inTable: \(self.item(stableId:item.stableId)!.className), new: \(item.className), stableId: \(item.stableId)") self.listhash[item.stableId] = item; let at = min(at, list.count) self.list.insert(item, at: at); From 2b006508e81bc0120c97ed19b9f133db4712d673 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 15 Feb 2024 12:39:02 -0400 Subject: [PATCH 11/50] - bugfixes --- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- buildbox/build-mac.sh | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 0fa50f182..6b3475f46 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259736 + 259737 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 1da38ad3c..d9a3cd6ad 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259736 + 259737 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/buildbox/build-mac.sh b/buildbox/build-mac.sh index c4a53d4cb..4cc694742 100644 --- a/buildbox/build-mac.sh +++ b/buildbox/build-mac.sh @@ -30,7 +30,7 @@ cd telegrammacos sh "scripts/configure_frameworks.sh" cp "configurations/${BUILD_CONFIGURATION}.xcconfig" "Telegram-Mac/Release.xcconfig" -xcodebuild -quiet archive -workspace "Telegram-Mac.xcworkspace" \ +xcodebuild archive -workspace "Telegram-Mac.xcworkspace" \ -scheme Release \ -configuration Release \ -archivePath ./ @@ -45,4 +45,4 @@ cp -R "${archive}/Products/Applications/Telegram.app" ${appname} cp -R "${archive}/dSYMs/Telegram.app.dSYM" ${appname}.dSYM -ditto -c -k --sequesterRsrc --keepParent ${appname}.dSYM ${appname}.DSYM.zip \ No newline at end of file +ditto -c -k --sequesterRsrc --keepParent ${appname}.dSYM ${appname}.DSYM.zip From da7e6b883f60ac20441f3a4d7cc444cc58eea429 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 15 Feb 2024 12:43:20 -0400 Subject: [PATCH 12/50] - bugfixes --- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/StoryChatListView.swift | 2 +- TelegramShare/Info.plist | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 6b3475f46..561767031 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259737 + 259738 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/StoryChatListView.swift b/Telegram-Mac/StoryChatListView.swift index c1759292c..2be30a91f 100644 --- a/Telegram-Mac/StoryChatListView.swift +++ b/Telegram-Mac/StoryChatListView.swift @@ -818,7 +818,7 @@ private final class StoryListEntryRowItem : TableRowItem { if !isChannel { items.append(.init(strings().storyListContextViewProfile, handler: { PeerInfoController.push(navigation: context.bindings.rootNavigation(), context: context, peerId: peerId) - }, itemImage: MenuAnimation.menu_open_profile.value)) + }, itemImage: self.entry.item.peer._asPeer().isSupergroup ? MenuAnimation.menu_create_group.value : MenuAnimation.menu_open_profile.value)) } diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index d9a3cd6ad..27de47bbf 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259737 + 259738 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion From dc08cbb0bf1801af28c5cac5b838dbaf6c73bc6c Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 15 Feb 2024 13:22:40 -0400 Subject: [PATCH 13/50] - bugfixes --- Telegram-Mac/Appearance.swift | 1 + .../Contents.json | 22 ++++++++++++++++++ .../ic_pf_archive.png | Bin 0 -> 836 bytes .../ic_pf_archive@2x.png | Bin 0 -> 1469 bytes Telegram-Mac/BusinessLocationController.swift | 6 +---- Telegram-Mac/ChannelInfoEntries.swift | 4 ++++ Telegram-Mac/GroupInfoEntries.swift | 4 ++++ Telegram-Mac/Info.plist | 2 +- Telegram-Mac/LocationModalController.swift | 6 +++++ Telegram-Mac/PeerInfoHeadItem.swift | 8 +++++++ Telegram-Mac/StoryControlsView.swift | 2 +- Telegram-Mac/StoryMediaController.swift | 2 +- Telegram-Mac/en.lproj/Localizable.strings | Bin 835882 -> 835996 bytes TelegramShare/Info.plist | 2 +- .../Sources/Localization/Localizable.swift | 2 ++ .../Sources/TelegramIconsTheme.swift | 16 +++++++++++++ tools/generate-images.swift | 2 +- 17 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Profile_Archive.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Profile_Archive.imageset/ic_pf_archive.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Profile_Archive.imageset/ic_pf_archive@2x.png diff --git a/Telegram-Mac/Appearance.swift b/Telegram-Mac/Appearance.swift index 754fe4507..f6de3e98a 100644 --- a/Telegram-Mac/Appearance.swift +++ b/Telegram-Mac/Appearance.swift @@ -2611,6 +2611,7 @@ private func generateIcons(from palette: ColorPalette, bubbled: Bool) -> Telegra profile_translate: { generateProfileIcon(NSImage(named: "Icon_Profile_Translate")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, profile_join_channel: { generateProfileIcon(NSImage(named: "Icon_Profile_JoinChannel")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, profile_boost: { generateProfileIcon(NSImage(named: "Icon_Profile_Boost")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_archive: { generateProfileIcon(NSImage(resource: .iconProfileArchive).precomposed(palette.accentIcon), backgroundColor: palette.accent) }, stats_boost_boost: { generateProfileIcon(NSImage(named: "Icon_Boost_Boost")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, stats_boost_giveaway: { generateProfileIcon(NSImage(named: "Icon_Boost_Gift")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, stats_boost_info: { generateProfileIcon(NSImage(named: "Icon_Boost_Info")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Archive.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Archive.imageset/Contents.json new file mode 100644 index 000000000..92420db06 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Archive.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_pf_archive.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_pf_archive@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Archive.imageset/ic_pf_archive.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Archive.imageset/ic_pf_archive.png new file mode 100644 index 0000000000000000000000000000000000000000..7a91e1ec1988852a5adf0bf94e868e8b266343ca GIT binary patch literal 836 zcmV-K1H1f*P)3P1f?fPZxC_=dV;t&;F&a0cO^(- zYUGt=Am5BV221uotCe;dN_>J&|&FqI0e7C6#<5|-7lSQR-PV_d1J ztC3sanf~6G)wV0R#-$o;zg>nM(i;-j#eSHhg&qz0V)I5++I)%IJ&I#G#vmp9FOY|o zJM0@ah<$f~;t&t@XFC7DA<-D%tGZtQd=Z%G)C?_<;;<~pACK4m7(f5b>s*yMMb69* z2*vH+$x6$>zLK%lyNCOs1=9yH)ES-6oo!lp$5ynHD(N)gCpPOpn5t_ALQu*Km^PS)$;o6S96>S9|( zjd6I!YjI4maKAKlhit;^hRNn;Qh5wL)3*nxQ|2V@{pO$LdoxG)=(T`{!W)~sc~pAK zh5yX10}t!gExyRv)pq3Ro}+1Onm2HXW84B6c3PhH8b8q-6LO=N7|SA+hlL$rg3owDk6RRUI|82Rh)%9YM~g_fE?lF~ zH)6h1H*kzQ0lBq}jcEUFMYPokJk_u0{w?Hu zn=}cyMP8~tgpd(1)*1Y-Qb4I`n7~v2dK)K?vZ%6J0u=+Om60OYB7~4-vZg! z-IKX$@^!n6!WAymYm}s%fNZj3e4=NEXa++v>60=hU0gsb-WT}WQIsVi5s!+BSWLj? zikDJY#QXv_U%Zq;BIXltf5c14En>c5jO{Q&><_k+v-;qRnzjcRnKt?)O?KCrs) z({P>%E6o9}P@UfViu*jZSW{V23w+nR6t4d9ebQ)`YCvxYEi%la`fh%tT)(kU<^6T#Zc2y zjPH=9c{7p69N=k(nzfr?sr|wcd_cnuK z!!tM0C50D?avoot_y_iZ7wQN;;P4!|7j`pl^Q8k}Tzwa^UKTJ!OKCb@Gs0;*Dx;;PYQ5!i$i)ZLL> zQ070@eH8J5HLgyYW8{YqWUL@Qh*E?kAoEGkQVz;M@?7{1E#=VM0s{=uQkJ{cgsTwn zT)0D%{+4odFbt->UxJpRB&=OJkp7I8q73-X3K%#s<7e;%?O>vyZKJaSvL969gV?9= zg}}kZd@|nx*$-;*R`?SBAe}htr&GyUFd-11oOdOblU9bmgLLFZ&-#qTbkZCBUHD*Y zE8LyqW`$LPOZ7Z`u^7(M4q73ffLu*Ve%vijTiADDQ9v$0lU_5oveBzJE`=5e;jluy9aA9LFTR=*#g>X`_`0sx$Byh2kXU_Qz7i}_0{ZL#8h zY)&BL4?cAH9t z@YU<{Idr6UaEA{&!#H2JCrrMNk-+*z-r=ZNm1$k@3Id+03ao8zH=(VFn Bool { - return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude - } -} + private final class Arguments { let context: AccountContext diff --git a/Telegram-Mac/ChannelInfoEntries.swift b/Telegram-Mac/ChannelInfoEntries.swift index 518f78754..cc8c4591c 100644 --- a/Telegram-Mac/ChannelInfoEntries.swift +++ b/Telegram-Mac/ChannelInfoEntries.swift @@ -238,6 +238,10 @@ class ChannelInfoArguments : PeerInfoArguments { pushViewController(SelectColorController(context: context, peer: peer)) } + func archiveStories() { + self.pushViewController(StoryMediaController(context: context, peerId: peerId, listContext: PeerStoryListContext(account: context.account, peerId: peerId, isArchived: true), standalone: true, isArchived: true)) + } + func openRequests() { pushViewController(RequestJoinMemberListController(context: context, peerId: peerId, manager: requestManager, openInviteLinks: { [weak self] in self?.openInviteLinks() diff --git a/Telegram-Mac/GroupInfoEntries.swift b/Telegram-Mac/GroupInfoEntries.swift index 6d6a6d62e..bcf12d108 100644 --- a/Telegram-Mac/GroupInfoEntries.swift +++ b/Telegram-Mac/GroupInfoEntries.swift @@ -425,6 +425,10 @@ final class GroupInfoArguments : PeerInfoArguments { func stats(_ datacenterId: Int32) { self.pushViewController(GroupStatsViewController(context, peerId: peerId)) } + + func archiveStories() { + self.pushViewController(StoryMediaController(context: context, peerId: peerId, listContext: PeerStoryListContext(account: context.account, peerId: peerId, isArchived: true), standalone: true, isArchived: true)) + } func boosts(_ access: GroupAccess) { let context = self.context diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 561767031..8b957b651 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259738 + 259747 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/LocationModalController.swift b/Telegram-Mac/LocationModalController.swift index 4ac3261ec..c3fd14567 100644 --- a/Telegram-Mac/LocationModalController.swift +++ b/Telegram-Mac/LocationModalController.swift @@ -474,6 +474,12 @@ enum SelectLocationDestination : Equatable { case business(CLLocationCoordinate2D?) } +extension CLLocationCoordinate2D : Equatable { + public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { + return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude + } +} + class LocationModalController: ModalViewController { diff --git a/Telegram-Mac/PeerInfoHeadItem.swift b/Telegram-Mac/PeerInfoHeadItem.swift index a132fa227..6f4b68779 100644 --- a/Telegram-Mac/PeerInfoHeadItem.swift +++ b/Telegram-Mac/PeerInfoHeadItem.swift @@ -283,6 +283,9 @@ private func actionItems(item: PeerInfoHeadItem, width: CGFloat, theme: Telegram items.append(ActionItem(text: strings().peerInfoActionBoostGroup, color: item.accentColor, image: theme.icons.profile_boost, animation: .menu_boost, action: { arguments.boosts(peer.groupAccess) })) + items.append(ActionItem(text: strings().peerInfoActionAcrhivedStories, color: item.accentColor, image: theme.icons.profile_archive, animation: .menu_archive, action: { + arguments.archiveStories() + })) } @@ -381,6 +384,11 @@ private func actionItems(item: PeerInfoHeadItem, width: CGFloat, theme: Telegram })) } } + + items.append(ActionItem(text: strings().peerInfoActionAcrhivedStories, color: item.accentColor, image: theme.icons.profile_archive, animation: .menu_archive, action: { + arguments.archiveStories() + })) + if let address = peer.addressName, !address.isEmpty { items.append(ActionItem(text: strings().peerInfoActionShare, color: item.accentColor, image: theme.icons.profile_share, animation: .menu_share, action: arguments.share)) } diff --git a/Telegram-Mac/StoryControlsView.swift b/Telegram-Mac/StoryControlsView.swift index fa5203cb8..bd7dc1b91 100644 --- a/Telegram-Mac/StoryControlsView.swift +++ b/Telegram-Mac/StoryControlsView.swift @@ -314,7 +314,7 @@ final class StoryControlsView : Control { if let author = story.storyItem.author { - date.append(string: " \(strings().bullet) ", color: color, font: .medium(.small)) + date.append(string: "\(strings().bullet) ", color: color, font: .medium(.small)) let current: AuthorView var isNew = false diff --git a/Telegram-Mac/StoryMediaController.swift b/Telegram-Mac/StoryMediaController.swift index 6747e914a..26f4ff82d 100644 --- a/Telegram-Mac/StoryMediaController.swift +++ b/Telegram-Mac/StoryMediaController.swift @@ -165,7 +165,7 @@ private func entries(_ state: State, arguments: Arguments) -> [Entry] { if standalone { var index = MessageIndex.absoluteUpperBound() - if arguments.isArchive { + if arguments.isArchive, arguments.isMy { entries.insert(.section(index: index), at: 0) index = index.globalPredecessor() entries.insert(.headerText(index: index, stableId: index, text: strings().storyMediaArchiveText, viewType: .singleItem), at: 0) diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index e20467725564308310ea146d628f122e11b12fad..7d67f9764485e0725147dffe52fc8274feba165e 100644 GIT binary patch delta 107 zcmZ40WIU(Yc*82C>1!0ZL?$1Q=VNtbNMl@rKd$Y981D4YJ!f>U~W-!3+` w=?{81gqja1wI5Jo1Y#y2W(HywAZ7((HXvpPVh$kY1Y#~A=H7lli6?6w0PKw>4gdfE delta 70 zcmbQ!Y`m(;c*82C$+uW|CJSh*HTx;I`zbR5F%u9o12GE_vjQ<25VHd@2M}`tF&7YX LZ}(H?@tX$#9C#F+ diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 27de47bbf..dfd776192 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259738 + 259747 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index 3bde16a2a..d645338af 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -10533,6 +10533,8 @@ public final class L10n { public static var peerInfoUsername: String { return L10n.tr("Localizable", "PeerInfo.username") } /// Description public static var peerInfoAboutPlaceholder: String { return L10n.tr("Localizable", "PeerInfo.About.Placeholder") } + /// Acrhived Stories + public static var peerInfoActionAcrhivedStories: String { return L10n.tr("Localizable", "PeerInfo.Action.AcrhivedStories") } /// Add public static var peerInfoActionAddMembers: String { return L10n.tr("Localizable", "PeerInfo.Action.AddMembers") } /// Boost Group diff --git a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift index cb0639afd..acac12b56 100644 --- a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift +++ b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift @@ -6557,6 +6557,19 @@ public final class TelegramIconsTheme { return image } } + public var profile_archive: CGImage { + if let image = cached.with({ $0["profile_archive"] }) { + return image + } else { + let image = _profile_archive() + _ = cached.modify { current in + var current = current + current["profile_archive"] = image + return current + } + return image + } + } public var stats_boost_boost: CGImage { if let image = cached.with({ $0["stats_boost_boost"] }) { return image @@ -10780,6 +10793,7 @@ public final class TelegramIconsTheme { private let _profile_translate: ()->CGImage private let _profile_join_channel: ()->CGImage private let _profile_boost: ()->CGImage + private let _profile_archive: ()->CGImage private let _stats_boost_boost: ()->CGImage private let _stats_boost_giveaway: ()->CGImage private let _stats_boost_info: ()->CGImage @@ -11572,6 +11586,7 @@ public final class TelegramIconsTheme { profile_translate: @escaping()->CGImage, profile_join_channel: @escaping()->CGImage, profile_boost: @escaping()->CGImage, + profile_archive: @escaping()->CGImage, stats_boost_boost: @escaping()->CGImage, stats_boost_giveaway: @escaping()->CGImage, stats_boost_info: @escaping()->CGImage, @@ -12363,6 +12378,7 @@ public final class TelegramIconsTheme { self._profile_translate = profile_translate self._profile_join_channel = profile_join_channel self._profile_boost = profile_boost + self._profile_archive = profile_archive self._stats_boost_boost = stats_boost_boost self._stats_boost_giveaway = stats_boost_giveaway self._stats_boost_info = stats_boost_info diff --git a/tools/generate-images.swift b/tools/generate-images.swift index f96783b3a..65487e40c 100644 --- a/tools/generate-images.swift +++ b/tools/generate-images.swift @@ -528,7 +528,7 @@ func initialize() -> [String] { array.append("profile_translate") array.append("profile_join_channel") array.append("profile_boost") - + array.append("profile_archive") array.append("stats_boost_boost") array.append("stats_boost_giveaway") From ce85c3a5091323a329906dbb53459cc57ee8f510 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 15 Feb 2024 14:31:32 -0400 Subject: [PATCH 14/50] - bugfixes --- .../BusinessQuickReplyController.swift | 124 +++++++++++++----- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- packages/TGUIKit/Sources/TableView.swift | 18 +-- 4 files changed, 105 insertions(+), 41 deletions(-) diff --git a/Telegram-Mac/BusinessQuickReplyController.swift b/Telegram-Mac/BusinessQuickReplyController.swift index bcc5c3d80..511576732 100644 --- a/Telegram-Mac/BusinessQuickReplyController.swift +++ b/Telegram-Mac/BusinessQuickReplyController.swift @@ -52,7 +52,7 @@ private final class QuickReplyRowItem : GeneralRowItem { var width = blockWidth width -= (leftInset + viewType.innerInset.left) - width -= 30 // photo + width -= 40 // photo width -= viewType.innerInset.left // photo @@ -92,7 +92,7 @@ private final class QuickReplyRowItemView: GeneralContainableRowView { private let container = View() private var remove: ImageButton? - + private var sort: ImageButton? required init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(container) @@ -101,6 +101,9 @@ private final class QuickReplyRowItemView: GeneralContainableRowView { imageView.layer?.backgroundColor = theme.colors.grayIcon.cgColor imageView.layer?.cornerRadius = imageView.frame.height / 2 + + textView.userInteractionEnabled = false + textView.isSelectable = false } required init?(coder: NSCoder) { @@ -117,37 +120,88 @@ private final class QuickReplyRowItemView: GeneralContainableRowView { textView.update(item.textLayout) if item.editing { - let current: ImageButton - var isNew = false - if let view = self.remove { - current = view - } else { - current = ImageButton() - current.scaleOnClick = true - current.autohighlight = false - addSubview(current) - self.remove = current + if "".isEmpty { + let current: ImageButton + var isNew = false + if let view = self.remove { + current = view + } else { + current = ImageButton() + current.scaleOnClick = true + current.autohighlight = false + addSubview(current) + self.remove = current + + current.set(handler: { [weak self] _ in + if let item = self?.item as? QuickReplyRowItem { + item.remove(item.reply) + } + }, for: .Click) + + isNew = true + } + current.set(image: theme.icons.deleteItem, for: .Normal) + current.sizeToFit(.zero, NSMakeSize(24, 24), thatFit: true) - current.set(handler: { [weak self] _ in - if let item = self?.item as? QuickReplyRowItem { - item.remove(item.reply) + if isNew { + current.centerY(x: item.leftInset) + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) } - }, for: .Click) - - isNew = true + } } - current.set(image: theme.icons.deleteItem, for: .Normal) - current.sizeToFit(.zero, NSMakeSize(24, 24), thatFit: true) - - if isNew { - current.centerY(x: item.leftInset) - if animated { - current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + if "".isEmpty { + let current: ImageButton + var isNew = false + if let view = self.sort { + current = view + } else { + current = ImageButton() + current.scaleOnClick = true + current.autohighlight = false + addSubview(current) + self.sort = current + + + current.set(handler: { [weak self] _ in + if let event = NSApp.currentEvent { + self?.mouseDown(with: event) + } + }, for: .Down) + + current.set(handler: { [weak self] _ in + if let event = NSApp.currentEvent { + self?.mouseDragged(with: event) + } + }, for: .MouseDragging) + + current.set(handler: { [weak self] _ in + if let event = NSApp.currentEvent { + self?.mouseUp(with: event) + } + }, for: .Up) + + isNew = true } + current.set(image: theme.icons.resort, for: .Normal) + current.sizeToFit(.zero, NSMakeSize(24, 24), thatFit: true) + + if isNew { + current.centerY(x: containerView.frame.width - current.frame.width - item.viewType.innerInset.right) + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + } + } else { + if let view = remove { + performSubviewRemoval(view, animated: animated) + self.remove = nil + } + if let view = sort { + performSubviewRemoval(view, animated: animated) + self.sort = nil } - } else if let view = remove { - performSubviewRemoval(view, animated: animated) - self.remove = nil } self.updateLayout(size: frame.size, transition: animated ? .animated(duration: 0.2, curve: .easeOut) : .immediate) @@ -170,6 +224,11 @@ private final class QuickReplyRowItemView: GeneralContainableRowView { transition.updateFrame(view: container, frame: containerRect) + + if let sort = sort { + transition.updateFrame(view: sort, frame: sort.centerFrameY(x: containerView.frame.width - sort.frame.width - item.viewType.innerInset.right)) + } + transition.updateFrame(view: imageView, frame: imageView.centerFrameY(x: 0)) transition.updateFrame(view: textView, frame: textView.centerFrameY(x: imageView.frame.maxX + 10)) } @@ -282,6 +341,9 @@ func BusinessQuickReplyController(context: AccountContext, peerId: PeerId) -> In let updateState: ((State) -> State) -> Void = { f in statePromise.set(stateValue.modify (f)) } + + let nextTransactionNonAnimated = Atomic(value: false) + let arguments = Arguments(context: context, add: { showModal(with: BusinessAddQuickReply(context: context, stateSignal: statePromise.get(), stateValue: stateValue, updateState: updateState, reply: nil), for: context.window) @@ -297,8 +359,8 @@ func BusinessQuickReplyController(context: AccountContext, peerId: PeerId) -> In showModal(with: BusinessAddQuickReply(context: context, stateSignal: statePromise.get(), stateValue: stateValue, updateState: updateState, reply: reply), for: context.window) }) - let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in - return InputDataSignalValue(entries: entries(state, arguments: arguments)) + let signal = statePromise.get() |> deliverOnMainQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments), animated: !nextTransactionNonAnimated.swap(false)) } let controller = InputDataController(dataSignal: signal, title: "Quick Replies", removeAfterDisappear: false) @@ -347,7 +409,7 @@ func BusinessQuickReplyController(context: AccountContext, peerId: PeerId) -> In let toValue = to - range.location var replies = stateValue.with { $0.replies } replies.move(at: fromValue, to: toValue) -// _ = nextTransactionNonAnimated.swap(true) + _ = nextTransactionNonAnimated.swap(true) updateState { current in var current = current current.replies = replies diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 8b957b651..7903a1b28 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259747 + 259759 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index dfd776192..a281c6a28 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259747 + 259759 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/TGUIKit/Sources/TableView.swift b/packages/TGUIKit/Sources/TableView.swift index dc0c9ef29..cdaeb0365 100644 --- a/packages/TGUIKit/Sources/TableView.swift +++ b/packages/TGUIKit/Sources/TableView.swift @@ -1822,8 +1822,7 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele private func moveHole(at fromIndex: Int, to toIndex: Int, animated: Bool) { var y: CGFloat = 0 - - + guard let controller = resortController, let resortRow = controller.resortRow, let resortView = controller.resortView else {return} if controller.resortRange.location == NSNotFound { self.stopResorting() @@ -1868,12 +1867,15 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele guard let controller = resortController else {return} - let row = min(max(tableView.row(at: point), controller.resortRange.location), controller.resortRange.max - 1) - controller.prevHoleIndex = controller.currentHoleIndex - controller.currentHoleIndex = row - if controller.prevHoleIndex != controller.currentHoleIndex { - moveHole(at: controller.prevHoleIndex!, to: controller.currentHoleIndex!, animated: true) - controller.updateItems(controller.resortView, self.list.filter { controller.canResort($0.index) }) + let row = tableView.row(at: point) + if row != -1 { + let row = min(max(tableView.row(at: point), controller.resortRange.location), controller.resortRange.max - 1) + controller.prevHoleIndex = controller.currentHoleIndex + controller.currentHoleIndex = row + if controller.prevHoleIndex != controller.currentHoleIndex { + moveHole(at: controller.prevHoleIndex!, to: controller.currentHoleIndex!, animated: true) + controller.updateItems(controller.resortView, self.list.filter { controller.canResort($0.index) }) + } } } From 9220a2804a66a1b3c26b227b13215eecf1285f8f Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 15 Feb 2024 14:32:18 -0400 Subject: [PATCH 15/50] - bugfixes --- Telegram.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Telegram.xcodeproj/project.pbxproj b/Telegram.xcodeproj/project.pbxproj index a70493743..533e2d9e7 100644 --- a/Telegram.xcodeproj/project.pbxproj +++ b/Telegram.xcodeproj/project.pbxproj @@ -7825,7 +7825,7 @@ "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.7.1; + MARKETING_VERSION = 10.8; ONLY_ACTIVE_ARCH = NO; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = ( @@ -7877,7 +7877,7 @@ "$(PROJECT_DIR)/core-xprojects/openssl/build/openssl/lib", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.7.1; + MARKETING_VERSION = 10.8; ONLY_ACTIVE_ARCH = YES; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/macosx/libswiftAppKit.dylib"; @@ -7924,7 +7924,7 @@ "$(PROJECT_DIR)/core-xprojects/openssl/build/openssl/lib", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.7.1; + MARKETING_VERSION = 10.8; ONLY_ACTIVE_ARCH = NO; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/macosx/libswiftAppKit.dylib"; @@ -8041,7 +8041,7 @@ "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.7.1; + MARKETING_VERSION = 10.8; ONLY_ACTIVE_ARCH = YES; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = ( From 5239121ef8c329049d1c42c5680662722a0d371e Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 16 Feb 2024 12:08:25 -0400 Subject: [PATCH 16/50] - bugfixes --- Telegram-Mac/AccountViewController.swift | 24 ++- Telegram-Mac/Appearance.swift | 1 + Telegram-Mac/ApplicationContext.swift | 18 +- .../Contents.json | 22 +++ .../bubble_24.png | Bin 0 -> 456 bytes .../bubble_24@2x.png | Bin 0 -> 807 bytes .../Contents.json | 22 +++ .../bot_24.png | Bin 0 -> 448 bytes .../bot_24@2x.png | Bin 0 -> 783 bytes .../Contents.json | 22 +++ .../hand_24.png | Bin 0 -> 442 bytes .../hand_24@2x.png | Bin 0 -> 899 bytes .../Contents.json | 22 +++ .../clock_24.png | Bin 0 -> 375 bytes .../clock_24@2x.png | Bin 0 -> 674 bytes .../Contents.json | 22 +++ .../location_24.png | Bin 0 -> 373 bytes .../location_24@2x.png | Bin 0 -> 710 bytes .../Contents.json | 22 +++ .../reply_24.png | Bin 0 -> 364 bytes .../reply_24@2x.png | Bin 0 -> 631 bytes .../Contents.json | 22 +++ .../business_24 (2).png | Bin 0 -> 1190 bytes .../business_24@2x (2).png | Bin 0 -> 3095 bytes .../AvatarStoryIndicatorComponent.swift | 28 ++- Telegram-Mac/BusinessHoursController.swift | 75 +++++--- Telegram-Mac/CallScreenController.swift | 175 ++++++++++++++++++ Telegram-Mac/CallWindowController.swift | 13 ++ Telegram-Mac/ChatListRowItem.swift | 5 +- Telegram-Mac/ChatSearchHeader.swift | 6 +- Telegram-Mac/ChatTitleBarView.swift | 4 +- Telegram-Mac/EmojiesController.swift | 2 +- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/InlineStickerItemLayer.swift | 5 + Telegram-Mac/PeerInfoHeadItem.swift | 2 +- Telegram-Mac/StoryChatListView.swift | 8 +- Telegram-Mac/en.lproj/Localizable.strings | Bin 835996 -> 836108 bytes Telegram.xcodeproj/project.pbxproj | 4 + TelegramShare/Info.plist | 2 +- .../Sources/Localization/Localizable.swift | 2 + .../Sources/PeerCallAction.swift | 14 +- .../Sources/PeerCallArguments.swift | 16 +- .../Sources/PeerCallPhotoView.swift | 1 + .../PeerCallRevealedSecretKeyView.swift | 2 +- .../Sources/PeerCallScreen.swift | 42 ++--- .../Sources/PeerCallScreenView.swift | 94 ++++++++-- .../Sources/PeerCallStatusView.swift | 42 +++-- .../Sources/PeerCallTooltipStatusView.swift | 150 +++++++++++++++ .../ic_microphoneoff.imageset/Contents.json | 22 +++ .../ic_call_microphoneoff.png | Bin 0 -> 472 bytes .../ic_call_microphoneoff@2x.png | Bin 0 -> 855 bytes .../PrivateCallScreen/Sources/State.swift | 17 +- packages/TGUIKit/Sources/Extensions.swift | 2 +- packages/TGUIKit/Sources/TGFont.swift | 25 ++- packages/TGUIKit/Sources/Window.swift | 12 +- .../Sources/TelegramIconsTheme.swift | 16 ++ submodules/telegram-ios | 2 +- tools/generate-images.swift | 1 + 58 files changed, 837 insertions(+), 151 deletions(-) create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Away.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Away.imageset/bubble_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Away.imageset/bubble_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Bot.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Bot.imageset/bot_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Bot.imageset/bot_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Greeting.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Greeting.imageset/hand_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Greeting.imageset/hand_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Hours.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Hours.imageset/clock_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Hours.imageset/clock_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Location.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Location.imageset/location_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Location.imageset/location_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_QuickReply.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_QuickReply.imageset/reply_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_QuickReply.imageset/reply_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_SettingsBusiness.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_SettingsBusiness.imageset/business_24 (2).png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_SettingsBusiness.imageset/business_24@2x (2).png create mode 100644 Telegram-Mac/CallScreenController.swift create mode 100644 packages/PrivateCallScreen/Sources/PeerCallTooltipStatusView.swift create mode 100644 packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_microphoneoff.imageset/Contents.json create mode 100644 packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_microphoneoff.imageset/ic_call_microphoneoff.png create mode 100644 packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_microphoneoff.imageset/ic_call_microphoneoff@2x.png diff --git a/Telegram-Mac/AccountViewController.swift b/Telegram-Mac/AccountViewController.swift index a622840df..347a155ec 100644 --- a/Telegram-Mac/AccountViewController.swift +++ b/Telegram-Mac/AccountViewController.swift @@ -141,6 +141,7 @@ private enum AccountInfoEntry : TableItemListNodeEntry { case update(index: Int, viewType: GeneralViewType, state: AnyUpdateStateEquatable) case filters(index: Int, viewType: GeneralViewType) case premium(index: Int, viewType: GeneralViewType) + case business(index: Int, viewType: GeneralViewType) case giftPremium(index: Int, viewType: GeneralViewType) case about(index: Int, viewType: GeneralViewType) case faq(index: Int, viewType: GeneralViewType) @@ -190,16 +191,18 @@ private enum AccountInfoEntry : TableItemListNodeEntry { return .index(17) case .premium: return .index(18) - case .giftPremium: + case .business: return .index(19) - case .faq: + case .giftPremium: return .index(20) - case .ask: + case .faq: return .index(21) - case .about: + case .ask: return .index(22) + case .about: + return .index(23) case let .attach(index, _, _): - return .index(23 + index) + return .index(24 + index) case let .whiteSpace(index, _): return .index(1000 + index) } @@ -249,6 +252,8 @@ private enum AccountInfoEntry : TableItemListNodeEntry { return index case let .premium(index, _): return index + case let .business(index, _): + return index case let .giftPremium(index, _): return index case let .faq(index, _): @@ -399,6 +404,11 @@ private enum AccountInfoEntry : TableItemListNodeEntry { return GeneralInteractedRowItem(initialSize, stableId: stableId, name: strings().accountSettingsPremium, icon: theme.icons.settingsPremium, activeIcon: theme.icons.settingsPremium, type: .next, viewType: viewType, action: { arguments.openPremium() }, border:[BorderType.Right], inset:NSEdgeInsets(left: 12, right: 12)) + case let .business(_, viewType): + //TODO LANG + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "Telegram Business", icon: theme.icons.settingsBusiness, activeIcon: theme.icons.settingsBusiness, type: .next, viewType: viewType, action: { + arguments.openPremium() + }, border:[BorderType.Right], inset:NSEdgeInsets(left: 12, right: 12)) case let .giftPremium(_, viewType): return GeneralInteractedRowItem(initialSize, stableId: stableId, name: strings().accountSettingsGiftPremium, icon: theme.icons.settingsGiftPremium, activeIcon: theme.icons.settingsGiftPremium, type: .next, viewType: viewType, action: arguments.giftPremium, border:[BorderType.Right], inset:NSEdgeInsets(left: 12, right: 12)) case let .faq(_, viewType): @@ -607,6 +617,10 @@ private func accountInfoEntries(peerView:PeerView, context: AccountContext, acco if !context.premiumIsBlocked { entries.append(.premium(index: index, viewType: .singleItem)) index += 1 + #if DEBUG + entries.append(.business(index: index, viewType: .singleItem)) + index += 1 + #endif entries.append(.giftPremium(index: index, viewType: .singleItem)) index += 1 diff --git a/Telegram-Mac/Appearance.swift b/Telegram-Mac/Appearance.swift index f6de3e98a..c7e8d0bc0 100644 --- a/Telegram-Mac/Appearance.swift +++ b/Telegram-Mac/Appearance.swift @@ -2361,6 +2361,7 @@ private func generateIcons(from palette: ColorPalette, bubbled: Bool) -> Telegra settingsUpdateActive: { generateSettingsActiveIcon(NSImage(named: "Icon_SettingsUpdate")!.precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, settingsFiltersActive: { generateSettingsActiveIcon(NSImage(named: "Icon_SettingsFilters")!.precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, settingsProfile: { generateSettingsIcon(NSImage(named: "Icon_SettingsProfile")!.precomposed(flipVertical: true)) }, + settingsBusiness: { generateSettingsIcon(NSImage(resource: .iconSettingsBusiness).precomposed(flipVertical: true)) }, generalCheck: { #imageLiteral(resourceName: "Icon_Check").precomposed(palette.accentIcon) }, settingsAbout: { #imageLiteral(resourceName: "Icon_SettingsAbout").precomposed(palette.accentIcon) }, settingsLogout: { #imageLiteral(resourceName: "Icon_SettingsLogout").precomposed(palette.redUI) }, diff --git a/Telegram-Mac/ApplicationContext.swift b/Telegram-Mac/ApplicationContext.swift index 553a4f87f..84fba7e47 100644 --- a/Telegram-Mac/ApplicationContext.swift +++ b/Telegram-Mac/ApplicationContext.swift @@ -524,17 +524,17 @@ final class AuthorizedApplicationContext: NSObject, SplitViewDelegate { #if DEBUG - var peerCall: PeerCallScreen? + // var peerCall: PeerCallScreen? self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in - peerCall = PeerCallScreen(external: PeerCallArguments(engine: context.engine, peerId: context.peerId, makeAvatar: { view, peer in - let control = view as? AvatarControl ?? AvatarControl(font: .avatar(17)) - control.setFrameSize(NSMakeSize(120, 120)) - control.userInteractionEnabled = false - control.setPeer(account: context.account, peer: peer) - return control - })) - peerCall?.show() +// peerCall = PeerCallScreen(external: PeerCallArguments(engine: context.engine, peerId: context.peerId, makeAvatar: { view, peer in +// let control = view as? AvatarControl ?? AvatarControl(font: .avatar(17)) +// control.setFrameSize(NSMakeSize(120, 120)) +// control.userInteractionEnabled = false +// control.setPeer(account: context.account, peer: peer) +// return control +// })) +// peerCall?.show() // context.bindings.rootNavigation().push(ChatListController(context, modal: false, mode: .savedMessagesChats)) //setCustomAppIcon(fromPath: "/Users/mikerenoir/Downloads/AppIcon.icns") return .invoked diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Away.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Away.imageset/Contents.json new file mode 100644 index 000000000..487084077 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Away.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "bubble_24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bubble_24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Away.imageset/bubble_24.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Away.imageset/bubble_24.png new file mode 100644 index 0000000000000000000000000000000000000000..e99e4a6794c2298c406c15ac6359beac85757e68 GIT binary patch literal 456 zcmV;(0XP1MP)l$F>VR~BOGpQ}1E2y-1%wJ5I!Ff^D#%cQLk00KUvM`*5lQG3C)t_RDAH-~ zbW%RNfp%Ja}Vl%R7AyiTPf&HTOtt zg?r)_Y3P7nQEEC7uQCpnB@U{I;t(50ZzdfZGpo-z&fFQ>$jtKg>V!G zU&I%hHFzgc1M;+`% zs1ystOHQ)nmR!vT2`E}BcKBZod?=x*rX2At#w(l(V#ke+cyGn1#`FhC^Z(!@;CYY; yw-SKDG`w?D;c`#r9~3V9=o>UR{j;Tob@0C*;38&K!o+z10000j z+b|4$?s))r19XD46O^5x?FQZ;-oWkz$p&&KNH78u6LaBX;JNS1JprJ)H|^f=1WYcvOV+dr ziQmpZXoy>%`1=`(_3_O}5a#JNL+(Vb0o9Df`dG8@g|*~}+r-DE`B5l~Gp=#G#me%t zJ(j|HX&cRjHvEky={yI@a(@a<&>-Z26Kqz(Iv{TZ*J#`~>p4hmAedAluuIf%Ofi7V zmvFl-C{asv1;ThBL>jTMClYXU`GVIoLkiThqPL&IxH@a}%LUROryt zNi8&hkOoQnVgrPm%`~p6e{W##QuByqJ~NoYR&ohj;dW zLK$fO*&}}F`V!AWLOZ#9Fym3AHB3e&yOTyrAhp*=wcZTQQcbG`2<|9&9`pW2hD z?qCip*Al{wzvN@YJ_}RQyU5zjNTiQD1i;qa?574T2x<9YEY|QOKi{U6YluR#qG~_6 l0p90x8zyJ;Jq9+u{sDc!i_n!ta}EFi002ovPDHLkV1n6)Z;=21 literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Bot.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Bot.imageset/Contents.json new file mode 100644 index 000000000..85f9aa3e5 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Bot.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "bot_24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "bot_24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Bot.imageset/bot_24.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Bot.imageset/bot_24.png new file mode 100644 index 0000000000000000000000000000000000000000..f95bc89dd1a73a169734fd5b4ff4482298879218 GIT binary patch literal 448 zcmV;x0YCnUP)?RukXb=e#VJ6Il=RoFMYjFW)qEnxkaRugd?uta$Ovz##K7^mbn-Oz$%fGaf4Mb83 z(@x3}gBr?QeS0UVZB3`Ka8FtaD&Gdydn1S$ZRQb=m}Q@jUc)OR-w#Bn0=p>!mRnFe zN*&vi@P+*yMMepfXwD=SZXGc=wl#TXhTz(=yPYevaf_-HbB-gniEx9ak`m5yq2mYP z5!J!}6h~K}a3;HBgZ5q7I{g#=;#YhHKaPxQ(_?=Z9^k={0O;U=iMYbT=w^_xH3)|m z#2s7X8u~6!2n+%*nnf5M`vi203ba$IOUz;%?onOJODOcwx_L7*(<(Y)IXvq_Pdx`_ qkG@qX1`B`mYgqjMwG{!^pT{?C%nl1J_MXfVlI^6x@e!lTh1BwvBqMVll| zn<4Sr1qdDD)+YX8fnsg!^aR5)!^Ra(OgB(2SgegT3178N1k&SAO9h7X_((qm(zeRR zB9`V9E?FU6xW0{reS(5SD{d@UYb3Vb5{RF$>b7Z0-u9g1#Q8 zj6CW-{T}@xMCFjt8vUV15bd!N*Qnvy14nJU!MF$O$5ueqbB@Nv3FGgZUX1A z5F0|X6L{@dob$j?2vb3l}rYmeBWZ zlwn%<>hVfkVp$n$MWz@6WJNJi!;68P%C1$h)-%GbP&zSQg^~|<1hILn*|FtX?@|nTaZW_ zcNhSt1H8}YHbnZ|rh;Sj;valwVMNviEW7{! N002ovPDHLkV1lmuVXOcE literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Greeting.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Greeting.imageset/Contents.json new file mode 100644 index 000000000..00524f58b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Greeting.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "hand_24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "hand_24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Greeting.imageset/hand_24.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Greeting.imageset/hand_24.png new file mode 100644 index 0000000000000000000000000000000000000000..b82cf1e5f33c18240037212aeda8d6c0a7b96642 GIT binary patch literal 442 zcmV;r0Y(0aP)07!$q?2AT9EE=7NivSrq8FNXK!*aTB>1qfw}g zyJRP)NF>BoxX>aeG(o6v5IQ0u5;|1ec|aJYMt*b;!N?Cyk;v8YjAxuVPYH*s0vBO* zOgND+SjO3iYgLQfiiDnp6rN%6cmgO?Zd1=e6g7wrE+HVHw+MvwAmpK56^M6J2|88d zt&l`mMnNb;+&=LZZ7P ki&M&y!Rwzb9mI~h0le&GDJHe5&;S4c07*qoM6N<$f_nk7EdT%j literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Greeting.imageset/hand_24@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Greeting.imageset/hand_24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3872a07137ef052255f5e19a36028ddadd59475f GIT binary patch literal 899 zcmV-}1AP36P)JZ*BJ83&1Ry)-(g9dT(5A|v~c;fe<6BTs59fp!9DfmKGlLb(ls zv>z5-%t^`;NZ8?RWKjOG{*p z^jX;Bvdnfl3{(@DtgX1YwMr}22F7V{i5{6sC}cu8;^V-3M@)g2D8bOiGCO%oz+i|u)*XRa6QHlI4`LuM?6$Q=GB@$Yfn4wZO3GT4sH{>fQb+qL%h$Z z08c#20CD3POyX3-bMUYl&Zz@!>f8p#j0wkJBJY?~vfkE#Db z+6YZi24c&_g&NZjYGs6;Xal2(1yyg{X!>s(|no>FZF;hK_pVe{N}Gi{lIHmqGX(*WHq=HNT7p0`L4K zAEDG*C_(QFZ+S-|eUz90xqG)iH5egK{ylFDdw90w8K#ZYIPMmYxb4qvi1c%tH3F|{ Z{{cuLg?|iFycGZd002ovPDHLkV1m~$iW&d_ literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Hours.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Hours.imageset/Contents.json new file mode 100644 index 000000000..e2b3e15fa --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Hours.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "clock_24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "clock_24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Hours.imageset/clock_24.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Hours.imageset/clock_24.png new file mode 100644 index 0000000000000000000000000000000000000000..9a872cf996d3182a6031b738e50e15135b2f5d5a GIT binary patch literal 375 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O(AJf1F&Ar*{U!!`;YGT`~s+-9VlGNtDL$2|r! z1s?`!hRHK{6PSZ8wNB$oDLC*%#GCKr{o2JI|I?$VG1p8K40rL~BOu?o=SLJXTc!WS z8If{QJeqQ|QiAuZG(UW}?awmAf*^4;6PegD@ zc|MAMq<67}w@8Wa_X+dUnZ8z&_&tu#;x1xa7v1M^{NnX4Hql2XliQpj+0T7TNfzuS#ST+ zBv@w1eRRX*Nr&E9RbD$a>w4t0Q%~$n&M6$O@PEc`=~nkeTH??D+d)ba=Kk8Rtqw_h S$DIa-6N9I#pUXO@geCwDv5!9h literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Hours.imageset/clock_24@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Hours.imageset/clock_24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..705ed360c09d0677e1069de3e3356244c58126db GIT binary patch literal 674 zcmV;T0$u%yP)n zfKna#WD$oujm0$;Xso?BDtpHMic97g6Bo`B4_dW>o-iR+*?q+0h z**jFl-7GH zP+LSo=mKUVyBSAfg8RtL3Oc|Y8Qe@9nA+H7oAW>x%EVltiyfH24;lCmg)4zBcA$no zGHbk|BQ6){Vh0ix!k8%P<9|>>O`>3p_{bsCBns}!U0=AY&x%jrwv6hW;nma^ za5oVmoqRS_3LgUvbGR>TY0tSU1HOaUMyLSwEiEPi=hU>g8|qm<#n0*kH6tZ{H$h8; zzxk`4!DTIGEKF&GgD$IY!*VY?H*Yki^~pE)+&2^^zR5?J(!J1Hf3gvYY-5HV_-r`D zP8)XRSMcBd2}zBXYCl82kp$NQ)coWI80P0TL@HD}bgT+~0VM`-WWXzbz5oCK07*qo IM6N<$g2{9bn*aa+ literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Location.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Location.imageset/Contents.json new file mode 100644 index 000000000..240ecc082 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Location.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "location_24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Location.imageset/location_24.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Location.imageset/location_24.png new file mode 100644 index 0000000000000000000000000000000000000000..f29318b5bdd7785c3055ea46f7e70f95ed5e2442 GIT binary patch literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O(AT%InDAr*{U!*25)GT_;g(AXznrXiiuvdM8q zOHPp4NsgWgf~WN3A1n}4x?buNyh>C$}J4>CMUR!6qG;xRDLY+x}kBU$ka1F zc^j5iFjWgTNvYdN$j@qYt_cUBi)i7k9M9^$k5^Y z;-c~OilX(gBiALixTs9JG&|?aHHjxLW)!+V+hApVVPWFEyN;Y^nbetYEt1g<3Ar#a zhyR4|ryd(4wr&CbP9a7uv67%p3sH*jD9a?G59SH z8=if-KGm~P3UPM7J(5Du95e11BT^>m2L!1Dsf*y5(4{@MY zFA&l;)x?3UN2F6HdPoXp%^-&q>Q64<4RTtA`jZQg4yC11-72lXGMJ80s% s#Gtj<1){>u&FVNe*w9ZC+;9c+mRO#2V*0Ec}MN!5yy;Z-}gDQm@Q6 z^0+4Qx6yxb&eSKz&a|dI+*@&Q6U!m5X(D%6Uj{hzo4gg6=-6tqGQdHr#oR@nYerLn zrpnKT`?ANfk{RCI;#@H2=dlBBbCVd~eC9b27kuFU8`lE~LHa%mR+`rQ@RE06cM4kb zGxmz&d@Hs)6aB;Gc5JgewIp|v!?XT>8lC^^qea#@|6qH_Yb9w|nEVVFI1HYyelF{r G5}E-2>x)GI literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_QuickReply.imageset/reply_24@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_QuickReply.imageset/reply_24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cf559ce4ecc07c87d9dff27521a6c0bfdc4a3a2d GIT binary patch literal 631 zcmV--0*L*IP)n+6~GEo)gdw93gJdb^>sMx&h`LaF;_u0wmAmzx#pfk@S6# zki0;KdEF})$~$Gz8`|Et8f|}3zAArdzdK7POXW5aS6Fb-_R2Tl--8~TVmjD@KSmI! z%|}!4`v`+I`OlpYh_l(uH7B71R1pVjvZTPXo}@;adbm@Y9OfSSE<`$^ zOB-#H{r(5t+=&)zFK|mYbFKWW+Lf+0)Lr-Gb4z)bX0#fBY@1@Z)FEd;T&5VC00szw z^#J-E9oJJjl-mq2Gzqa0V5**i3%~@irXD?2eR2V)Jp?}mv1Pgb)?fX1hyU{mpjX^7 zxTdAW2L2_$qlN&!k*=-;z_AD$N*9_+&_HxUntB`#BzQWVPggpLK;kd?NQa069^3fB zyNoDElS^1cbjf~dFi2qiLQ@D!7zW1-c90z1q-yx&2H^SJhD;gU30+1vsb5e<6jtgi RU8w*7002ovPDHLkV1kO)2!H?p literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsBusiness.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SettingsBusiness.imageset/Contents.json new file mode 100644 index 000000000..43e895550 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SettingsBusiness.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "business_24 (2).png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "business_24@2x (2).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsBusiness.imageset/business_24 (2).png b/Telegram-Mac/Assets.xcassets/Icon_SettingsBusiness.imageset/business_24 (2).png new file mode 100644 index 0000000000000000000000000000000000000000..51a9a1fcad2f95289190f1872bd48e2d74115f55 GIT binary patch literal 1190 zcmV;X1X=ruP);4CYKk_yq~mL%eiT@1biDY zYYbz}=#w;R5(HyO=uM*JVXmPQYdSzZeDm`c_6J;b@Vf_XnvafgaG9W4RJ}Kj>(@dR zHmn6Tix5iTf$M6ngsMPTZo>Ml{pY(P;LcZPCIHvQHN^#J+W2LHxCIqBS<$#y5*OC_ zyjY~k0BhQfNAqd?W&4XQ$qC@F#_++8D8XbFvOLbxLxo1T9RR~ImE_2<7XXR|cbY&e zp27(TR$adA<4(l(W)8XB(Bd*5hq&?bgI9i%r)Xcl_2^Y}2g+t@NfUYpS&cBLg>#L1 z2g)$&fi3&{lOOLKbtSk07mXmdah;(aT-8*UP&Z;^bt~L?K+;G(+ZZG*JF1xj;6euv z+?JR-m@0GRiwk7`*HWD?c|OhY;aISiHMVf!o=RRWUg#OKHmdZ%Bb0LKJjMCO^!^*2 zT)%ZkTc_)=^caF#I-7cjAsIz`jhgRG@89aA^5W|4tMk*HaQ5mT zGM0R(VSczc1l_sLgdjN1sutnyim}Lk2=fzncg6F2+6#FgR%;TYKz^h^u8$l*JpoJy zA7@w+l*~rvL!fwlBHPEVxs%lSQ1}$-sX`X>bj4Nk>hfeX%j5iWFa0EdG{Q0_&O}IY zu}}c_G|iUea#kv@uNg~;E5h`rIRvLxLnQ*dzPSJP3>Z^+%J#$UBlko0LPZ(THKB`~ z#j3?ma0=jq$^U3{DGH!#PR9-wcpWk;wU9r)a?RGDY$#HI<1GJP5AfDGgHzHKEJ2hX=s9|PAwW%IYYAGd`t(Q_#=h@9u(eX5nzgDY{8EUh%g z%&HBU2Zlg^rEhU=%>#r=u1o@dU}P=0+j@V17=6ZvX+nYIhC%*-kUY! z@j4`TWN6@v94b?-FPhrG&Rz?UmkL`5uVrRYc0xMWQNJ-5vuGFZeYm9>Hd)+;zb)l~ zum2{7$DUQF{>$*cGAs}*d=|Uj^rU}wX?ME&I(a1j05d|Dst2Qj*#H0l07*qoM6N<$ Ef;nk7Hvj+t literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsBusiness.imageset/business_24@2x (2).png b/Telegram-Mac/Assets.xcassets/Icon_SettingsBusiness.imageset/business_24@2x (2).png new file mode 100644 index 0000000000000000000000000000000000000000..ee5131be965b317d5a4aa8b02feab99cb5fc859f GIT binary patch literal 3095 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 literal 0 HcmV?d00001 diff --git a/Telegram-Mac/AvatarStoryIndicatorComponent.swift b/Telegram-Mac/AvatarStoryIndicatorComponent.swift index 042546f53..0aab30369 100644 --- a/Telegram-Mac/AvatarStoryIndicatorComponent.swift +++ b/Telegram-Mac/AvatarStoryIndicatorComponent.swift @@ -195,6 +195,7 @@ public final class AvatarStoryIndicatorComponent : Equatable { public let inactiveLineWidth: CGFloat public let counters: Counters? public let activeColors: ActiveColors + public let isRoundedRect: Bool public init( hasUnseen: Bool, hasUnseenCloseFriendsItems: Bool, @@ -202,7 +203,8 @@ public final class AvatarStoryIndicatorComponent : Equatable { activeLineWidth: CGFloat, inactiveLineWidth: CGFloat, counters: Counters?, - activeColors: ActiveColors = .default + activeColors: ActiveColors = .default, + isRoundedRect: Bool = false ) { self.hasUnseen = hasUnseen self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems @@ -211,19 +213,20 @@ public final class AvatarStoryIndicatorComponent : Equatable { self.inactiveLineWidth = inactiveLineWidth self.counters = counters self.activeColors = activeColors + self.isRoundedRect = isRoundedRect } - public convenience init(story: EngineStorySubscriptions.Item, presentation: PresentationTheme, active: Bool = false) { + public convenience init(story: EngineStorySubscriptions.Item, presentation: PresentationTheme, active: Bool = false, isRoundedRect: Bool? = nil) { let hasUnseen = story.hasUnseen || story.hasUnseenCloseFriends - self.init(hasUnseen: hasUnseen, hasUnseenCloseFriendsItems: story.hasUnseenCloseFriends, theme: presentation, activeLineWidth: 2.0, inactiveLineWidth: 1.0, counters: .init(totalCount: story.storyCount, unseenCount: active && hasUnseen ? story.storyCount : story.unseenCount)) + self.init(hasUnseen: hasUnseen, hasUnseenCloseFriendsItems: story.hasUnseenCloseFriends, theme: presentation, activeLineWidth: 2.0, inactiveLineWidth: 1.0, counters: .init(totalCount: story.storyCount, unseenCount: active && hasUnseen ? story.storyCount : story.unseenCount), isRoundedRect: isRoundedRect ?? story.peer._asPeer().isForum) } - public convenience init(stats: EngineChatList.StoryStats, presentation: PresentationTheme, activeColors: ActiveColors = .default) { + public convenience init(stats: EngineChatList.StoryStats, presentation: PresentationTheme, activeColors: ActiveColors = .default, isRoundedRect: Bool = false) { let hasUnseen = stats.unseenCount > 0 - self.init(hasUnseen: hasUnseen, hasUnseenCloseFriendsItems: stats.hasUnseenCloseFriends, theme: presentation, activeLineWidth: 2.0, inactiveLineWidth: 1.0, counters: .init(totalCount: stats.totalCount, unseenCount: stats.unseenCount), activeColors: activeColors) + self.init(hasUnseen: hasUnseen, hasUnseenCloseFriendsItems: stats.hasUnseenCloseFriends, theme: presentation, activeLineWidth: 2.0, inactiveLineWidth: 1.0, counters: .init(totalCount: stats.totalCount, unseenCount: stats.unseenCount), activeColors: activeColors, isRoundedRect: isRoundedRect) } - public convenience init(state storyState: PeerExpiringStoryListContext.State, presentation: PresentationTheme, activeColors: ActiveColors = .default) { - self.init(hasUnseen: storyState.hasUnseen, hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends, theme: presentation, activeLineWidth: 2.0, inactiveLineWidth: 1.0, counters: .init(totalCount: storyState.items.count, unseenCount: storyState.unseenCount), activeColors: activeColors) + public convenience init(state storyState: PeerExpiringStoryListContext.State, presentation: PresentationTheme, activeColors: ActiveColors = .default, isRoundedRect: Bool = false) { + self.init(hasUnseen: storyState.hasUnseen, hasUnseenCloseFriendsItems: storyState.hasUnseenCloseFriends, theme: presentation, activeLineWidth: 2.0, inactiveLineWidth: 1.0, counters: .init(totalCount: storyState.items.count, unseenCount: storyState.unseenCount), activeColors: activeColors, isRoundedRect: isRoundedRect) } @@ -323,7 +326,7 @@ public final class AvatarStoryIndicatorComponent : Equatable { context.setLineCap(.round) let spacing: CGFloat = 3.0 * progress - if let counters = component.counters, counters.totalCount > 1, spacing >= 2, counters.totalCount < 50 { + if let counters = component.counters, counters.totalCount > 1, spacing >= 2, counters.totalCount < 50, !component.isRoundedRect { let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5) let radius = (diameter - lineWidth) * 0.5 @@ -380,8 +383,13 @@ public final class AvatarStoryIndicatorComponent : Equatable { return } } - let ellipse = CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5) - context.addEllipse(in: ellipse) + + if component.isRoundedRect { + context.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerWidth: floor(diameter * 0.33), cornerHeight: floor(diameter * 0.33), transform: nil)) + } else { + let ellipse = CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5) + context.addEllipse(in: ellipse) + } context.replacePathWithStrokedPath() context.clip() diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index 6fd9314bf..216124916 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -57,15 +57,11 @@ private func getAllGMTTimeZones() -> [GMTZone] { } private func formatHourToLocaleTime(hour: Int) -> String { - guard hour >= 0 && hour < 24 else { - print("Invalid hour. Please provide a value between 0 and 23.") - return "" - } - // Create a DateComponents object with the hour set to the provided value var components = DateComponents() - components.hour = hour - + components.hour = Int(Float(hour) / 60) + components.minute = hour % 60 + // Use the current calendar to ensure the components are interpreted correctly let calendar = Calendar.current @@ -90,6 +86,14 @@ private func formatHourToLocaleTime(hour: Int) -> String { return formattedTime } +private let formattedMinutes: [Int: String] = { + var values: [Int: String] = [:] + for i in 0 ..< 60 { + values[i] = formatMinutesToLocaleTime(minutes: i) + } + return values +}() + private func formatMinutesToLocaleTime(minutes: Int) -> String { // Create a DateComponents object with the hour set to the provided value @@ -120,19 +124,16 @@ private func formatMinutesToLocaleTime(minutes: Int) -> String { return formattedTime } -// Example usage -let formattedTime = formatHourToLocaleTime(hour: 3) - private final class Arguments { let context: AccountContext let toggleEnabled:()->Void let toggleDay:(State.Day)->Void let enableDay:(State.Day)->Void let addSpecific:(State.Day)->Void - let editSpefic:(State.Day, State.Hours.Hour)->Void - let removeSpefic:(State.Day, State.Hours.Hour)->Void + let editSpefic:(State.Day, State.Hours.MinutesInDay)->Void + let removeSpefic:(State.Day, State.Hours.MinutesInDay)->Void let selectTimezone:(GMTZone)->Void - init(context: AccountContext, toggleEnabled:@escaping()->Void, toggleDay:@escaping(State.Day)->Void, enableDay:@escaping(State.Day)->Void, addSpecific:@escaping(State.Day)->Void, removeSpefic:@escaping(State.Day, State.Hours.Hour)->Void, editSpefic:@escaping(State.Day, State.Hours.Hour)->Void, selectTimezone:@escaping(GMTZone)->Void) { + init(context: AccountContext, toggleEnabled:@escaping()->Void, toggleDay:@escaping(State.Day)->Void, enableDay:@escaping(State.Day)->Void, addSpecific:@escaping(State.Day)->Void, removeSpefic:@escaping(State.Day, State.Hours.MinutesInDay)->Void, editSpefic:@escaping(State.Day, State.Hours.MinutesInDay)->Void, selectTimezone:@escaping(GMTZone)->Void) { self.context = context self.toggleEnabled = toggleEnabled self.toggleDay = toggleDay @@ -178,12 +179,12 @@ private struct State : Equatable { } } struct Hours: Equatable { - struct Hour : Equatable { + struct MinutesInDay : Equatable { var from: Int var to: Int var uniqueId: Int64 } - var list:[Hour] = [] + var list:[MinutesInDay] = [] } var enabled: Bool = false @@ -222,13 +223,13 @@ private let _id_timezone = InputDataIdentifier("_id_timezone") private let _id_add_specific = InputDataIdentifier("_id_add_specific") -private func _id_opening_time(_ day: State.Hours.Hour) -> InputDataIdentifier { +private func _id_opening_time(_ day: State.Hours.MinutesInDay) -> InputDataIdentifier { return InputDataIdentifier("_id_opening_time\(day.uniqueId)") } -private func _id_closing_time(_ day: State.Hours.Hour) -> InputDataIdentifier { +private func _id_closing_time(_ day: State.Hours.MinutesInDay) -> InputDataIdentifier { return InputDataIdentifier("_id_closing_time\(day.uniqueId)") } -private func _id_remove_opening(_ day: State.Hours.Hour) -> InputDataIdentifier { +private func _id_remove_opening(_ day: State.Hours.MinutesInDay) -> InputDataIdentifier { return InputDataIdentifier("_id_remove_opening\(day.uniqueId)") } @@ -311,30 +312,44 @@ private func dayEntries(_ state: State, day: State.Day, arguments: Arguments) -> // entries if let hours = state.data[day] { - let getHoursMenu:(State.Hours.Hour, Bool)->[ContextMenuItem] = { hour, from in + let getHoursMenu:(State.Hours.MinutesInDay, Bool)->[ContextMenuItem] = { hour, from in var items:[ContextMenuItem] = [] var start: Int = 0 - var end: Int = 24 + var end: Int = 24 * 60 if from { start = 0 end = hour.to } else { start = hour.from - end = 24 + end = 23 * 60 } - let minutes = ContextMenu() - for j in 0 ..< 60 { - minutes.addItem(ContextMenuItem(formatMinutesToLocaleTime(minutes: j))) - } + - for i in start ..< end { - let item = ContextMenuItem(formatHourToLocaleTime(hour: i), handler: { - arguments.editSpefic(day, .init(from: from ? i : hour.from, to: !from ? i : hour.to, uniqueId: hour.uniqueId)) - }, state: i == (from ? hour.from : hour.to) ? .on : nil) + let s = Int(Float(start) / 60.0) + let e = Int(Float(end) / 60.0) + + let from_hours = Int(Float(hour.from) / 60) + let to_hours = Int(Float(hour.to) / 60) + + for i in s ... e { + let state: NSControl.StateValue? = i == (from ? from_hours : to_hours) ? .on : nil + let item = ContextMenuItem(formatHourToLocaleTime(hour: i * 60), handler: { + arguments.editSpefic(day, .init(from: from ? i * 60 : hour.from, to: !from ? i * 60 : hour.to, uniqueId: hour.uniqueId)) + }, state: state) + let minutes = ContextMenu() + var minuteItems:[ContextMenuItem] = [] + for j in 0 ..< 60 { + let item = ContextMenuItem(formattedMinutes[j]!, handler: { + arguments.editSpefic(day, .init(from: from ? i * 60 + j : hour.from, to: !from ? i * 60 + j : hour.to, uniqueId: hour.uniqueId)) + }, state: state == .on && j == (from ? hour.from % 60 : hour.to % 60) ? .on : nil) + + minuteItems.append(item) + } + minutes.items = minuteItems item.submenu = minutes items.append(item) @@ -422,7 +437,7 @@ func BusinessHoursController(context: AccountContext, peerId: PeerId) -> InputDa updateState { current in var current = current var hours = current.data[day] ?? .init() - hours.list.append(.init(from: 9, to: 23, uniqueId: arc4random64())) + hours.list.append(.init(from: 9 * 60, to: 23 * 60, uniqueId: arc4random64())) current.data[day] = hours return current } diff --git a/Telegram-Mac/CallScreenController.swift b/Telegram-Mac/CallScreenController.swift new file mode 100644 index 000000000..60751138c --- /dev/null +++ b/Telegram-Mac/CallScreenController.swift @@ -0,0 +1,175 @@ +// +// CallScreenController.swift +// Telegram +// +// Created by Mikhail Filimonov on 15.02.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import Cocoa +import TGUIKit +import TelegramCore +import ObjcUtils +import Postbox +import SwiftSignalKit +import TgVoipWebrtc +import TelegramVoip +import ColorPalette +import PrivateCallScreen +import Emoji + +private func insertSpacesBetweenEmojis(in text: String) -> String { + var newString = "" + var previousCharacterWasEmoji = false + + for character in text { + if character.isEmoji { + if previousCharacterWasEmoji { + // Insert a space before the current emoji + newString += " " + } + previousCharacterWasEmoji = true + } else { + previousCharacterWasEmoji = false + } + newString += String(character) + } + + return newString +} + + + +private func mapCallState(_ state: CallState) -> ExternalPeerCallState { + + + let externalState: ExternalPeerCallState.State + switch state.state { + case .waiting: + externalState = .waiting + case .ringing: + externalState = .ringing + case .requesting(let bool): + externalState = .requesting(ringing: bool) + case .connecting: + externalState = .connecting + case .active(let startTime, let reception, let data): + externalState = .active(startTime: startTime, reception: reception, emoji: insertSpacesBetweenEmojis(in: ObjcUtils.callEmojies(data))) + case .reconnecting(let startTime, let reception, let data): + externalState = .reconnecting(startTime: startTime, reception: reception, emoji: insertSpacesBetweenEmojis(in: ObjcUtils.callEmojies(data))) + case .terminating: + externalState = .terminating + case .terminated(_, let callSessionTerminationReason, _): + let reason: PeerCallSessionTerminationReason? + if let callSessionTerminationReason { + switch callSessionTerminationReason { + case .ended(let callSessionEndedType): + switch callSessionEndedType { + case .hungUp: + reason = .ended(.hungUp) + case .busy: + reason = .ended(.busy) + case .missed: + reason = .ended(.missed) + } + case .error(let callSessionError): + switch callSessionError { + case .generic: + reason = .error(.generic) + case .privacyRestricted: + reason = .error(.privacyRestricted) + case .notSupportedByPeer(let isVideo): + reason = .error(.notSupportedByPeer(isVideo: isVideo)) + case .serverProvided(let text): + reason = .error(.serverProvided(text: text)) + case .disconnected: + reason = .error(.disconnected) + } + } + } else { + reason = nil + } + externalState = .terminated(reason) + } + + let videoState: ExternalPeerCallState.VideoState + switch state.videoState { + case .notAvailable: + videoState = .notAvailable + case .inactive(let bool): + videoState = .inactive(bool) + case .active(let bool): + videoState = .active(bool) + case .paused(let bool): + videoState = .paused(bool) + } + let remoteVideoState: ExternalPeerCallState.RemoteVideoState + switch state.remoteVideoState { + case .active: + remoteVideoState = .active + case .inactive: + remoteVideoState = .inactive + case .paused: + remoteVideoState = .paused + } + + let remoteAudioState: ExternalPeerCallState.RemoteAudioState + switch state.remoteAudioState { + case .active: + remoteAudioState = .active + case .muted: + remoteAudioState = .muted + } + + let remoteBatteryLevel: ExternalPeerCallState.RemoteBatteryLevel + switch state.remoteBatteryLevel { + case .normal: + remoteBatteryLevel = .normal + case .low: + remoteBatteryLevel = .low + } + + return .init(state: externalState, videoState: videoState, remoteVideoState: remoteVideoState, isMuted: state.isMuted, isOutgoingVideoPaused: state.isOutgoingVideoPaused, remoteAspectRatio: state.remoteAspectRatio, remoteAudioState: remoteAudioState, remoteBatteryLevel: remoteBatteryLevel, isScreenCapture: state.isScreenCapture) +} + +private var peerCall: PeerCallScreen? + + +func callScreen(_ context: AccountContext, _ result:PCallResult) { + + + + + switch result { + case let .samePeer(session), let .success(session): + let screen = peerCall ?? PeerCallScreen(external: PeerCallArguments(engine: context.engine, peerId: session.peerId, makeAvatar: { view, peer in + let control = view as? AvatarControl ?? AvatarControl(font: .avatar(17)) + control.setFrameSize(NSMakeSize(120, 120)) + control.userInteractionEnabled = false + control.setPeer(account: context.account, peer: peer) + return control + }, toggleMute: { [weak session] in + session?.toggleMute() + }, toggleCamera: { + + }, toggleScreencast: { + + }, endcall: { + + }, recall: { + + })) + screen.show() + + screen.setState(session.state |> map { + mapCallState($0) + }) + + peerCall = screen + + default: + break + } + +} diff --git a/Telegram-Mac/CallWindowController.swift b/Telegram-Mac/CallWindowController.swift index 3434f4d1a..137bdf218 100644 --- a/Telegram-Mac/CallWindowController.swift +++ b/Telegram-Mac/CallWindowController.swift @@ -15,6 +15,7 @@ import SwiftSignalKit import TgVoipWebrtc import TelegramVoip import ColorPalette +import PrivateCallScreen private let defaultWindowSize = NSMakeSize(720, 560) extension CallState { @@ -1455,7 +1456,19 @@ func closeCall(minimisize: Bool = false) { } + + + +private var peerCall: PeerCallScreen? func applyUIPCallResult(_ context: AccountContext, _ result:PCallResult) { + + #if DEBUG + + callScreen(context, result) + + return + #endif + assertOnMainThread() switch result { case let .success(session): diff --git a/Telegram-Mac/ChatListRowItem.swift b/Telegram-Mac/ChatListRowItem.swift index 6d2180f50..dabb4521b 100644 --- a/Telegram-Mac/ChatListRowItem.swift +++ b/Telegram-Mac/ChatListRowItem.swift @@ -795,7 +795,7 @@ class ChatListRowItem: TableRowItem { self.readState = readState if let story = story, peer?.id != context.peerId { - self.avatarStoryIndicator = .init(stats: story, presentation: theme) + self.avatarStoryIndicator = .init(stats: story, presentation: theme, isRoundedRect: peer?.isForum == true) } else { self.avatarStoryIndicator = nil } @@ -1961,6 +1961,9 @@ class ChatListRowItem: TableRowItem { var isReplyToStory: Bool { if self.messages.first(where: { $0.storyAttribute != nil }) != nil { + if isForum && !isTopic { + return false + } return true } return false diff --git a/Telegram-Mac/ChatSearchHeader.swift b/Telegram-Mac/ChatSearchHeader.swift index 5840bf2c3..db9b6e6bc 100644 --- a/Telegram-Mac/ChatSearchHeader.swift +++ b/Telegram-Mac/ChatSearchHeader.swift @@ -186,7 +186,7 @@ private final class ChatSearchTagsView: View { self.change(opacity: item.enabled ? 1.0 : 0.8, animated: animated) - let image = NSImage(named: "Icon_SearchTagTokenBackground")! + let image = NSImage(named: "Icon_SavedMessages_Premium_Tag")! let background = NSImage(cgImage: generateTintedImage(image: image._cgImage, color: selected ? theme.colors.accent : theme.colors.grayBackground)!, size: image.size) self.backgroundView.image = background @@ -304,7 +304,7 @@ private final class ChatSearchTagsView: View { return 40 } override var height: CGFloat { - var width: CGFloat = 50 + var width: CGFloat = 40 if let textViewLayout = textViewLayout { width += textViewLayout.layoutSize.width + 5 @@ -519,7 +519,7 @@ private final class ChatSearchTagsView: View { let image = NSImage(named: "Icon_SavedMessages_Premium_Tag")! tagImageView.image = NSImage(cgImage: generateTintedImage(image: image._cgImage, color: theme.colors.accent.withAlphaComponent(0.2))!, size: image.size) - tagImageView.setFrameSize(NSMakeSize(tagLayout.layoutSize.width + 12, 22)) + tagImageView.setFrameSize(NSMakeSize(tagLayout.layoutSize.width + 6, 22)) chevron.image = theme.icons.generalNext chevron.sizeToFit() diff --git a/Telegram-Mac/ChatTitleBarView.swift b/Telegram-Mac/ChatTitleBarView.swift index 42d13da28..a62794326 100644 --- a/Telegram-Mac/ChatTitleBarView.swift +++ b/Telegram-Mac/ChatTitleBarView.swift @@ -276,7 +276,9 @@ class ChatTitleBarView: TitledBarView, InteractionContentViewProtocol { let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeOut) : .immediate if self.story != story { if let storyState = story, !storyState.items.isEmpty { - let compoment = AvatarStoryIndicatorComponent(state: storyState, presentation: theme) + let peer: Peer? = peerView != nil ? peerViewMainPeer(peerView!) : nil + + let compoment = AvatarStoryIndicatorComponent(state: storyState, presentation: theme, isRoundedRect: peer?.isForum == true) avatarControl.update(component: compoment, availableSize: NSMakeSize(30, 30), transition: transition) } else { avatarControl.update(component: nil, availableSize: NSMakeSize(36, 36), transition: transition) diff --git a/Telegram-Mac/EmojiesController.swift b/Telegram-Mac/EmojiesController.swift index d1826cd0c..4102a311c 100644 --- a/Telegram-Mac/EmojiesController.swift +++ b/Telegram-Mac/EmojiesController.swift @@ -1460,7 +1460,7 @@ final class AnimatedEmojiesView : Control { func updateSearchState(_ searchState: SearchState, animated: Bool) { - if let window = kitWindow, self.mode == .reactions { + if let window = kitWindow, self.mode == .reactions || self.mode == .defaultTags { switch searchState.state { case .Focus: window._canBecomeKey = true diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 7903a1b28..8360889d6 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259759 + 259860 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/InlineStickerItemLayer.swift b/Telegram-Mac/InlineStickerItemLayer.swift index 40a66efd9..bfdbb65ec 100644 --- a/Telegram-Mac/InlineStickerItemLayer.swift +++ b/Telegram-Mac/InlineStickerItemLayer.swift @@ -569,6 +569,11 @@ final class InlineStickerItemLayer : SimpleLayer { layer?.animateContents() layer?.isPreviousPreview = false } + if self?.superview?.window == nil { + DispatchQueue.main.async { + self?.set(nil, force: true) + } + } }, release: { }, updateState: { [weak self] state in diff --git a/Telegram-Mac/PeerInfoHeadItem.swift b/Telegram-Mac/PeerInfoHeadItem.swift index 6f4b68779..a1196f0e9 100644 --- a/Telegram-Mac/PeerInfoHeadItem.swift +++ b/Telegram-Mac/PeerInfoHeadItem.swift @@ -634,7 +634,7 @@ class PeerInfoHeadItem: GeneralRowItem { colors = .default } - let compoment = AvatarStoryIndicatorComponent(state: storyState, presentation: theme, activeColors: colors) + let compoment = AvatarStoryIndicatorComponent(state: storyState, presentation: theme, activeColors: colors, isRoundedRect: peer?.isForum == true) self.avatarStoryComponent = compoment } else { self.avatarStoryComponent = nil diff --git a/Telegram-Mac/StoryChatListView.swift b/Telegram-Mac/StoryChatListView.swift index 2be30a91f..02d1c4456 100644 --- a/Telegram-Mac/StoryChatListView.swift +++ b/Telegram-Mac/StoryChatListView.swift @@ -762,7 +762,7 @@ private final class StoryListEntryRowItem : TableRowItem { self.entry = entry self.context = context self.open = open - self.stateComponent = .init(story: entry.item, presentation: presentation) + self.stateComponent = .init(story: entry.item, presentation: presentation, isRoundedRect: false) super.init(initialSize) } @@ -931,7 +931,7 @@ private final class ComponentView : Control { transition.updateFrame(view: stateView, frame: stateRect.insetBy(dx: -3, dy: -3)) stateView.update(component: item.stateComponent, availableSize: NSMakeSize(size.width - 6, size.width - 6), progress: progress, transition: transition, displayProgress: !self.loadingStatuses.isEmpty) - + } @@ -1063,8 +1063,8 @@ private final class ItemView : Control { self.progress = progress self.item = item - imageView.setPeer(account: item.context.account, peer: item.entry.item.peer._asPeer(), size: StoryListChatListRowItem.fullSize) - smallImageView.setPeer(account: item.context.account, peer: item.entry.item.peer._asPeer(), size: StoryListChatListRowItem.smallSize) + imageView.setPeer(account: item.context.account, peer: item.entry.item.peer._asPeer(), size: StoryListChatListRowItem.fullSize, disableForum: true) + smallImageView.setPeer(account: item.context.account, peer: item.entry.item.peer._asPeer(), size: StoryListChatListRowItem.smallSize, disableForum: true) imageView.isHidden = progress == 0 smallImageView.isHidden = progress != 0 diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index 7d67f9764485e0725147dffe52fc8274feba165e..e6700d99748ad5faa3b789ce827e057dad13356c 100644 GIT binary patch delta 98 zcmbQ!Y~0gg+|a_IEo2j!98oOR-nEDgh}nUd1Bf|+mCFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259759 + 259860 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index d645338af..9535ea83d 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -1015,6 +1015,8 @@ public final class L10n { public static func callToastMicroOff(_ p1: String) -> String { return L10n.tr("Localizable", "Call.Toast.MicroOff", p1) } + /// Your microphone is off + public static var callToastMicroOffYour: String { return L10n.tr("Localizable", "Call.Toast.MicroOff.Your") } /// Add an optional comment public static var callFeedbackAddComment: String { return L10n.tr("Localizable", "CallFeedback.AddComment") } /// Include technical information diff --git a/packages/PrivateCallScreen/Sources/PeerCallAction.swift b/packages/PrivateCallScreen/Sources/PeerCallAction.swift index cd26a2dec..205758944 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallAction.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallAction.swift @@ -27,6 +27,8 @@ struct PeerCallAction { var loading: Bool = false var enabled: Bool = true var interactive: Bool = true + + var action:()->Void } private let actionSize = NSMakeSize(50, 50) @@ -58,9 +60,9 @@ func makeActiveAction(_ image: NSImage) -> CGImage { })! } -func makeAction(text: String, resource: ImageResource, interactive: Bool = true) -> PeerCallAction { +func makeAction(text: String, resource: ImageResource, active: Bool = false, enabled: Bool = true, loading: Bool = false, interactive: Bool = true, action: @escaping()->Void) -> PeerCallAction { let image = NSImage(resource: resource) - return .init(text: text, normal: !interactive ? image._cgImage! : makeNormalAction(image), activeImage: !interactive ? nil : makeActiveAction(image), active: false, loading: false, enabled: true, interactive: interactive) + return .init(text: text, normal: !interactive ? image._cgImage! : makeNormalAction(image), activeImage: !interactive ? nil : makeActiveAction(image), active: active, loading: loading, enabled: enabled, interactive: interactive, action: action) } @@ -99,7 +101,6 @@ final class PeerCallActionView : Control { imageLayer.cornerRadius = imageLayer.frame.height / 2 backgroundLayer.cornerRadius = imageLayer.frame.height / 2 - backgroundView.layer?.cornerRadius = backgroundView.frame.height / 2 addSubview(backgroundView) @@ -108,9 +109,8 @@ final class PeerCallActionView : Control { scaleOnClick = true set(handler: { [weak self] _ in - if var state = self?.state { - state.active = !state.active - self?.update(state, animated: true) + if let state = self?.state { + state.action() } }, for: .Click) @@ -178,7 +178,7 @@ final class PeerCallActionView : Control { current.isEventLess = true addSubview(current) } - let layout = TextViewLayout(.initialize(string: text, color: .white, font: .normal(12)), maximumNumberOfLines: 1) + let layout = TextViewLayout(.initialize(string: text, color: .white, font: .roundTimer(12)), maximumNumberOfLines: 1) layout.measure(width: 100) current.update(layout) } else if let textView = self.textView { diff --git a/packages/PrivateCallScreen/Sources/PeerCallArguments.swift b/packages/PrivateCallScreen/Sources/PeerCallArguments.swift index f7df72432..aef0f766a 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallArguments.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallArguments.swift @@ -13,11 +13,9 @@ import AppKit internal final class Arguments { let external: PeerCallArguments - let toggleAnim:()->Void let toggleSecretKey:()->Void - init(external: PeerCallArguments, toggleAnim:@escaping()->Void, toggleSecretKey:@escaping()->Void) { + init(external: PeerCallArguments, toggleSecretKey:@escaping()->Void) { self.external = external - self.toggleAnim = toggleAnim self.toggleSecretKey = toggleSecretKey } } @@ -27,9 +25,19 @@ public final class PeerCallArguments { let peerId: PeerId let engine: TelegramEngine let makeAvatar:(NSView?, Peer?)->NSView - public init(engine: TelegramEngine, peerId: PeerId, makeAvatar: @escaping (NSView?, Peer?) -> NSView) { + let toggleMute:()->Void + let toggleCamera:()->Void + let toggleScreencast:()->Void + let endcall:()->Void + let recall:()->Void + public init(engine: TelegramEngine, peerId: PeerId, makeAvatar: @escaping (NSView?, Peer?) -> NSView, toggleMute:@escaping()->Void, toggleCamera:@escaping()->Void, toggleScreencast:@escaping()->Void, endcall:@escaping()->Void, recall:@escaping()->Void) { self.engine = engine self.peerId = peerId self.makeAvatar = makeAvatar + self.toggleMute = toggleMute + self.toggleCamera = toggleCamera + self.toggleScreencast = toggleScreencast + self.endcall = endcall + self.recall = recall } } diff --git a/packages/PrivateCallScreen/Sources/PeerCallPhotoView.swift b/packages/PrivateCallScreen/Sources/PeerCallPhotoView.swift index 3b967371e..02a42073b 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallPhotoView.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallPhotoView.swift @@ -34,6 +34,7 @@ final class PeerCallPhotoView : Control, CallViewUpdater { self.addSubview(photoView) self.photoView = photoView + } func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { if let photoView = photoView { diff --git a/packages/PrivateCallScreen/Sources/PeerCallRevealedSecretKeyView.swift b/packages/PrivateCallScreen/Sources/PeerCallRevealedSecretKeyView.swift index 94910b98b..425a9278e 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallRevealedSecretKeyView.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallRevealedSecretKeyView.swift @@ -107,7 +107,7 @@ internal final class PeerCallRevealedSecretKeyView : NSVisualEffectView, CallVie self.arguments = arguments let text = "If emoji on **\(state.compactTitle)** screen are the same, this call is 100% secure." - let textLayout = TextViewLayout(.initialize(string: text, color: darkPalette.grayIcon.withAlphaComponent(0.7), font: .normal(.text)).detectBold(with: .medium(.text)), alignment: .center) + let textLayout = TextViewLayout(.initialize(string: text, color: NSColor.white.withAlphaComponent(0.8), font: .normal(.text)).detectBold(with: .medium(.text)), alignment: .center) textLayout.measure(width: 300 - 40) textView.update(textLayout) diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift index b6188b5dd..3c240738c 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift @@ -34,6 +34,8 @@ public final class PeerCallScreen : ViewController { statePromise.set(stateValue.modify (f)) } + + public init(external: PeerCallArguments) { self.external = external let size = NSMakeSize(720, 560) @@ -52,6 +54,16 @@ public final class PeerCallScreen : ViewController { } + public func setState(_ signal: Signal) { + actionsDisposable.add(signal.start(next: { [weak self] external in + self?.updateState { current in + var current = current + current.externalState = external + return current + } + })) + } + public override func viewClass() -> AnyClass { return PeerCallScreenView.self } @@ -75,32 +87,7 @@ public final class PeerCallScreen : ViewController { } - let arguments = Arguments(external: external, toggleAnim: { - // updateState { current in - // var current = current - // current.externalState = current.stateIndex + 1 - // if current.stateIndex > 2 { - // current.stateIndex = 0 - // } - // if let networkStatus = current.networkStatus { - // switch networkStatus { - // case .connecting: - // current.networkStatus = .calling - // case .calling: - // current.networkStatus = .failed - // case .failed: - // current.networkStatus = nil - // } - // } else { - // current.networkStatus = .connecting - // } - // current.networkSignal = current.networkSignal + 1 - // if current.networkSignal > 4 { - // current.networkSignal = 0 - // } - // return current - // } - }, toggleSecretKey: { + let arguments = Arguments(external: external, toggleSecretKey: { updateState { current in var current = current current.secretKeyViewState = current.secretKeyViewState.rev @@ -209,7 +196,7 @@ public final class PeerCallScreen : ViewController { if keyIsShown { arguments.toggleSecretKey() return .invoked - } else if event.keyCode == KeyboardKey.Space.rawValue { + } else if event.keyCode == KeyboardKey.Space.rawValue || event.keyCode == KeyboardKey.Return.rawValue { arguments.toggleSecretKey() return .invoked } @@ -220,6 +207,7 @@ public final class PeerCallScreen : ViewController { screen.set(handler: invokeEsc, with: self, for: .Escape) screen.set(handler: invokeEsc, with: self, for: .Space) + screen.set(handler: invokeEsc, with: self, for: .Return) } diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift index 0a2b123d5..dfeb050e2 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift @@ -96,7 +96,11 @@ final class PeerCallScreenView : Control { private var secretView: SecretKeyView? private var revealedKey: PeerCallRevealedSecretKeyView? + private var keyoverlay: Control? + private var tooltipsViews: [PeerCallTooltipStatusView] = [] + private var tooltips: [PeerCallTooltipStatusView.TooltipType] = [] + required init(frame frameRect: NSRect) { super.init(frame: frameRect) self.layer?.addSublayer(backgroundLayer) @@ -110,14 +114,7 @@ final class PeerCallScreenView : Control { addSubview(muteAction!) addSubview(endAction!) - muteAction?.update(makeAction(text: "Mute", resource: .icMute), animated: false) - videoAction?.update(makeAction(text: "Video", resource: .icVideo), animated: false) - screencastAction?.update(makeAction(text: "Screen", resource: .icScreen), animated: false) - endAction?.update(makeAction(text: "End Call", resource: .icDecline, interactive: false), animated: false) - - photoView.set(handler: { [weak self] _ in - self?.arguments?.toggleAnim() - }, for: .Click) + updateLayout(size: self.frame.size, transition: .immediate) } @@ -173,6 +170,12 @@ final class PeerCallScreenView : Control { transition.updateFrame(view: revealedKey, frame: revealedKeyFrame(view: revealedKey, state: state)) revealedKey.updateLayout(size: revealedKey.frame.size, transition: transition) } + if let keyoverlay { + transition.updateFrame(view: keyoverlay, frame: size.bounds) + } + + + let actions = [self.videoAction, self.screencastAction, self.muteAction, self.endAction].compactMap { $0 } @@ -183,6 +186,13 @@ final class PeerCallScreenView : Control { transition.updateFrame(view: action, frame: CGRect(origin: CGPoint(x: x, y: size.height - action.frame.height - 40), size: action.frame.size)) x += action.frame.width + 36 } + + var y: CGFloat = size.height - 70 - 40 + for tooltip in tooltipsViews { + y -= (tooltip.frame.height + 10) + transition.updateFrame(view: tooltip, frame: tooltip.centerFrameX(y: y)) + tooltip.reveal(animated: transition.isAnimated) + } } func updateState(_ state: PeerCallState, arguments: Arguments, transition: ContainedViewLayoutTransition) { @@ -194,6 +204,12 @@ final class PeerCallScreenView : Control { self.backgroundLayer.update(stateIndex: state.stateIndex, isEnergySavingEnabled: false, transition: transition) + muteAction?.update(makeAction(text: "Mute", resource: .icMute, active: state.externalState.isMuted, action: arguments.external.toggleMute), animated: false) + videoAction?.update(makeAction(text: "Video", resource: .icVideo, action: arguments.external.toggleCamera), animated: false) + screencastAction?.update(makeAction(text: "Screen", resource: .icScreen, action: arguments.external.toggleScreencast), animated: false) + endAction?.update(makeAction(text: "End Call", resource: .icDecline, interactive: false, action: arguments.external.endcall), animated: false) + + if let tooltip = state.statusTooltip { if self.statusTooltip?.string != tooltip { if let statusTooltip = self.statusTooltip { @@ -244,9 +260,33 @@ final class PeerCallScreenView : Control { current.layer?.animateScaleSpring(from: 0.8, to: 1, duration: 0.2, bounce: false) } } - } else if let revealedKey = self.revealedKey { - performSubviewRemoval(revealedKey, animated: transition.isAnimated, scale: false) - self.revealedKey = nil + + if "".isEmpty { + let current: Control + if let view = self.keyoverlay { + current = view + } else { + current = Control(frame: bounds) + self.addSubview(current, positioned: .below, relativeTo: revealedKey) + self.keyoverlay = current + + current.set(handler: { [weak arguments] _ in + arguments?.toggleSecretKey() + }, for: .Click) + } + ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: bounds) + } + + + } else { + if let revealedKey = self.revealedKey { + performSubviewRemoval(revealedKey, animated: transition.isAnimated, scale: false) + self.revealedKey = nil + } + if let keyoverlay = self.keyoverlay { + performSubviewRemoval(keyoverlay, animated: transition.isAnimated, scale: false) + self.keyoverlay = nil + } } @@ -276,6 +316,38 @@ final class PeerCallScreenView : Control { self.secretView = nil } + var tooltips:[PeerCallTooltipStatusView.TooltipType] = [] + + if state.externalState.isMuted, state.isActive { + tooltips.append(.yourMicroOff) + } + if state.externalState.remoteAudioState == .muted, state.isActive { + tooltips.append(.microOff(state.compactTitle)) + } + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.tooltips, rightList: tooltips) + + for deleteIndex in deleteIndices.reversed() { + let view = self.tooltipsViews.remove(at: deleteIndex) + performSubviewRemoval(view, animated: transition.isAnimated, scale: true) + } + for indicesAndItem in indicesAndItems { + let view = PeerCallTooltipStatusView(frame: .zero) + view.set(type: indicesAndItem.1) + tooltipsViews.insert(view, at: indicesAndItem.0) + } + for updateIndex in updateIndices { + let view = self.tooltipsViews[updateIndex.0] + view.set(type: updateIndex.1) + } + CATransaction.begin() + for view in self.tooltipsViews { + view.removeFromSuperview() + } + self.subviews.insert(contentsOf: self.tooltipsViews, at: 0) + CATransaction.commit() + + self.tooltips = tooltips } } diff --git a/packages/PrivateCallScreen/Sources/PeerCallStatusView.swift b/packages/PrivateCallScreen/Sources/PeerCallStatusView.swift index 3a2d9837b..e40f6f0e2 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallStatusView.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallStatusView.swift @@ -70,7 +70,7 @@ private class ActiveCallView : View { let time = Int32(CFAbsoluteTimeGetCurrent() - referenceTime) let text = String.durationTransformed(elapsed: Int(time)) - let value = DynamicCounterTextView.make(for: text, count: text, font: .normal(.text), textColor: .white, width: .greatestFiniteMagnitude) + let value = DynamicCounterTextView.make(for: text, count: text, font: .roundTimer(.text), textColor: .white, width: .greatestFiniteMagnitude) duration.update(value, animated: transition.isAnimated) duration.change(size: value.size, animated: transition.isAnimated) @@ -144,25 +144,27 @@ internal final class PeerCallStatusView : View, CallViewUpdater { performSubviewRemoval(view, animated: transition.isAnimated, scale: true) self.activeView = nil } - if let view = networkStatus { - performSubviewRemoval(view, animated: transition.isAnimated, scale: true) - self.networkStatus = nil - } - let current: TextView = TextView() - current.userInteractionEnabled = false - current.isSelectable = false - let layout = TextViewLayout(.initialize(string: string, color: NSColor.white, font: .normal(.header))) - layout.measure(width: .greatestFiniteMagnitude) - current.update(layout) - - addSubview(current) - self.networkStatus = current - - current.centerX(y: textView.frame.maxY + 10) - - if transition.isAnimated { - current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) - current.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) + if string != self.networkStatus?.textLayout?.attributedString.string { + if let view = networkStatus { + performSubviewRemoval(view, animated: transition.isAnimated, scale: true) + self.networkStatus = nil + } + let current: TextView = TextView() + current.userInteractionEnabled = false + current.isSelectable = false + let layout = TextViewLayout(.initialize(string: string, color: NSColor.white, font: .roundTimer(.header))) + layout.measure(width: .greatestFiniteMagnitude) + current.update(layout) + + addSubview(current) + self.networkStatus = current + + current.centerX(y: textView.frame.maxY + 10) + + if transition.isAnimated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + current.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) + } } case .timer(let double, let int32): diff --git a/packages/PrivateCallScreen/Sources/PeerCallTooltipStatusView.swift b/packages/PrivateCallScreen/Sources/PeerCallTooltipStatusView.swift new file mode 100644 index 000000000..f7a9022f0 --- /dev/null +++ b/packages/PrivateCallScreen/Sources/PeerCallTooltipStatusView.swift @@ -0,0 +1,150 @@ +// +// File.swift +// +// +// Created by Mikhail Filimonov on 15.02.2024. +// + +import Foundation +import TGUIKit +import AppKit +import Localization + +private let micro = NSImage(resource: .icMicrophoneoff).precomposed(.white) + +final class PeerCallTooltipStatusView : NSVisualEffectView { + + enum TooltipType : Comparable, Identifiable { + + static func <(lhs: TooltipType, rhs: TooltipType) -> Bool { + return lhs.index < rhs.index + } + + case yourMicroOff + case microOff(String) + + var index:Int { + switch self { + case .yourMicroOff: + return 0 + case .microOff: + return 1 + } + } + var stableId: AnyHashable { + return self.index + } + + var icon: CGImage { + switch self { + case .yourMicroOff: + return micro + case .microOff: + return micro + } + } + var text: String { + switch self { + case .yourMicroOff: + return L10n.callToastMicroOffYour + case let .microOff(title): + return L10n.callToastMicroOff(title) + } + } + } + + + private let imageView = ImageView() + private let textView = TextView() + private let maskLayer = SimpleShapeLayer() + private let control = Control() + + private var revealed = false + + required override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + self.addSubview(imageView) + self.addSubview(textView) + + addSubview(control) + + self.textView.userInteractionEnabled = false + self.textView.isSelectable = false + + self.wantsLayer = true + self.material = .light + self.state = .active + self.blendingMode = .withinWindow + + self.layer?.mask = maskLayer + + +// control.set(handler: { [weak self] _ in +// self?.reveal() +// }, for: .Click) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var imageOffset: CGFloat { + return 5 + self.imageView.frame.width + 5 + } + + func set(type: TooltipType) { + + self.imageView.image = type.icon + self.imageView.sizeToFit() + + let layout = TextViewLayout(.initialize(string: type.text, color: NSColor.white, font: .medium(.text)), alignment: .center) + layout.measure(width: .greatestFiniteMagnitude) + self.textView.update(layout) + + self.setFrameSize(NSMakeSize(imageOffset + layout.layoutSize.width + 10, layout.layoutSize.height + 10)) + + self.layer?.cornerRadius = frame.height / 2 + if #available(macOS 10.15, *) { + self.layer?.cornerCurve = .continuous + } + + let rect: NSRect + if !revealed { + rect = NSMakeRect(0, 0, imageOffset, frame.height) + } else { + rect = bounds + } + + let path = CGMutablePath() + path.addRoundedRect(in: rect, cornerWidth: frame.height / 2, cornerHeight: frame.height / 2) + maskLayer.path = path + maskLayer.frame = NSMakeRect(0, 0, frame.width, frame.height) + + } + + func reveal(animated: Bool) { + guard !revealed else { + return + } + let path = CGMutablePath() + path.addRoundedRect(in: self.bounds, cornerWidth: self.frame.height / 2, cornerHeight: self.frame.height / 2) + self.maskLayer.animate(from: maskLayer.path!, to: path, keyPath: "path", timingFunction: .spring, duration: 0.5, removeOnCompletion: false, completion: { [weak self] _ in + self?.maskLayer.path = path + }) + + self.layer?.animatePosition(from: NSMakePoint(self.frame.minX + (self.frame.width / 2) - self.imageOffset / 2, self.frame.minY), to: self.frame.origin, duration: 0.5, timingFunction: .spring) + + revealed = true + + } + + + override func layout() { + super.layout() + control.frame = bounds + imageView.centerY(x: 5) + textView.centerY(x: imageOffset) + } + +} diff --git a/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_microphoneoff.imageset/Contents.json b/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_microphoneoff.imageset/Contents.json new file mode 100644 index 000000000..bacfcadb1 --- /dev/null +++ b/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_microphoneoff.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_call_microphoneoff.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_call_microphoneoff@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_microphoneoff.imageset/ic_call_microphoneoff.png b/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_microphoneoff.imageset/ic_call_microphoneoff.png new file mode 100644 index 0000000000000000000000000000000000000000..b22951ae89dcca047139e3d15052572274f36d9e GIT binary patch literal 472 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6-QsS%!O zzP=1vKsE;hV|yk83y{SK#8N=az`(SC2`(bBfEmFCNgn(gJ{_po+tbA{#Dn+k6k9JA zM}by$Q9&M7|HMbEHqF_~^en0v4n}kQJ1FO%?8tM*JpP7r#*&1Dghd;F*>3OBY`Yn^ z{CI)=I{S%0$0%1FjSAdm@$QvR+Y8z2uoN`<3i#17DZRhUYJ7FMe&#TW$8S zL3T!cW|{o!bb-$~yNom&wN`87Nv>G=KxwO^yH@Sm^ws#L$~CFiAsM3L0sC1yUljhVbP+sMsiFD1N$8@N%%|7dA9r3W zo8HC0;;qy{#Ve=QE&QOb9l7`-V@-?y2Z=0hS-qeth3of%HOwA%B&^STq;I%4!?e!u w)OGH8omZ?oj#_0T+8FM?-}>PE-9NHF7{%^~#ImgI;{*k-r>mdKI;Vst0C;S-od5s; literal 0 HcmV?d00001 diff --git a/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_microphoneoff.imageset/ic_call_microphoneoff@2x.png b/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_microphoneoff.imageset/ic_call_microphoneoff@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..48abd6a53d435fa6f4c6241ad60430e13801e99f GIT binary patch literal 855 zcmV-d1E~CoP)iK?SXB)JC+lGlslO)ZbtCz{%ddJ9EwKL)ioW zJ7;&!oOAv&b7pt%$VeyM40JQl&A`7f0}~{9JTlQ;*iC^&@CAGWFTpJ6f!bx9h+E(X zm<~igO#f@}a{!)j{|Km6jDV6VBe9qM7hq8Gc3&iFUkS~;k}o51JtBD{q3u+=gh)If zBTF#UswNtdG7^W#=oJVhuYMJFW0HzQV+0g91&Kxpq#)5aft`|Q1%Vh6I&q2$yEO#< zmPDfj^mNwyk(W?m=U2jyE%N94lj;h#nc+mv^DrcAXqi8=N!nheRyose*@ zs#QmE{|*?5t1g^YLfc8RuP-nCwQ>)5DXR%@SE+sh?|P*Wi8bI!a2b?Vkho2nwhC$& zfYk(QpB&?kP92k0khn=U_LqxDsIXNBI%02t8E_Zqp6s->T1VnMNxcVtHOln+*m`Nj zO6VB82A+ZqkSDOhy+v>Z6urW NSFont { + if let font = caches[.init(type: .roundTimer, size: size)] { + return font + } + if #available(OSX 10.15, *) { + if let descriptor = NSFont.systemFont(ofSize: size).fontDescriptor.withDesign(.rounded), let font = NSFont(descriptor: descriptor, size: size) { + return font + } else { + return .systemFont(ofSize: size, weight: .medium) + } + } else { + if let font = NSFont(name: ".SFCompactRounded-Semibold", size: size) { + return font + } else { + return .systemFont(ofSize: size, weight: .medium) + } + } + } + static func medium(_ size:FontSize) ->NSFont { if let font = caches[.init(type: .medium, size: size)] { return font diff --git a/packages/TGUIKit/Sources/Window.swift b/packages/TGUIKit/Sources/Window.swift index c064060e1..1684dec8f 100644 --- a/packages/TGUIKit/Sources/Window.swift +++ b/packages/TGUIKit/Sources/Window.swift @@ -311,7 +311,7 @@ open class Window: NSWindow { private var swipeHandlers:[SwipeIdentifier: SwipeHandler] = [:] private var swipeState:[SwipeIdentifier: SwipeDirection] = [:] public var keyUpHandler:((NSEvent)->Void)? - private var responsders:[ResponderObserver] = [] + private var responders:[ResponderObserver] = [] private var mouseHandlers:[UInt:[MouseObserver]] = [:] private var swipePoints:[NSPoint] = [] private var saver:WindowSaver? @@ -363,17 +363,17 @@ open class Window: NSWindow { } public func set(responder:@escaping() -> NSResponder?, with object:NSObject?, priority:HandlerPriority, ignoreKeys: [KeyboardKey] = []) { - responsders.append(ResponderObserver(responder, object, priority, ignoreKeys + [.Escape, .LeftArrow, .RightArrow, .Tab, .UpArrow, .DownArrow, .Space])) + responders.append(ResponderObserver(responder, object, priority, ignoreKeys + [.Escape, .LeftArrow, .RightArrow, .Tab, .UpArrow, .DownArrow, .Space])) } public func removeObserver(for object:NSObject) { var copy:[ResponderObserver] = [] - for observer in responsders { + for observer in responders { copy.append(observer) } for i in stride(from: copy.count - 1, to: -1, by: -1) { if copy[i].object.value == object || copy[i].object.value == nil { - responsders.remove(at: i) + responders.remove(at: i) } } } @@ -440,7 +440,7 @@ open class Window: NSWindow { self.swipeHandlers = self.swipeHandlers.filter { key, value in return value.object.value !== object && value.object.value != nil } - self.responsders = responsders.filter { + self.responders = responders.filter { $0.object.value !== object } } @@ -532,7 +532,7 @@ open class Window: NSWindow { public func applyResponderIfNeeded(_ event: NSEvent? = nil) ->Void { - let sorted = responsders.sorted(by: >) + let sorted = responders.sorted(by: >) if let event = event, event.modifierFlags.contains(.option) || event.modifierFlags.contains(.command) || event.modifierFlags.contains(.control) { diff --git a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift index acac12b56..092832b74 100644 --- a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift +++ b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift @@ -3372,6 +3372,19 @@ public final class TelegramIconsTheme { return image } } + public var settingsBusiness: CGImage { + if let image = cached.with({ $0["settingsBusiness"] }) { + return image + } else { + let image = _settingsBusiness() + _ = cached.modify { current in + var current = current + current["settingsBusiness"] = image + return current + } + return image + } + } public var generalCheck: CGImage { if let image = cached.with({ $0["generalCheck"] }) { return image @@ -10548,6 +10561,7 @@ public final class TelegramIconsTheme { private let _settingsUpdateActive: ()->CGImage private let _settingsFiltersActive: ()->CGImage private let _settingsProfile: ()->CGImage + private let _settingsBusiness: ()->CGImage private let _generalCheck: ()->CGImage private let _settingsAbout: ()->CGImage private let _settingsLogout: ()->CGImage @@ -11341,6 +11355,7 @@ public final class TelegramIconsTheme { settingsUpdateActive: @escaping()->CGImage, settingsFiltersActive: @escaping()->CGImage, settingsProfile: @escaping()->CGImage, + settingsBusiness: @escaping()->CGImage, generalCheck: @escaping()->CGImage, settingsAbout: @escaping()->CGImage, settingsLogout: @escaping()->CGImage, @@ -12133,6 +12148,7 @@ public final class TelegramIconsTheme { self._settingsUpdateActive = settingsUpdateActive self._settingsFiltersActive = settingsFiltersActive self._settingsProfile = settingsProfile + self._settingsBusiness = settingsBusiness self._generalCheck = generalCheck self._settingsAbout = settingsAbout self._settingsLogout = settingsLogout diff --git a/submodules/telegram-ios b/submodules/telegram-ios index 2e7d6be64..180e1f814 160000 --- a/submodules/telegram-ios +++ b/submodules/telegram-ios @@ -1 +1 @@ -Subproject commit 2e7d6be64ca1fd90498825a9d2edf02a34363f1e +Subproject commit 180e1f8141aed2af4d2a7dc859590f96b4d09bfd diff --git a/tools/generate-images.swift b/tools/generate-images.swift index 65487e40c..2e1dcd482 100644 --- a/tools/generate-images.swift +++ b/tools/generate-images.swift @@ -261,6 +261,7 @@ func initialize() -> [String] { array.append("settingsUpdateActive") array.append("settingsFiltersActive") array.append("settingsProfile") + array.append("settingsBusiness") array.append("generalCheck") array.append("settingsAbout") array.append("settingsLogout") From e1288fb510ef3cca3f62aa72f014d79551f035d8 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 16 Feb 2024 14:07:33 -0400 Subject: [PATCH 17/50] business progress --- Telegram-Mac/AccountViewController.swift | 17 +- .../Contents.json | 0 .../status.png | Bin .../status@2x.png | Bin Telegram-Mac/Info.plist | 2 +- Telegram-Mac/PremiumBoardingController.swift | 249 +++++++++++++----- .../PremiumBoardingFeaturesController.swift | 12 + Telegram-Mac/PremiumBoardingRowItem.swift | 4 +- TelegramShare/Info.plist | 2 +- 9 files changed, 216 insertions(+), 70 deletions(-) rename Telegram-Mac/Assets.xcassets/{Premium_Boarding_Status.imageset => Icon_Premium_Boarding_Status.imageset}/Contents.json (100%) rename Telegram-Mac/Assets.xcassets/{Premium_Boarding_Status.imageset => Icon_Premium_Boarding_Status.imageset}/status.png (100%) rename Telegram-Mac/Assets.xcassets/{Premium_Boarding_Status.imageset => Icon_Premium_Boarding_Status.imageset}/status@2x.png (100%) diff --git a/Telegram-Mac/AccountViewController.swift b/Telegram-Mac/AccountViewController.swift index 347a155ec..40b71ae1e 100644 --- a/Telegram-Mac/AccountViewController.swift +++ b/Telegram-Mac/AccountViewController.swift @@ -67,7 +67,7 @@ fileprivate final class AccountInfoArguments { let openFaq:()->Void let ask:()->Void let openUpdateApp:() -> Void - let openPremium:()->Void + let openPremium:(Bool)->Void let giftPremium:()->Void let addAccount:([AccountWithInfo])->Void let setStatus:(Control, TelegramUser)->Void @@ -75,7 +75,7 @@ fileprivate final class AccountInfoArguments { let set2Fa:(TwoStepVeriticationAccessConfiguration?)->Void let openStory:(StoryInitialIndex?)->Void let openWebBot:(AttachMenuBot)->Void - init(context: AccountContext, storyList: PeerStoryListContext, presentController:@escaping(ViewController, Bool)->Void, openFaq: @escaping()->Void, ask:@escaping()->Void, openUpdateApp: @escaping() -> Void, openPremium:@escaping()->Void, giftPremium:@escaping()->Void, addAccount:@escaping([AccountWithInfo])->Void, setStatus:@escaping(Control, TelegramUser)->Void, runStatusPopover:@escaping()->Void, set2Fa:@escaping(TwoStepVeriticationAccessConfiguration?)->Void, openStory:@escaping(StoryInitialIndex?)->Void, openWebBot:@escaping(AttachMenuBot)->Void) { + init(context: AccountContext, storyList: PeerStoryListContext, presentController:@escaping(ViewController, Bool)->Void, openFaq: @escaping()->Void, ask:@escaping()->Void, openUpdateApp: @escaping() -> Void, openPremium:@escaping(Bool)->Void, giftPremium:@escaping()->Void, addAccount:@escaping([AccountWithInfo])->Void, setStatus:@escaping(Control, TelegramUser)->Void, runStatusPopover:@escaping()->Void, set2Fa:@escaping(TwoStepVeriticationAccessConfiguration?)->Void, openStory:@escaping(StoryInitialIndex?)->Void, openWebBot:@escaping(AttachMenuBot)->Void) { self.context = context self.storyList = storyList self.presentController = presentController @@ -402,12 +402,12 @@ private enum AccountInfoEntry : TableItemListNodeEntry { }, border:[BorderType.Right], inset:NSEdgeInsets(left: 12, right: 12)) case let .premium(_, viewType): return GeneralInteractedRowItem(initialSize, stableId: stableId, name: strings().accountSettingsPremium, icon: theme.icons.settingsPremium, activeIcon: theme.icons.settingsPremium, type: .next, viewType: viewType, action: { - arguments.openPremium() + arguments.openPremium(false) }, border:[BorderType.Right], inset:NSEdgeInsets(left: 12, right: 12)) case let .business(_, viewType): //TODO LANG return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "Telegram Business", icon: theme.icons.settingsBusiness, activeIcon: theme.icons.settingsBusiness, type: .next, viewType: viewType, action: { - arguments.openPremium() + arguments.openPremium(true) }, border:[BorderType.Right], inset:NSEdgeInsets(left: 12, right: 12)) case let .giftPremium(_, viewType): return GeneralInteractedRowItem(initialSize, stableId: stableId, name: strings().accountSettingsGiftPremium, icon: theme.icons.settingsGiftPremium, activeIcon: theme.icons.settingsGiftPremium, type: .next, viewType: viewType, action: arguments.giftPremium, border:[BorderType.Right], inset:NSEdgeInsets(left: 12, right: 12)) @@ -877,8 +877,13 @@ class AccountViewController : TelegramGenericViewController CFBundleVersion - 259860 + 259874 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/PremiumBoardingController.swift b/Telegram-Mac/PremiumBoardingController.swift index 9b9b47636..33469ec43 100644 --- a/Telegram-Mac/PremiumBoardingController.swift +++ b/Telegram-Mac/PremiumBoardingController.swift @@ -64,6 +64,8 @@ enum PremiumLogEventsSource : Equatable { case last_seen case message_privacy case saved_tags + case business + case business_standalone var value: String { switch self { case let .deeplink(ref): @@ -112,6 +114,10 @@ enum PremiumLogEventsSource : Equatable { return "message_privacy" case .saved_tags: return "saved_tags" + case .business: + return "business" + case .business_standalone: + return "business_standalone" } } @@ -159,6 +165,10 @@ enum PremiumLogEventsSource : Equatable { return .last_seen case .message_privacy: return .message_privacy + case .business: + return nil + case .business_standalone: + return nil } } @@ -258,6 +268,23 @@ enum PremiumValue : String { case saved_tags case last_seen case message_privacy + + case business_location + case business_hours + case business_quick_replies + case business_greeting_messages + case business_away_messages + case business_chatbots + + var isBusiness: Bool { + switch self { + case .business_location, .business_hours, .business_quick_replies, .business_greeting_messages, .business_away_messages, .business_chatbots: + return true + default: + return false + } + } + func gradient(_ index: Int) -> [NSColor] { let colors:[NSColor] = [ NSColor(rgb: 0xef6922), NSColor(rgb: 0xe95a2c), @@ -282,17 +309,31 @@ enum PremiumValue : String { return [colors[index]] } - func icon(_ index: Int, presentation: TelegramPresentationTheme) -> CGImage { + func businessGradient(_ index: Int) -> [NSColor] { + let colors = [ + NSColor(red: 0, green: 0.478, blue: 1, alpha: 1), + NSColor(red: 0.675, green: 0.392, blue: 0.953, alpha: 1), + NSColor(red: 0.937, green: 0.412, blue: 0.133, alpha: 1), + NSColor(red: 0.914, green: 0.365, blue: 0.267, alpha: 1), + NSColor(red: 0.949, green: 0.51, blue: 0.165, alpha: 1), + NSColor(red: 0.906, green: 0.584, blue: 0.098, alpha: 1) + ] + return [colors[index]] + } + + func icon(_ index: Int, business: Bool, presentation: TelegramPresentationTheme) -> CGImage { let image = self.image(presentation) let size = image.backingSize let img = generateImage(size, contextGenerator: { size, ctx in ctx.clear(size.bounds) ctx.clip(to: size.bounds, mask: image) - let colors = gradient(index).compactMap { $0.cgColor } as NSArray + let gradient: [NSColor] = business ? businessGradient(index) : gradient(index) + + let colors = gradient.compactMap { $0.cgColor } as NSArray - if gradient(index).count == 1 { - ctx.setFillColor(gradient(index)[0].cgColor) + if gradient.count == 1 { + ctx.setFillColor(gradient[0].cgColor) ctx.fill(size.bounds) } else { let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) @@ -321,43 +362,55 @@ enum PremiumValue : String { func image(_ presentation: TelegramPresentationTheme) -> CGImage { switch self { case .double_limits: - return NSImage(named: "Icon_Premium_Boarding_X2")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingX2).precomposed(presentation.colors.accent) case .more_upload: - return NSImage(named: "Icon_Premium_Boarding_Files")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingFiles).precomposed(presentation.colors.accent) case .faster_download: - return NSImage(named: "Icon_Premium_Boarding_Speed")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingSpeed).precomposed(presentation.colors.accent) case .voice_to_text: - return NSImage(named: "Icon_Premium_Boarding_Voice")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingVoice).precomposed(presentation.colors.accent) case .no_ads: - return NSImage(named: "Icon_Premium_Boarding_Ads")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingAds).precomposed(presentation.colors.accent) case .infinite_reactions: - return NSImage(named: "Icon_Premium_Boarding_Reactions")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingReactions).precomposed(presentation.colors.accent) case .emoji_status: - return NSImage(named: "Premium_Boarding_Status")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingStatus).precomposed(presentation.colors.accent) case .premium_stickers: - return NSImage(named: "Icon_Premium_Boarding_Stickers")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingStickers).precomposed(presentation.colors.accent) case .animated_emoji: - return NSImage(named: "Icon_Premium_Boarding_Emoji")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingEmoji).precomposed(presentation.colors.accent) case .advanced_chat_management: - return NSImage(named: "Icon_Premium_Boarding_Chats")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingChats).precomposed(presentation.colors.accent) case .profile_badge: - return NSImage(named: "Icon_Premium_Boarding_Badge")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingBadge).precomposed(presentation.colors.accent) case .animated_userpics: - return NSImage(named: "Icon_Premium_Boarding_Profile")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingProfile).precomposed(presentation.colors.accent) case .translations: - return NSImage(named: "Icon_Premium_Boarding_Translations")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingTranslations).precomposed(presentation.colors.accent) case .stories: - return NSImage(named: "Icon_Premium_Stories")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumStories).precomposed(presentation.colors.accent) case .wallpapers: - return NSImage(named: "Icon_Premium_Wallpapers")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumWallpapers).precomposed(presentation.colors.accent) case .peer_colors: - return NSImage(named: "Icon_Premium_Peer_Colors")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumPeerColors).precomposed(presentation.colors.accent) case .saved_tags: - return NSImage(named: "Icon_Premium_Boarding_Tag")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingTag).precomposed(presentation.colors.accent) case .last_seen: - return NSImage(named: "Icon_Premium_Boarding_LastSeen")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingLastSeen).precomposed(presentation.colors.accent) case .message_privacy: - return NSImage(named: "Icon_Premium_Boarding_MessagePrivacy")!.precomposed(presentation.colors.accent) + return NSImage(resource: .iconPremiumBoardingMessagePrivacy).precomposed(presentation.colors.accent) + case .business_location: + return NSImage(resource: .iconPremiumBusinessLocation).precomposed(presentation.colors.accent) + case .business_hours: + return NSImage(resource: .iconPremiumBusinessHours).precomposed(presentation.colors.accent) + case .business_quick_replies: + return NSImage(resource: .iconPremiumBusinessQuickReply).precomposed(presentation.colors.accent) + case .business_greeting_messages: + return NSImage(resource: .iconPremiumBusinessGreeting).precomposed(presentation.colors.accent) + case .business_away_messages: + return NSImage(resource: .iconPremiumBusinessAway).precomposed(presentation.colors.accent) + case .business_chatbots: + return NSImage(resource: .iconPremiumBusinessBot).precomposed(presentation.colors.accent) } } @@ -401,6 +454,19 @@ enum PremiumValue : String { return strings().premiumBoardingLastSeenTitle case .message_privacy: return strings().premiumBoardingMessagePrivacyTitle + case .business_location: + //TODOLANG + return "Location" + case .business_hours: + return "Opening Hours" + case .business_quick_replies: + return "Quick Replies" + case .business_greeting_messages: + return "Greeting Messages" + case .business_away_messages: + return "Away Messages" + case .business_chatbots: + return "ChatBots" } } func info(_ limits: PremiumLimitConfig) -> String { @@ -443,6 +509,19 @@ enum PremiumValue : String { return strings().premiumBoardingLastSeenInfo case .message_privacy: return strings().premiumBoardingMessagePrivacyInfo + case .business_location: + //TODOLANG + return "Display the location of your business on your account." + case .business_hours: + return "Show to your customers when you are open for business." + case .business_quick_replies: + return "Set up shortcuts with rich text and media to respond to messages faster." + case .business_greeting_messages: + return "Create greetings that will be automatically sent to new customers." + case .business_away_messages: + return "Define messages that are automatically sent when you are off." + case .business_chatbots: + return "Add any third party chatbots that will process customer interactions." } } } @@ -451,6 +530,12 @@ enum PremiumValue : String { private struct State : Equatable { var values:[PremiumValue] = [.double_limits, .stories, .more_upload, .faster_download, .voice_to_text, .no_ads, .infinite_reactions, .emoji_status, .premium_stickers, .animated_emoji, .advanced_chat_management, .profile_badge, .animated_userpics, .translations, .saved_tags, .last_seen, .message_privacy] + #if DEBUG + var businessValues: [PremiumValue] = [.business_location, .business_hours, .business_greeting_messages, .business_away_messages, .business_quick_replies, .business_chatbots] + #else + var businessValues: [PremiumValue] = [] + #endif + let source: PremiumLogEventsSource var premiumProduct: InAppPurchaseManager.Product? @@ -501,8 +586,6 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { sectionId += 1 - - entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("header"), equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in let status = ChatMessageItem.applyMessageEntities(with: [TextEntitiesMessageAttribute(entities: state.premiumConfiguration.statusEntities)], for: state.premiumConfiguration.status, message: nil, context: arguments.context, fontSize: 13, openInfo: arguments.openInfo, isDark: theme.colors.isDark, bubbled: theme.bubbled) return PremiumBoardingHeaderItem(initialSize, stableId: stableId, context: arguments.context, presentation: arguments.presentation, isPremium: state.isPremium, peer: state.peer?.peer, emojiStatus: state.status, source: state.source, premiumText: status, viewType: .legacy) @@ -539,55 +622,85 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { })) index += 1 - entries.append(.sectionId(sectionId, type: .customModern(15))) + entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 } - - for (i, value) in state.values.enumerated() { - let viewType = bestGeneralViewType(state.values, for: i) - - struct Tuple : Equatable { - let value: PremiumValue - let isNew: Bool + if state.source == .business || state.source == .business_standalone, !state.businessValues.isEmpty { + for (i, value) in state.businessValues.enumerated() { + let viewType = bestGeneralViewType(state.businessValues, for: i) + + struct Tuple : Equatable { + let value: PremiumValue + let isNew: Bool + } + let tuple = Tuple(value: value, isNew: state.newPerks.contains(value.rawValue)) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init(value.rawValue), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return PremiumBoardingRowItem(initialSize, stableId: stableId, viewType: viewType, presentation: arguments.presentation, index: i, value: value, limits: arguments.context.premiumLimits, isLast: false, isNew: tuple.isNew, callback: { value in + arguments.openFeature(value, true) + }) + })) + index += 1 } - let tuple = Tuple(value: value, isNew: state.newPerks.contains(value.rawValue)) - entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init(value.rawValue), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in - return PremiumBoardingRowItem(initialSize, stableId: stableId, viewType: viewType, presentation: arguments.presentation, index: i, value: value, limits: arguments.context.premiumLimits, isLast: false, isNew: tuple.isNew, callback: { value in - arguments.openFeature(value, true) - }) - })) - index += 1 + if state.source == .business { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("+\(state.values.count) MORE TELEGRAM PREMIUM FEATURES"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + } } - - if !state.isPremium { - let status = ChatMessageItem.applyMessageEntities(with: [TextEntitiesMessageAttribute(entities: state.premiumConfiguration.statusEntities)], for: state.premiumConfiguration.status, message: nil, context: arguments.context, fontSize: 11.5, openInfo: arguments.openInfo, textColor: arguments.presentation.colors.listGrayText, isDark: theme.colors.isDark, bubbled: theme.bubbled) - - entries.append(.desc(sectionId: sectionId, index: index, text: .attributed(status), data: .init(color: arguments.presentation.colors.listGrayText, viewType: .textBottomItem))) - index += 1 - } else { + if state.source != .business_standalone { + for (i, value) in state.values.enumerated() { + let viewType = bestGeneralViewType(state.values, for: i) + + struct Tuple : Equatable { + let value: PremiumValue + let isNew: Bool + } + let tuple = Tuple(value: value, isNew: state.newPerks.contains(value.rawValue)) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init(value.rawValue), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return PremiumBoardingRowItem(initialSize, stableId: stableId, viewType: viewType, presentation: arguments.presentation, index: i, value: value, limits: arguments.context.premiumLimits, isLast: false, isNew: tuple.isNew, callback: { value in + arguments.openFeature(value, true) + }) + })) + index += 1 + } - entries.append(.sectionId(sectionId, type: .customModern(15))) - sectionId += 1 + if !state.isPremium { + let status = ChatMessageItem.applyMessageEntities(with: [TextEntitiesMessageAttribute(entities: state.premiumConfiguration.statusEntities)], for: state.premiumConfiguration.status, message: nil, context: arguments.context, fontSize: 11.5, openInfo: arguments.openInfo, textColor: arguments.presentation.colors.listGrayText, isDark: theme.colors.isDark, bubbled: theme.bubbled) - - entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().premiumBoardingAboutTitle.uppercased()), data: .init(color: arguments.presentation.colors.listGrayText, viewType: .textTopItem))) - index += 1 - - entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("_id_about"), equatable: nil, comparable: nil, item: { initialSize, stableId in - return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .singleItem, text: strings().premiumBoardingAboutText, font: .normal(.text)) - })) - - entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(strings().premiumBoardingAboutTos, linkHandler: { _ in + entries.append(.desc(sectionId: sectionId, index: index, text: .attributed(status), data: .init(color: arguments.presentation.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + } else { - }), data: .init(color: arguments.presentation.colors.listGrayText, viewType: .textBottomItem))) - index += 1 + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().premiumBoardingAboutTitle.uppercased()), data: .init(color: arguments.presentation.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("_id_about"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .singleItem, text: strings().premiumBoardingAboutText, font: .normal(.text)) + })) + + entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(strings().premiumBoardingAboutTos, linkHandler: { _ in + + }), data: .init(color: arguments.presentation.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + + } } - entries.append(.sectionId(sectionId, type: .customModern(15))) + + + + entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 @@ -976,6 +1089,7 @@ final class PremiumBoardingController : ModalViewController { self.openFeatures = openFeatures self.presentation = presentation super.init(frame: NSMakeRect(0, 0, 380, 530)) + bar = .init(height: 50, enableBorder: false) } override var hasBorder: Bool { @@ -1040,6 +1154,19 @@ final class PremiumBoardingController : ModalViewController { private var arguments: Arguments? + override var enableBack: Bool { + return true + } + + override func loadView() { + if self.source == .business_standalone { + self.leftBarView = getLeftBarViewOnce() + self.centerBarView = getCenterBarViewOnce() + self.rightBarView = getRightBarViewOnce() + } + super.loadView() + } + override func viewDidLoad() { super.viewDidLoad() diff --git a/Telegram-Mac/PremiumBoardingFeaturesController.swift b/Telegram-Mac/PremiumBoardingFeaturesController.swift index aab67ae71..b3db8050c 100644 --- a/Telegram-Mac/PremiumBoardingFeaturesController.swift +++ b/Telegram-Mac/PremiumBoardingFeaturesController.swift @@ -311,6 +311,18 @@ final class PremiumBoardingFeaturesView: View { slideView.displaySlide(at: 17, animated: false) case .message_privacy: slideView.displaySlide(at: 18, animated: false) + case .business_location: + fatalError() + case .business_hours: + fatalError() + case .business_quick_replies: + fatalError() + case .business_greeting_messages: + fatalError() + case .business_away_messages: + fatalError() + case .business_chatbots: + fatalError() } needsLayout = true diff --git a/Telegram-Mac/PremiumBoardingRowItem.swift b/Telegram-Mac/PremiumBoardingRowItem.swift index a6aedfda8..cc6aa1cd0 100644 --- a/Telegram-Mac/PremiumBoardingRowItem.swift +++ b/Telegram-Mac/PremiumBoardingRowItem.swift @@ -21,6 +21,7 @@ final class PremiumBoardingRowItem : GeneralRowItem { fileprivate let callback: (PremiumValue)->Void fileprivate let premValueIndex: Int + fileprivate let business: Bool fileprivate let presentation: TelegramPresentationTheme fileprivate let isNew: Bool init(_ initialSize: NSSize, stableId: AnyHashable, viewType: GeneralViewType, presentation: TelegramPresentationTheme, index: Int, value: PremiumValue, limits: PremiumLimitConfig, isLast: Bool, isNew: Bool, callback: @escaping(PremiumValue)->Void) { @@ -30,6 +31,7 @@ final class PremiumBoardingRowItem : GeneralRowItem { self.premValueIndex = index self.isLastItem = isLast self.callback = callback + self.business = value.isBusiness self.isNew = isNew self.titleLayout = .init(.initialize(string: value.title(limits), color: presentation.colors.text, font: .medium(.title))) self.infoLayout = .init(.initialize(string: value.info(limits), color: presentation.colors.grayText, font: .normal(.text))) @@ -134,7 +136,7 @@ private final class PremiumBoardingRowView: GeneralContainableRowView { return } - imageView.image = item.value.icon(item.premValueIndex, presentation: item.presentation) + imageView.image = item.value.icon(item.premValueIndex, business: item.business, presentation: item.presentation) imageView.sizeToFit() nextView.image = item.presentation.icons.premium_boarding_feature_next diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 74700d0b4..78d917cce 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259860 + 259874 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion From 05c590328ee90c005b218ef491bf1e15982598c1 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Sun, 18 Feb 2024 10:15:47 -0400 Subject: [PATCH 18/50] - bugfixes --- Telegram-Mac/ApplicationContext.swift | 27 ----- .../Contents.json | 22 ++++ .../business_24 (3).png | Bin 0 -> 373 bytes .../business_24@2x (3).png | Bin 0 -> 624 bytes .../Contents.json | 22 ++++ .../sleep_24.png | Bin 0 -> 681 bytes .../sleep_24@2x.png | Bin 0 -> 1228 bytes .../Contents.json | 22 ++++ .../bot_24.png | Bin 0 -> 591 bytes .../bot_24@2x.png | Bin 0 -> 1072 bytes .../Contents.json | 22 ++++ .../hand_24.png | Bin 0 -> 625 bytes .../hand_24@2x.png | Bin 0 -> 1127 bytes .../Contents.json | 22 ++++ .../clock_24.png | Bin 0 -> 581 bytes .../clock_24@2x.png | Bin 0 -> 1190 bytes .../Contents.json | 22 ++++ .../location_24.png | Bin 0 -> 586 bytes .../location_24@2x.png | Bin 0 -> 1160 bytes .../Contents.json | 22 ++++ .../reply_24.png | Bin 0 -> 555 bytes .../reply_24@2x.png | Bin 0 -> 1041 bytes .../BusinessAwayMessageController.swift | 2 +- Telegram-Mac/BusinessChatbotController.swift | 2 +- Telegram-Mac/BusinessHoursController.swift | 2 +- Telegram-Mac/BusinessLocationController.swift | 4 +- .../BusinessQuickReplyController.swift | 2 +- Telegram-Mac/FastSettings.swift | 3 +- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/PremiumBoardingController.swift | 51 ++++++++- .../PremiumBoardingFeaturesController.swift | 71 ++++++++----- .../PremiumBoardingStoriesController.swift | 99 +++++++++++++++--- TelegramShare/Info.plist | 2 +- submodules/telegram-ios | 2 +- 34 files changed, 343 insertions(+), 80 deletions(-) create mode 100644 Telegram-Mac/Assets.xcassets/Icon_PremiumBoarding_Business.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_PremiumBoarding_Business.imageset/business_24 (3).png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_PremiumBoarding_Business.imageset/business_24@2x (3).png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Away.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Away.imageset/sleep_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Away.imageset/sleep_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Bot.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Bot.imageset/bot_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Bot.imageset/bot_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Greeting.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Greeting.imageset/hand_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Greeting.imageset/hand_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Hours.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Hours.imageset/clock_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Hours.imageset/clock_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Location.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Location.imageset/location_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Location.imageset/location_24@2x.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Reply.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Reply.imageset/reply_24.png create mode 100644 Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Reply.imageset/reply_24@2x.png diff --git a/Telegram-Mac/ApplicationContext.swift b/Telegram-Mac/ApplicationContext.swift index 84fba7e47..ba27a51ae 100644 --- a/Telegram-Mac/ApplicationContext.swift +++ b/Telegram-Mac/ApplicationContext.swift @@ -540,33 +540,6 @@ final class AuthorizedApplicationContext: NSObject, SplitViewDelegate { return .invoked }, with: self, for: .T, priority: .supreme, modifierFlags: [.command]) - window.set(handler: { [weak self] _ -> KeyHandlerResult in - context.bindings.rootNavigation().push(BusinessHoursController(context: context, peerId: context.peerId)) - - return .invoked - }, with: self, for: .Y, priority: .supreme, modifierFlags: [.command]) - - window.set(handler: { [weak self] _ -> KeyHandlerResult in - context.bindings.rootNavigation().push(BusinessAwayMessageController(context: context, peerId: context.peerId)) - - return .invoked - }, with: self, for: .U, priority: .supreme, modifierFlags: [.command]) - - window.set(handler: { [weak self] _ -> KeyHandlerResult in - context.bindings.rootNavigation().push(BusinessLocationController(context: context, peerId: context.peerId)) - - return .invoked - }, with: self, for: .I, priority: .supreme, modifierFlags: [.command]) - window.set(handler: { [weak self] _ -> KeyHandlerResult in - context.bindings.rootNavigation().push(BusinessChatbotController(context: context, peerId: context.peerId)) - - return .invoked - }, with: self, for: .J, priority: .supreme, modifierFlags: [.command]) - window.set(handler: { [weak self] _ -> KeyHandlerResult in - context.bindings.rootNavigation().push(BusinessQuickReplyController(context: context, peerId: context.peerId)) - - return .invoked - }, with: self, for: .R, priority: .supreme, modifierFlags: [.command]) #endif diff --git a/Telegram-Mac/Assets.xcassets/Icon_PremiumBoarding_Business.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PremiumBoarding_Business.imageset/Contents.json new file mode 100644 index 000000000..e3222e819 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PremiumBoarding_Business.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "business_24 (3).png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "business_24@2x (3).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_PremiumBoarding_Business.imageset/business_24 (3).png b/Telegram-Mac/Assets.xcassets/Icon_PremiumBoarding_Business.imageset/business_24 (3).png new file mode 100644 index 0000000000000000000000000000000000000000..542e8657ccfc97c5193629a7ee37bb9c0a5e115d GIT binary patch literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O(AT%InDAr*{!r#13683?%UZ)22LsQiM(ib?qj z?+pjD0+zc61S1aezF^zbxb0&H`<5qBYwUk6`Ld&W$~2}Ap_#SY^440v4qsw@>gop% zRk7G6O}-zgg?yUaQkOd1UY!(L=@9=`D)#|j*@V?xS2DU8?!HXQRhZ26HNWFX>KgwE z;?E~&+iW;`)4#z-{LBlN&E2Oxi!^&ey&J5(3*II!>OUm0LYBqC{^XyHOOG@)B&wJ` z(GN|o-<^YYz(?7e=(ln*yVbMJvElg z6dGo+m^}KXvR$C&_U^)VwohN>vTZNy<~eF@C(?RG{;#9d>-uP^4bwmKrU|e{`0lB= Q3JfI%Pgg&ebxsLQ0R1D9RR910 literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_PremiumBoarding_Business.imageset/business_24@2x (3).png b/Telegram-Mac/Assets.xcassets/Icon_PremiumBoarding_Business.imageset/business_24@2x (3).png new file mode 100644 index 0000000000000000000000000000000000000000..bd8b7e27cde951d4df4df3ed007ab3b1a035243b GIT binary patch literal 624 zcmV-$0+0QPP)}J< z14p|Q>zrSD=&Vj>B_ZHvwP~wPLJc7G*jb%2ay+UdDZrz|^%>XVa8ExA@FGv)ju)mc ztC9Sfg2yM^v1MLAxkEL|a){xs3dOu65Yz^!{tJXjdB?aomwJPn+*c7&Pf|0}1SX9o zLH{H$^UX`=8xs66fCR&nz%qah{2_zJ&rwUFwmx}kX`5Nv_ANGT3D67ErJ-G*DxE%U z1uWaZ2B%|0^Ps+LW5sq+QGL=yUf*nLX%jflU0d4;rU7`%hhHCI?KDhz<`P_y?1JxQWQ`jGa+!zqcrVY>l0000< KMNUMnLSTY^1P6Hl literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Away.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Away.imageset/Contents.json new file mode 100644 index 000000000..a14dfe35c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Away.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "sleep_24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sleep_24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Away.imageset/sleep_24.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Away.imageset/sleep_24.png new file mode 100644 index 0000000000000000000000000000000000000000..ef4a106fdbd136029ced8d54e9329b827fd8f4cb GIT binary patch literal 681 zcmV;a0#^NrP)U>R0G`0HN=%mAzzI-V#qf9A z&7&$qW`@Kn7gqUg;D7bkkFR?G{&ir|=IpHEF=6TU-rmW#A^ZVyNt<@TH8k*IxnS@Z zIQ-G|4)b{^+r_+!h67{F`0=|@06TzLhg1JqhZt&3eFQGNT&Bt0Mk z7Z3xXe&ODE7G(d#Fh+J1Giy;J#H)xDN6(eeR0;!0^ z2ktY$DoQpnW7;1Z)ol>#WJ`AFfYBP%lgzJ+pWeMv7n`>&$il?043gxw3p4+qp>MbM z@AQGSwk}F(s9fQm_|^5wmZ@oTc6XzxQjCMI$-*F^hHYIOL`@XBNQ+`T3lu{4g@}0Rxs0mNJ%FVyIG;&gmCU zt}Y=1iQj`&k>#5phURA8(eR5IBl*Duj*I}A`#qLM>SUS=(@f@!Q6Z6!rv$6NEj1?GxlDD0l z0M>S5g<-l<)i4NQ-17rQio_?uG@6>8@9XaR=>}kpHP-lF1Ctuwo_${;)Q8s22>h*T zw+<^rVgWa2dlx`ZiL(oHdbjZQPajr$>I2tPXxw4eWU-GL<_@eItv-Ou}nT2oWnLVl;#^``HCWdUdeJOm+hknnu_ z^tuTPAhUot^3&*fZa$a$*3}^tiF64N52l3qOrxGvNG1Vk$Ulagcl$>KRGSD<*bIlc zlY-ajr|2fVfMt-M#*7d@m)k5qd)x7oKA%d!vdKs2c`sb0R4a>!6k+tc3VYtg_-7v! zTt4V*fjrRjih1P(EhH+6gX?g!oMbsIKM!bpS@5!jNFzcGu4E<OfVgWME zoCiww-gJMv`o|?K0F&`ax1_Bcd>NN77Rm$3$dEhtrbX=6`d$MOUURr@Z&$C&V1ZQt z@s!@FpoR{7%#t}d6O-KnkLSgISxSx?^rRi?&4SGr6j*Spf(nPYH=5`*WC9-Z8=8|3 zk7#scIxYvVC9>kM!taOE0mSg7!tBJU<5Z{9hY9rlGEY<5-w?0`kNW(|viEaXS{ zCR+D-o0V0-qR38u><1%Q1ti`NWI;q;+LD}{Kz-rJG`Wwc%3Ian+pqxQX26%P-uo9W zWB^(3VILJe8XoU{**D9V5UtBMCJM3hl8l_mz)eVrozw0vWQ6P;Mh*JQW*6hc7q-k~0z|GEJM_IGnjbFlar62qAy#41a}j(hGZnCOgpmMv;8Y6^cLCw8sdDM5*3;Rk+ko>^SV zberg*f)Bi;j*I~>1#w0M%1Gen5PXhQ>xN6VNsc#aSUz3nypnCcn+S;HEse4o{ySEQ@K)Iym z1h|VRLvQ7>_n`oT!%7t;wt#SfN)UjA3vGn-mk792cm_glzZD4Iat;zt3E(7}cAB;2 z-t{ZaUZn{Fmq5^eo70RMx4mwE-`jkTr#Ol`ZXeCgXML{pytme0PEI0xF2!+{WpQnA z;OUdjtPXaHzJxg}H&e)t{#TBGwvsVLaV@58&UDe_$|zGLoV$yhM2egxJ4(hpt|%$ap_SN~GjCbrbo7(Bj6`Y#WyfqKgC#DpBy#4hy7HEj=KgmzabdumY+3QDr%I z`1td~d*@%Rk*F^30mfKY&Q)WziNprjLXx)e*84sOId`1M&$jX&5Mr5!i)=o#(GPeC zz!EIgG*jqt56juQfG>tI00LfjTh%gr0`d}-0P}JXQs}xvCA%{lCxgA;3kY#o^fTU| zUGdfR*UEa2%N)&R8`J*y$1Z!wv}~W7KLP=NForsz-1XKjn-c}dvQzR+6=s0!4X53} d^wR#o8c*Mg*8la|p;Q0>002ovPDHLkV1n=e|Cj&( literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Bot.imageset/bot_24@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Bot.imageset/bot_24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1e50de1bd8c2e79e91145cbc855fde416aa00d GIT binary patch literal 1072 zcmV-01kd}4P)-7{fI7=ANffH z5~kBLyC}j!W*63!m3>tdv%51r-90niZyF#$f&>Ys!L3`D@&_?u3F1W-Vuk@v98cZ= zgazU9B%4|J(JN94UxT16B+L*}X84@|S_XncWSoOBXj;2%2Nr<24UmeP-F_Jdus^D> z#?%ckM0H+6<}#pNx5_|h-zkpJL;>fNqO-1Pc5*vTXV~|9eKS9RK^_nbyc}EpYlnAn z)hb;zAR;0g;MO-PyfJKFN)wQFbiL^wuV-Ok*gU47UKnUwyVq5SfJg?6Do+Fl8z0;n zM8NeIYabB~)k8!hLL1=LH;Vw`M7C34yjd$myj}~ETs%VWbrxTP@6HA`}<+WYHuOseSsdY zF(@~BVBMq50t%{eH0todw5`4NX`qZ5{f6>cq0VhToy#5EN3dDYUjdQYQvd9!iD*M& z7)rC8z^1RvjDAX{zY6JeuLzpZHh?F7>osV?y9hX%ai@CjZ~bHrQUA!3ZG8dOkVo~{_NerBcl5}y_)9x#XVP| zZ4R}~@qWQ)0EJ?SGR0dT+xA0G(~er&1wut7DO1q)@SU7fWoUt(=Lsu8V}s9t&)0ju z(v9&?R;exDH6SMN-hc4i{k>e{$W%dsd{Fc92gTqh>C?8WaJ?x7=p5@ffEKeFvY0AJiv@I z7a;tCDa1PgROZJshyj>l*^&&6-8q8vxzh_Yamz_fodZsC*GE>Ud#et-BAH5{4r|ra z9j9%8Oeli9O9*Gh!$le)m1oEMS4;|3ZVX=+`n+y4JFNJl`3{SpmX^?2d#_(&@z^J1 zi28X1D&LnHS@}}ih7gEYnOiUNP`|`7KO~vsvsH~!s)=43Rb>$NoL8*PLI`7|!m_l& qRJp}6E^c2e?rxDFL4pJcLg5d(8??06Fq?V+0000$*SQ7;RH33k-CIEDeegfCrEOFrYES=(5kzJ9TKVH8SI9VV2rDE$$@2Kk`1@w&&3gb3xEB}(UKy4I&T^awJ4Jo3y2%GZtN;U$ zwOgg|WB_*+7zVG)*2Uk|%R>C3(_0(v`3-}(pNMFTajrilaj^UR^XXX@m&sJ6s~Q0` zohfVr;&}ANua}N`4PtI9sgi4qsJDUX5G=og8Mq;!t+9+R5%562$=EFd4ALToEwjPm zO2I`ER!Lf#EAW_pH}uU+$4LvaL}WEU=KZxW8WVE+2~BDX2_P8B#0=JtzSWX!vqT)j zds!}y_I!^CyD|g^iwVTgBA{BjCI3a3%WA8S2@bC$A+aNwhcgvR^A*wyY~ic)Ka@?@ zY~k6*UPC4bROGLHy1CIgd7H$pf9kaoTjGzw;EtHl+zLdZ?5u63TcXpl5q8Hg1F^+O z$X?}EKnzXIR_9$sfL#A))&C`Dp#@9eMg>`YXTWhD(TanL{P!bQAWNjwISnBPa^UNd zNEdDcy&f*A+vj?6=fz#fK3!J*ewNS7`|)}um(}ec?H>icr^LwM{jsaNSsVxpKq~Pp zLS4*?Jkv{!5z7c%BnQpwT~vM!rW00000 LNkvXXu0mjfNN^C0 literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Greeting.imageset/hand_24@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Greeting.imageset/hand_24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..051c8496ea70bb097803b417e73def8f75517926 GIT binary patch literal 1127 zcmV-t1ep7YP)Y1!qVrk9824!E?KEWn8aI`_%H?r?6nLR;vPZ0YAgcHO`Rx92JZ%7HkH^4W* zJXuN}Fd_j7Pj^u@<4(pVPR}1fNb^b7jJtcP>#MG+u5N$|6)IGyP~kfULP{Tt zoqzr1?(+fM5DF6*27fe^)-M65VcQf!`{(`R4%`585f}!oh7W$@3~K$``utU-JYpY+ z2Hd?NV{VE>CIVt@Plvk%(3+NlgAZzPwb?x!4gb0qD4<vn?X{XCmNG zdtN7x>#0nGb@)h>M}!eG@7Z>KABpnvzI<@hHGqH1yCGeSH z#4wNpsR@YnA$E_ER{rYJ!SR{~_*eRfXeFuWbtFWNC@ian8dr_yH^l&An9~RAEy#$} z1T^TnJdr21Oa05i@yHniBV`+rP(HsglAR|%}ScLH)vQ!3ZU)6UD z!oHOwu^@f7RJaq7BHFZVr4o>3|D+dQiNB^*bY5R*{4IX%%l{wsSs>3l-JXr9x@{|! zfX(yC;4MOe2dtbJdKwub1U`2U;aIR`i30_vBQ-lQj%8EQBaIMQMc;mx7 z!R#cULW+ZzH>rzM`QLH7Fo_A0Ze)<6RWNF1pD9A%2VeK(DV8~T`1|ICG9{5nx(pdS zj9*|2Qo>0O*WKBWl=||J7<;&?mG|eYOxuSF zruq!PvzQocLkc)+p%bocXj0oueTWn+?Z4Qw|A!CO+iEgtkwWfm(5jEM|5{S^2d{R= z#@#$PSAYd=PhL7^95$`Bohy-}ED(sx!epP|n#s8{r*NUT&cDkFE4hdDwG}&3vd& z=KI;bpbT8>JDkZR8Mwx0Iui@NX7Ea7YWE#GhYOhr2)-7Hnm*gLrF&X;%tb&@Hi?tX zd%L`WZOEa?C~qz8cbQ{g7Da2I=w>g}Pvwv!%CnHQrS4Hb?e~QVL`oz`ut+)lcle^J zcinC*cd4Y+eF76-Zpk$ZSEgB%k3NHv5Lwdj*_kfyNaNaGaVc)zkcT5O+(+~)plT(@ zu?s%_=r6WimFHC`jevz*EqXwk1lJ!6BVRYTN4~dHq;4vmfQ6*Q+u{2b`}7JQ73+vx tm?6I5`%u0Ug1(%-phATT6)Jpdd5qwt7&(Exf#n8x0|;>qCO}x~34kNGBY^~53!*208(2?ZJpl=jm{{6zrYQzW zTMBDt1DVd7*YBJ6rU0kd6PTuG-1sq^Unh?W8a{}~Qgz)bcFyg6*hGSt>LSktP5x^l z;+seGEqMjuhM^`A*{Y`9ry<&jQFYNARu{8TZC;73;b)`T@=I#rkQ4X-#EPOix5ZA@ zUgwAPc>xKoO5MxBIyY|24A0dkKr|@zGVO3LC7h7^j%qGJ9-1DZv6+(^sU?61xzfD^ z?rPNs@Y><#Ng=g}^cc)5otYs-u-X%d(Ror-jK$=#Pd;dusCr1d`ab@PuL@=^C!t&m6aE TJK^>Y00000NkvXXu0mjfQ&{=@ literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Hours.imageset/clock_24@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Hours.imageset/clock_24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6e5487e227f91c89d838964f03310068839a3b20 GIT binary patch literal 1190 zcmV;X1X=ruP)qd(G60K;{IBhu{-&*eF9S_u=1Cw)hYekGAKz2^c#jw@1p)gQ= z>mTCHp^Y8qS$0#0L@aNCVgOI)|iVCt60l4Hm1oTHUhOU?{-oyT{Pe$ZTOc;{q zI^nR~*GB*v0$S&PyX739sBl)i^>f$|l>5eO5*#vnNuYja6r_~XCFUd`)GD#fUfpw0 z29qeq{qK&JUyV4+A&ih&>KfBYz`C-Oo(|b$aFXEy$Nds<4>%9jdh|8#lq+SLxdwMPa%xL=Jhgb+u?Hw?-8{P^SWj?=3f7i)!ApFVY^&iX{0Z}lF04wF-M5_MD7t(4cfa`6R`Fr?OGQrH07`+>vk zi0pG7ZBtQx${{iRz=1Q+*+@~07*qoM6N<$ Ef<;&(OaK4? literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Location.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Location.imageset/Contents.json new file mode 100644 index 000000000..240ecc082 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Location.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "location_24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Location.imageset/location_24.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Location.imageset/location_24.png new file mode 100644 index 0000000000000000000000000000000000000000..4a0b1a03114cf7305c12be95ad1b352649c0587b GIT binary patch literal 586 zcmV-Q0=4~#P)}()5Mj8%xsq!5yYb z)3JE3q(mT?vWi;#cSA;d`<2ATRM4A^WiL}Od?~w>=1D+GcZd7p2BcyX^SMs(IE;br zKq8&kM@1?1;@0FnI||3t{V39ug$?)~O-=QrK_;!pOny4(0uO7POLL}Sl;8<1-tiWc zvJJ3$wz%M~!`TzO$53|-ZO1X4gJNGafA-HP?e4q0X_QZX`$daGDn`*jgMHQUCw| literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Location.imageset/location_24@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Location.imageset/location_24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9f62af2b7f2a71014193202707fe5d059b4e6a92 GIT binary patch literal 1160 zcmV;31b6$1P)n&Q)5pz0D1zwMcuK^9!m@1;00SN5RDSS6*PjspVn z!IFA5(u`{p9yl|+F8SKxIYvH1-B$qCU(h`va>VqBz86>t7A2DpLOV;u&sT-p4UuZe z{9&Mg`8x&#_Z5Kk1=C#Np=3wjo9&=Y$PCRT&m;2{Z432vlPA72LJZYTCQVn5SjhuIxzOM( z0E<57fh{s&nkK*9FrU3lK3F9&LW^l;v<7Y~@Q@~)Uzy)wQ4ti`J2>5 zyGEfbAd?H40N_6#|*&;=0{wr^hAWddjC@w>!Pr&#Y=YSSDz;y+Z_}sK< zews-mg6)_RS#dUfS_={P!e?s=z*PxxOO9|~ft7XUY@HAE7D%%2n-p_kC`o3^C{s6t zXGqv)9zC*O9!RQ1O{LnuHN2Ao?#g47rveRbC9w*K6h-mg7td>{F?qmG0ff((oe@~p zxC$#?h^eea_ce+9%s;tR?&;?=N7d3akNC|Aricoh;~ z_@R>y~i(Y zZ3G?_4#%qX1CQLSCtJzqXb`mGxRK#e0o9M!PuK@yy8?=ZI#b1o2tK1Mz2+YIT-{Iw zRPV@6jQ|5H&_!Wr#kU|mc^gy}9;m0K9I8Hv5^&}TTlx4EO;_);9Y6JDmN8&8#Zi8@cTE$UYQKxd{n*#&&SE0`JK9ZuRd*kFVI a41WOh5t|g_!Q9dS0000FVNAr5HGvFKzM=p0`&#z#UKfeyZ|F0NBM#LKs}fkWT)*h zKwyDg@T3WK*`1l5s;Qpt0hr<6ppsVG^|Xq^ln|2y)t!=ynl61!&oCt-1)fr+V;}sS zB{0du6_^sC0yRgSX!0|Ld_6Cl@C(Gk$O*^`l2=p3j3#O>wXBD=d-(gl5ZMbYOopA%5Vy7V5YWXK%8eV@h!m(g>P!QzF10W- ziG73Gp|YV(3dV>GrN}~Bo`|^vLrRDj%7m0C?Dh7Tl<*Vc@$Z(2;R7kI zsH1*op`F}uay{gvyPlU1Ea-86T=d?lFKze7M7~8v1O=WN5gs;2#h6Rxm@NO=8X-QF tfWbyyULm104HXun0p(&#oWl%%3g3?0*yPT002ovPDHLkV1oZ9@a_Nr literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Reply.imageset/reply_24@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Premium_Business_Feature_Reply.imageset/reply_24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8d7d7d48b70111288d4a4cbf293654230cf9e3df GIT binary patch literal 1041 zcmV+s1n&EZP)*`0a202wl5$dDn!KMaI~al2vUZGwul6+&(PQ0qd9 zn1Te+`DDym78yX0m(LbB&myD<1qnfnw|jIU#@hf*=SzZRNEwL`xG8;E72|n5&WQ0# ztu-Acgs}cup|$olY><)Vv`A->(9)K}jHuJF_t^mCI`UqY1813C7I9}@b`w?0 z`|6>?I?JCR++`zad}dhUh<&Z%j|4xf&jpzm0UN4OGJhn*lfxqMSAX_0lhY+S(7(mVa|+tKDH~2DCWK3Q+JK>1Z>%%1kU)i7 z&V(^Y2xR>W5qBKZCfecX-Fd&G3OrLd82eVDaHuudOPWmXkSL!VKn%z@9E~}5f6O{6 zRY34AtY0GcLQ5^}OlkwTT82p&5CeW;C)YIY*P)Ukv70%`>z)UhL|=?ycn%)~A%Qt3 zq8>s;Wnu)p$zA=_M84#bQx?R4I761Dq+Gr@s6&Pf88T$Z@W0|W7EZ}@Y4QLt00000 LNkvXXu0mjfJG#pQ literal 0 HcmV?d00001 diff --git a/Telegram-Mac/BusinessAwayMessageController.swift b/Telegram-Mac/BusinessAwayMessageController.swift index c6cdfd4f8..cf7e6dc65 100644 --- a/Telegram-Mac/BusinessAwayMessageController.swift +++ b/Telegram-Mac/BusinessAwayMessageController.swift @@ -308,7 +308,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { return entries } -func BusinessAwayMessageController(context: AccountContext, peerId: PeerId) -> InputDataController { +func BusinessAwayMessageController(context: AccountContext) -> InputDataController { let actionsDisposable = DisposableSet() diff --git a/Telegram-Mac/BusinessChatbotController.swift b/Telegram-Mac/BusinessChatbotController.swift index 05d68cc26..3bf3385dd 100644 --- a/Telegram-Mac/BusinessChatbotController.swift +++ b/Telegram-Mac/BusinessChatbotController.swift @@ -407,7 +407,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { return entries } -func BusinessChatbotController(context: AccountContext, peerId: PeerId) -> InputDataController { +func BusinessChatbotController(context: AccountContext) -> InputDataController { let actionsDisposable = DisposableSet() diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index 216124916..6a3826a4e 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -395,7 +395,7 @@ private func dayEntries(_ state: State, day: State.Day, arguments: Arguments) -> } -func BusinessHoursController(context: AccountContext, peerId: PeerId) -> InputDataController { +func BusinessHoursController(context: AccountContext) -> InputDataController { let actionsDisposable = DisposableSet() diff --git a/Telegram-Mac/BusinessLocationController.swift b/Telegram-Mac/BusinessLocationController.swift index 0582b1832..9b37a8382 100644 --- a/Telegram-Mac/BusinessLocationController.swift +++ b/Telegram-Mac/BusinessLocationController.swift @@ -279,7 +279,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { return entries } -func BusinessLocationController(context: AccountContext, peerId: PeerId) -> InputDataController { +func BusinessLocationController(context: AccountContext) -> InputDataController { let actionsDisposable = DisposableSet() @@ -291,7 +291,7 @@ func BusinessLocationController(context: AccountContext, peerId: PeerId) -> Inpu statePromise.set(stateValue.modify (f)) } - let chatInteraction = ChatInteraction(chatLocation: .peer(peerId), context: context) + let chatInteraction = ChatInteraction(chatLocation: .peer(context.peerId), context: context) chatInteraction.sendLocation = { location, venue in diff --git a/Telegram-Mac/BusinessQuickReplyController.swift b/Telegram-Mac/BusinessQuickReplyController.swift index 511576732..34d93f341 100644 --- a/Telegram-Mac/BusinessQuickReplyController.swift +++ b/Telegram-Mac/BusinessQuickReplyController.swift @@ -330,7 +330,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { } -func BusinessQuickReplyController(context: AccountContext, peerId: PeerId) -> InputDataController { +func BusinessQuickReplyController(context: AccountContext) -> InputDataController { let actionsDisposable = DisposableSet() diff --git a/Telegram-Mac/FastSettings.swift b/Telegram-Mac/FastSettings.swift index 1cf5d204f..a7e192786 100644 --- a/Telegram-Mac/FastSettings.swift +++ b/Telegram-Mac/FastSettings.swift @@ -635,7 +635,8 @@ class FastSettings { PremiumValue.peer_colors.rawValue, PremiumValue.saved_tags.rawValue, PremiumValue.last_seen.rawValue, - PremiumValue.message_privacy.rawValue] + PremiumValue.message_privacy.rawValue, + PremiumValue.business.rawValue] let dismissedPerks = UserDefaults.standard.value(forKey: "dismissedPerks") as? [String] ?? [] return perks.filter { !dismissedPerks.contains($0) } } diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index ee9a0dbbe..0bcc1a9db 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259874 + 259882 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/PremiumBoardingController.swift b/Telegram-Mac/PremiumBoardingController.swift index 33469ec43..651cbf65f 100644 --- a/Telegram-Mac/PremiumBoardingController.swift +++ b/Telegram-Mac/PremiumBoardingController.swift @@ -269,6 +269,8 @@ enum PremiumValue : String { case last_seen case message_privacy + case business + case business_location case business_hours case business_quick_replies @@ -287,6 +289,7 @@ enum PremiumValue : String { func gradient(_ index: Int) -> [NSColor] { let colors:[NSColor] = [ NSColor(rgb: 0xef6922), + NSColor(rgb: 0xD6593E), NSColor(rgb: 0xe95a2c), NSColor(rgb: 0xe74e33), NSColor(rgb: 0xe54837), @@ -399,6 +402,8 @@ enum PremiumValue : String { return NSImage(resource: .iconPremiumBoardingLastSeen).precomposed(presentation.colors.accent) case .message_privacy: return NSImage(resource: .iconPremiumBoardingMessagePrivacy).precomposed(presentation.colors.accent) + case .business: + return NSImage(resource: .iconPremiumBoardingBusiness).precomposed(presentation.colors.accent) case .business_location: return NSImage(resource: .iconPremiumBusinessLocation).precomposed(presentation.colors.accent) case .business_hours: @@ -454,6 +459,8 @@ enum PremiumValue : String { return strings().premiumBoardingLastSeenTitle case .message_privacy: return strings().premiumBoardingMessagePrivacyTitle + case .business: + return "Telegram Business" case .business_location: //TODOLANG return "Location" @@ -509,6 +516,8 @@ enum PremiumValue : String { return strings().premiumBoardingLastSeenInfo case .message_privacy: return strings().premiumBoardingMessagePrivacyInfo + case .business: + return "Upgrade your account with business features such as location, opening hours and quick replies." case .business_location: //TODOLANG return "Display the location of your business on your account." @@ -837,7 +846,7 @@ private final class PremiumBoardingView : View { } - private let headerView: HeaderView + let headerView: HeaderView let tableView = TableView() private var bottomView: View? private let bottomBorder = View() @@ -1167,6 +1176,14 @@ final class PremiumBoardingController : ModalViewController { super.loadView() } + override var defaultBarTitle: String { + if source == .business_standalone { + //TODOLANG + return "Telegram Business" + } + return super.defaultBarTitle + } + override func viewDidLoad() { super.viewDidLoad() @@ -1190,7 +1207,13 @@ final class PremiumBoardingController : ModalViewController { canMakePayment = inAppPurchaseManager.canMakePayments #endif - let initialState = State(values: context.premiumOrder.premiumValues, source: source, isPremium: context.isPremium, premiumConfiguration: PremiumPromoConfiguration.defaultValue, stickers: [], canMakePayment: canMakePayment, newPerks: FastSettings.premiumPerks) + var initialState = State(values: context.premiumOrder.premiumValues, source: source, isPremium: context.isPremium, premiumConfiguration: PremiumPromoConfiguration.defaultValue, stickers: [], canMakePayment: canMakePayment, newPerks: FastSettings.premiumPerks) + + #if DEBUG + if source != .business && source != .business_standalone { + initialState.values.insert(.business, at: 1) + } + #endif let statePromise: ValuePromise = ValuePromise(ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -1215,9 +1238,30 @@ final class PremiumBoardingController : ModalViewController { close() }, openFeature: { [weak self] value, animated in + guard let strongSelf = self else { return } + + if strongSelf.source == .business_standalone { + switch value { + case .business_location: + strongSelf.navigationController?.push(BusinessLocationController(context: context)) + case .business_hours: + strongSelf.navigationController?.push(BusinessHoursController(context: context)) + case .business_quick_replies: + strongSelf.navigationController?.push(BusinessQuickReplyController(context: context)) + case .business_greeting_messages: + strongSelf.navigationController?.push(BusinessAwayMessageController(context: context)) + case .business_away_messages: + strongSelf.navigationController?.push(BusinessAwayMessageController(context: context)) + case .business_chatbots: + strongSelf.navigationController?.push(BusinessChatbotController(context: context)) + default: + fatalError("not possible") + } + return + } strongSelf.genericView.append(PremiumBoardingFeaturesController(context, presentation: strongSelf.presentation, value: value, stickers: stateValue.with { $0.stickers }, configuration: stateValue.with { $0.premiumConfiguration }, back: { [weak strongSelf] in _ = strongSelf?.escapeKeyAction() }, makeAcceptView: { [weak strongSelf] in @@ -1510,6 +1554,9 @@ final class PremiumBoardingController : ModalViewController { close() } } + + genericView.headerView.isHidden = source == .business_standalone + genericView.accept = { diff --git a/Telegram-Mac/PremiumBoardingFeaturesController.swift b/Telegram-Mac/PremiumBoardingFeaturesController.swift index b3db8050c..a6173373d 100644 --- a/Telegram-Mac/PremiumBoardingFeaturesController.swift +++ b/Telegram-Mac/PremiumBoardingFeaturesController.swift @@ -89,30 +89,49 @@ final class PremiumBoardingFeaturesView: View { let bounds = slideView.bounds + let navigationGray: CGImage = NSImage(resource: .iconChatNavigationBack).precomposed(presentation.colors.grayIcon) + let navigationWhite: CGImage = NSImage(resource: .iconChatNavigationBack).precomposed(.white) + let stories = PremiumFeatureSlideView(frame: slideView.bounds, presentation: presentation) stories.setup(context: context, type: .stories, decoration: .none, getView: { _ in - let view = PremiumBoardingStoriesView(frame: bounds, presentation: presentation) - view.initialize(context: context, initialSize: bounds.size) + let view = PremiumBoardingExtraFeaturesView(frame: bounds, presentation: presentation) + view.initialize(context: context, initialSize: bounds.size, list: PremiumBoardingExtraFeatureItem.stories, title: PremiumValue.stories.title(context.premiumLimits)) return view }) stories.appear = { [weak self] in - self?.dismiss.set(image: NSImage(named: "Icon_ChatNavigationBack")!.precomposed(presentation.colors.grayIcon), for: .Normal) + self?.dismiss.set(image: navigationGray, for: .Normal) } stories.disappear = { [weak self] in - self?.dismiss.set(image: NSImage(named: "Icon_ChatNavigationBack")!.precomposed(.white), for: .Normal) + self?.dismiss.set(image: navigationWhite, for: .Normal) } slideView.addSlide(stories) + + let business = PremiumFeatureSlideView(frame: slideView.bounds, presentation: presentation) + business.setup(context: context, type: .business, decoration: .none, getView: { _ in + let view = PremiumBoardingExtraFeaturesView(frame: bounds, presentation: presentation) + view.initialize(context: context, initialSize: bounds.size, list: PremiumBoardingExtraFeatureItem.business, title: PremiumValue.business.title(context.premiumLimits)) + return view + }) + + business.appear = { [weak self] in + self?.dismiss.set(image: navigationGray, for: .Normal) + } + business.disappear = { [weak self] in + self?.dismiss.set(image: navigationWhite, for: .Normal) + } + + slideView.addSlide(business) let double_limits = PremiumFeatureSlideView(frame: slideView.bounds, presentation: presentation) double_limits.appear = { [weak self] in - self?.dismiss.set(image: NSImage(named: "Icon_ChatNavigationBack")!.precomposed(presentation.colors.grayIcon), for: .Normal) + self?.dismiss.set(image: navigationGray, for: .Normal) } double_limits.disappear = { [weak self] in - self?.dismiss.set(image: NSImage(named: "Icon_ChatNavigationBack")!.precomposed(.white), for: .Normal) + self?.dismiss.set(image: navigationWhite, for: .Normal) } double_limits.setup(context: context, type: .double_limits, decoration: .none, getView: { _ in @@ -273,44 +292,46 @@ final class PremiumBoardingFeaturesView: View { slideView.addSlide(messagesPrivacy) switch value { - case .double_limits: - slideView.displaySlide(at: 1, animated: false) case .stories: slideView.displaySlide(at: 0, animated: false) - case .more_upload: + case .business: + slideView.displaySlide(at: 1, animated: false) + case .double_limits: slideView.displaySlide(at: 2, animated: false) - case .faster_download: + case .more_upload: slideView.displaySlide(at: 3, animated: false) - case .voice_to_text: + case .faster_download: slideView.displaySlide(at: 4, animated: false) - case .no_ads: + case .voice_to_text: slideView.displaySlide(at: 5, animated: false) - case .infinite_reactions: + case .no_ads: slideView.displaySlide(at: 6, animated: false) - case .emoji_status: + case .infinite_reactions: slideView.displaySlide(at: 7, animated: false) - case .premium_stickers: + case .emoji_status: slideView.displaySlide(at: 8, animated: false) - case .animated_emoji: + case .premium_stickers: slideView.displaySlide(at: 9, animated: false) - case .advanced_chat_management: + case .animated_emoji: slideView.displaySlide(at: 10, animated: false) - case .profile_badge: + case .advanced_chat_management: slideView.displaySlide(at: 11, animated: false) - case .animated_userpics: + case .profile_badge: slideView.displaySlide(at: 12, animated: false) - case .translations: + case .animated_userpics: slideView.displaySlide(at: 13, animated: false) - case .peer_colors: + case .translations: slideView.displaySlide(at: 14, animated: false) - case .wallpapers: + case .peer_colors: slideView.displaySlide(at: 15, animated: false) - case .saved_tags: + case .wallpapers: slideView.displaySlide(at: 16, animated: false) - case .last_seen: + case .saved_tags: slideView.displaySlide(at: 17, animated: false) - case .message_privacy: + case .last_seen: slideView.displaySlide(at: 18, animated: false) + case .message_privacy: + slideView.displaySlide(at: 19, animated: false) case .business_location: fatalError() case .business_hours: diff --git a/Telegram-Mac/PremiumBoardingStoriesController.swift b/Telegram-Mac/PremiumBoardingStoriesController.swift index 25ece2cfb..8a5e9211a 100644 --- a/Telegram-Mac/PremiumBoardingStoriesController.swift +++ b/Telegram-Mac/PremiumBoardingStoriesController.swift @@ -12,9 +12,9 @@ import TGUIKit final class PremiumBoardingStoryRowItem : GeneralRowItem { let title: TextViewLayout let info: TextViewLayout - let itemType: PremiumBoardingStoriesItem + let itemType: PremiumBoardingExtraFeatureItem let presentation: TelegramPresentationTheme - init(_ initialSize: NSSize, type: PremiumBoardingStoriesItem, presentation: TelegramPresentationTheme) { + init(_ initialSize: NSSize, type: PremiumBoardingExtraFeatureItem, presentation: TelegramPresentationTheme) { self.itemType = type self.presentation = presentation self.title = .init(.initialize(string: type.title, color: presentation.colors.text, font: .medium(.title))) @@ -108,7 +108,7 @@ private final class PremiumBoardingStoriesRowView : TableRowView { -enum PremiumBoardingStoriesItem { +enum PremiumBoardingExtraFeatureItem { case priority case stealth case permanentViews @@ -117,7 +117,16 @@ enum PremiumBoardingStoriesItem { case longerCaption case linksAndFormating case highQuality - static var all: [PremiumBoardingStoriesItem] { + + case business_location + case business_hours + case business_quick_replies + case business_greeting_messages + case business_away_messages + case business_chatbots + + + static var stories: [PremiumBoardingExtraFeatureItem] { return [.priority, .stealth, .highQuality, @@ -128,6 +137,16 @@ enum PremiumBoardingStoriesItem { .linksAndFormating] } + static var business: [PremiumBoardingExtraFeatureItem] { + return [.business_location, + .business_hours, + .business_quick_replies, + .business_greeting_messages, + .business_away_messages] + } + + + var title: String { switch self { case .priority: @@ -146,6 +165,19 @@ enum PremiumBoardingStoriesItem { return strings().premiumBoardingStoriesLinkFormattingTitle case .highQuality: return strings().premiumBoardingStoriesHighQualityTitle + case .business_location: + //TODOLANG + return "Location" + case .business_hours: + return "Opening Hours" + case .business_quick_replies: + return "Quick Replies" + case .business_greeting_messages: + return "Greeting Messages" + case .business_away_messages: + return "Away Messages" + case .business_chatbots: + return "ChatBots" } } var info: String { @@ -166,35 +198,60 @@ enum PremiumBoardingStoriesItem { return strings().premiumBoardingStoriesLinkFormattingInfo case .highQuality: return strings().premiumBoardingStoriesHighQualityInfo + case .business_location: + //TODOLANG + return "Display the location of your business on your account." + case .business_hours: + return "Show to your customers when you are open for business." + case .business_quick_replies: + return "Set up shortcuts with rich text and media to respond to messages faster." + case .business_greeting_messages: + return "Create greetings that will be automatically sent to new customers." + case .business_away_messages: + return "Define messages that are automatically sent when you are off." + case .business_chatbots: + return "Add any third party chatbots that will process customer interactions." } } var color: NSColor { return .random } - var image: CGImage? { + var image: CGImage { switch self { case .priority: - return NSImage(named: "Icon_PremiumBoarding_PriorityOrder")?.precomposed() + return NSImage(resource: .iconPremiumBoardingPriorityOrder).precomposed() case .stealth: - return NSImage(named: "Icon_PremiumBoarding_StealthMode")?.precomposed() + return NSImage(resource: .iconPremiumBoardingStealthMode).precomposed() case .permanentViews: - return NSImage(named: "Icon_PremiumBoarding_PermanentViewsHistory")?.precomposed() + return NSImage(resource: .iconPremiumBoardingPermanentViewsHistory).precomposed() case .expiratationDuration: - return NSImage(named: "Icon_PremiumBoarding_ExpirationDurations")?.precomposed() + return NSImage(resource: .iconPremiumBoardingExpirationDurations).precomposed() case .saveToGallery: - return NSImage(named: "Icon_PremiumBoarding_SaveStoriesToGallery")?.precomposed() + return NSImage(resource: .iconPremiumBoardingSaveStoriesToGallery).precomposed() case .longerCaption: - return NSImage(named: "Icon_PremiumBoarding_LongerCaptions")?.precomposed() + return NSImage(resource: .iconPremiumBoardingLongerCaptions).precomposed() case .linksAndFormating: - return NSImage(named: "Icon_PremiumBoarding_LinksAndFormatting")?.precomposed() + return NSImage(resource: .iconPremiumBoardingLinksAndFormatting).precomposed() case .highQuality: - return NSImage(named: "Icon_PremiumBoarding_HighQuality")?.precomposed(NSColor(0x9A64EE)) + return NSImage(resource: .iconPremiumBoardingHighQuality).precomposed(NSColor(0x9A64EE)) + case .business_location: + return NSImage(resource: .iconPremiumBusinessFeatureLocation).precomposed() + case .business_hours: + return NSImage(resource: .iconPremiumBusinessFeatureHours).precomposed() + case .business_quick_replies: + return NSImage(resource: .iconPremiumBusinessFeatureReply).precomposed() + case .business_greeting_messages: + return NSImage(resource: .iconPremiumBusinessFeatureGreeting).precomposed() + case .business_away_messages: + return NSImage(resource: .iconPremiumBusinessFeatureAway).precomposed() + case .business_chatbots: + return NSImage(resource: .iconPremiumBusinessFeatureBot).precomposed() } } } -final class PremiumBoardingStoriesView: View, PremiumSlideView { +final class PremiumBoardingExtraFeaturesView: View, PremiumSlideView { final class HeaderView: View { private let container = View() @@ -223,6 +280,13 @@ final class PremiumBoardingStoriesView: View, PremiumSlideView { } + func set(string: String) { + let layout = TextViewLayout(.initialize(string: string, color: presentation.colors.text, font: .medium(.header))) + layout.measure(width: 300) + + titleView.update(layout) + } + override func layout() { super.layout() @@ -279,10 +343,13 @@ final class PremiumBoardingStoriesView: View, PremiumSlideView { } - func initialize(context: AccountContext, initialSize: NSSize) { + func initialize(context: AccountContext, initialSize: NSSize, list: [PremiumBoardingExtraFeatureItem], title: String) { + + self.headerView.set(string: title) + _ = self.tableView.addItem(item: GeneralRowItem(initialSize, height: 15, backgroundColor: presentation.colors.background)) - for type in PremiumBoardingStoriesItem.all { + for type in list { let item = PremiumBoardingStoryRowItem(initialSize, type: type, presentation: presentation) _ = self.tableView.addItem(item: item) _ = self.tableView.addItem(item: GeneralRowItem(initialSize, height: 15, backgroundColor: presentation.colors.background)) diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 78d917cce..3f38a5cdc 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259874 + 259882 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/submodules/telegram-ios b/submodules/telegram-ios index 180e1f814..f4e351ab2 160000 --- a/submodules/telegram-ios +++ b/submodules/telegram-ios @@ -1 +1 @@ -Subproject commit 180e1f8141aed2af4d2a7dc859590f96b4d09bfd +Subproject commit f4e351ab2ff87d8a83b60ec834c9bc1802b3493a From cc07b4991d650f26c9ad391b1cc0d54d1bc898d9 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 21 Feb 2024 09:54:42 +0000 Subject: [PATCH 19/50] business --- Telegram-Mac/AccountViewController.swift | 2 +- .../BusinessAwayMessageController.swift | 155 ++++++- Telegram-Mac/BusinessLocationController.swift | 16 +- .../BusinessQuickReplyController.swift | 121 ++++-- Telegram-Mac/ChatController.swift | 188 +++++++-- Telegram-Mac/ChatEmptyPeerItem.swift | 2 + Telegram-Mac/ChatHistoryViewForLocation.swift | 20 +- Telegram-Mac/ChatInputView.swift | 2 +- Telegram-Mac/ChatInterfaceInteraction.swift | 4 + Telegram-Mac/ChatListController.swift | 2 +- Telegram-Mac/ChatMapRowItem.swift | 6 +- Telegram-Mac/ChatMessageItem.swift | 28 +- Telegram-Mac/ChatMessageMenuItems.swift | 2 +- .../ChatPresentationInterfaceState.swift | 5 + Telegram-Mac/ChatRowItem.swift | 7 +- Telegram-Mac/ChatRowView.swift | 3 + Telegram-Mac/ChatSwitchInlineController.swift | 2 + Telegram-Mac/ChatTitleBarView.swift | 6 +- Telegram-Mac/CoreExtension.swift | 25 +- Telegram-Mac/FastSettings.swift | 3 +- Telegram-Mac/GalleryViewer.swift | 17 + Telegram-Mac/GeneralRowView.swift | 7 +- .../GreetingMessageSetupChatContents.swift | 227 +++++++++++ Telegram-Mac/Info.plist | 2 +- Telegram-Mac/LiveLocationViewController.swift | 11 +- Telegram-Mac/NetworkStatusManager.swift | 4 +- Telegram-Mac/PeerInfoBusinessItems.swift | 385 ++++++++++++++++++ Telegram-Mac/PremiumBoardingController.swift | 12 +- .../PremiumBoardingFeaturesController.swift | 10 + Telegram-Mac/PremiumBoardingHeaderItem.swift | 166 ++++---- Telegram-Mac/PremiumCoinSceneView.swift | 311 ++++++++++++++ Telegram-Mac/PremiumLimitConfig.swift | 7 +- Telegram-Mac/PremiumStarSceneView.swift | 11 +- Telegram-Mac/PreviewSenderController.swift | 2 +- .../PrivacyAndSecurityViewController.swift | 2 +- Telegram-Mac/SenderController.swift | 57 ++- Telegram-Mac/UserInfoEntries.swift | 124 ++++-- Telegram-Mac/premium/coin.png | Bin 0 -> 16191 bytes Telegram-Mac/premium/coin.scn | Bin 0 -> 844313 bytes Telegram-Mac/premium/coin_anim.png | Bin 0 -> 61046 bytes Telegram-Mac/premium/coin_edge.png | Bin 0 -> 332 bytes Telegram-Mac/premium/darkerTexture.jpg | Bin 0 -> 9624 bytes Telegram-Mac/premium/diagonal_shine.png | Bin 0 -> 11771 bytes Telegram-Mac/premium/lighterTexture.jpg | Bin 0 -> 9269 bytes Telegram.xcodeproj/project.pbxproj | 40 ++ TelegramShare/Info.plist | 2 +- packages/TGUIKit/Sources/Extensions.swift | 8 + packages/TGUIKit/Sources/TextView.swift | 263 +----------- submodules/telegram-ios | 2 +- 49 files changed, 1748 insertions(+), 521 deletions(-) create mode 100644 Telegram-Mac/GreetingMessageSetupChatContents.swift create mode 100644 Telegram-Mac/PeerInfoBusinessItems.swift create mode 100644 Telegram-Mac/PremiumCoinSceneView.swift create mode 100644 Telegram-Mac/premium/coin.png create mode 100644 Telegram-Mac/premium/coin.scn create mode 100644 Telegram-Mac/premium/coin_anim.png create mode 100644 Telegram-Mac/premium/coin_edge.png create mode 100644 Telegram-Mac/premium/darkerTexture.jpg create mode 100644 Telegram-Mac/premium/diagonal_shine.png create mode 100644 Telegram-Mac/premium/lighterTexture.jpg diff --git a/Telegram-Mac/AccountViewController.swift b/Telegram-Mac/AccountViewController.swift index 40b71ae1e..db1cce889 100644 --- a/Telegram-Mac/AccountViewController.swift +++ b/Telegram-Mac/AccountViewController.swift @@ -880,7 +880,7 @@ class AccountViewController : TelegramGenericViewControllerVoid + init(_ initialSize: NSSize, stableId: AnyHashable, messages: [Message], myPeer: EnginePeer, context: AccountContext, viewType: GeneralViewType, open: @escaping()->Void) { + self.messages = messages + self.myPeer = myPeer + self.open = open + self.context = context + let attr = chatListText(account: context.account, for: messages[0]) + + self.textLayout = TextViewLayout(attr) + self.titleLayout = TextViewLayout(.initialize(string: myPeer._asPeer().displayTitle, color: theme.colors.text, font: .medium(.title))) + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + + private var textWidth: CGFloat { + var width = blockWidth + width -= (leftInset + viewType.innerInset.left) + + width -= 60 // photo + width -= viewType.innerInset.left // photo + + return width + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + + textLayout.measure(width: textWidth) + titleLayout.measure(width: textWidth) + + return true + } + + override func viewClass() -> AnyClass { + return MessageRowItemView.self + } + + override var height: CGFloat { + return 70 + } + + var leftInset: CGFloat { + return 20 + } +} + +private final class MessageRowItemView: GeneralContainableRowView { + private let textView = TextView() + private let titleView = TextView() + private let imageView = AvatarControl(font: .avatar(15)) + private let container = View() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(container) + container.addSubview(imageView) + container.addSubview(textView) + container.addSubview(titleView) + + imageView.setFrameSize(NSMakeSize(50, 50)) + + imageView.layer?.cornerRadius = imageView.frame.height / 2 + + textView.userInteractionEnabled = false + textView.isSelectable = false + + titleView.userInteractionEnabled = false + titleView.isSelectable = false + + containerView.set(handler: { [weak self] _ in + if let item = self?.item as? MessageRowItem { + item.open() + } + }, for: .Click) + + containerView.scaleOnClick = true + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? MessageRowItem else { + return + } + + imageView.setPeer(account: item.context.account, peer: item.myPeer._asPeer()) + + textView.update(item.textLayout) + titleView.update(item.titleLayout) + + self.updateLayout(size: frame.size, transition: animated ? .animated(duration: 0.2, curve: .easeOut) : .immediate) + } + + override func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + super.updateLayout(size: size, transition: transition) + + guard let item = item as? MessageRowItem else { + return + } + + let contentInset: CGFloat = 16 + + let containerRect = NSMakeRect(contentInset, 0, containerView.frame.width - contentInset, containerView.frame.height) + + transition.updateFrame(view: container, frame: containerRect) + + transition.updateFrame(view: imageView, frame: imageView.centerFrameY(x: 0)) + + transition.updateFrame(view: titleView, frame: CGRect(origin: NSMakePoint(imageView.frame.maxX + 10, imageView.frame.minY), size: titleView.frame.size)) + + transition.updateFrame(view: textView, frame: CGRect(origin: NSMakePoint(imageView.frame.maxX + 10, titleView.frame.maxY + 6), size: textView.frame.size)) + } +} + + + class BusinessSelectChatsCallbackObject : ShareObject { private let callback:([PeerId])->Signal private let limitReachedText: String @@ -119,7 +249,8 @@ private struct State : Equatable { } var enabled: Bool = false - + var messages: [Message] = [] + var myPeer: EnginePeer? var schedule: Schedule = .alwaysSend var recepient: Recepient = .all @@ -182,7 +313,14 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { entries.append(.desc(sectionId: sectionId, index: index, text: .plain("AWAY MESSAGE"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) index += 1 - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_create_message, data: .init(name: "Create an Away Message", color: theme.colors.accent, icon: theme.icons.create_new_message_general, type: .next, viewType: .singleItem, action: arguments.createMessage))) + if state.messages.isEmpty { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_create_message, data: .init(name: "Create an Away Message", color: theme.colors.accent, icon: theme.icons.create_new_message_general, type: .next, viewType: .singleItem, action: arguments.createMessage))) + } else if let myPeer = state.myPeer { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_message, equatable: .init(state), comparable: nil, item: { initialSize, stableId in + return MessageRowItem(initialSize, stableId: stableId, messages: state.messages, myPeer: myPeer, context: arguments.context, viewType: .singleItem, open: arguments.createMessage) + })) + } + entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 @@ -337,6 +475,17 @@ func BusinessAwayMessageController(context: AccountContext) -> InputDataControll return current } })) + + let messages = GreetingMessageSetupChatContents(context: context, messages: [], kind: .awayMessageInput) + + actionsDisposable.add(combineLatest(messages.messages, context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.peerId))).startStandalone(next: { messages, myPeer in + updateState { current in + var current = current + current.messages = messages + current.myPeer = myPeer + return current + } + })) let arguments = Arguments(context: context, toggleEnabled: { updateState { current in @@ -345,7 +494,7 @@ func BusinessAwayMessageController(context: AccountContext) -> InputDataControll return current } }, createMessage: { - + context.bindings.rootNavigation().push(ChatAdditionController(context: context, chatLocation: .peer(context.peerId),mode: .customChatContents(contents: messages))) }, toggleSchedule: { schedule in updateState { current in var current = current diff --git a/Telegram-Mac/BusinessLocationController.swift b/Telegram-Mac/BusinessLocationController.swift index 9b37a8382..20aab1666 100644 --- a/Telegram-Mac/BusinessLocationController.swift +++ b/Telegram-Mac/BusinessLocationController.swift @@ -213,10 +213,12 @@ private final class Arguments { let context: AccountContext let setLocation:()->Void let openMap:()->Void - init(context: AccountContext, setLocation:@escaping()->Void, openMap:@escaping()->Void) { + let remove:()->Void + init(context: AccountContext, setLocation:@escaping()->Void, openMap:@escaping()->Void, remove:@escaping()->Void) { self.context = context self.setLocation = setLocation self.openMap = openMap + self.remove = remove } } @@ -239,6 +241,8 @@ private let _id_input = InputDataIdentifier("_id_enabled") private let _id_map_enabled = InputDataIdentifier("_id_map_enabled") private let _id_map_map = InputDataIdentifier("_id_map_enabled") +private let _id_remove = InputDataIdentifier("_id_remove") + private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { var entries:[InputDataEntry] = [] @@ -267,8 +271,10 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { if let location = state.location { entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_map_map, equatable: .init(state.location), comparable: nil, item: { initialSize, stableId in - return MapRowItem(initialSize, height: 200, stableId: stableId, context: arguments.context, location: location, viewType: .lastItem, action: arguments.openMap) + return MapRowItem(initialSize, height: 200, stableId: stableId, context: arguments.context, location: location, viewType: .innerItem, action: arguments.openMap) })) + + entries.append(.general(sectionId: sectionId, index: 0, value: .none, error: nil, identifier: _id_remove, data: .init(name: "Remove", color: theme.colors.redUI, type: .none, viewType: .lastItem, action: arguments.setLocation, autoswitch: false))) } // entries @@ -328,6 +334,12 @@ func BusinessLocationController(context: AccountContext) -> InputDataController } }, openMap: { showModal(with: LocationModalController(chatInteraction, destination: .business(stateValue.with { $0.location?.coordinate })), for: context.window) + }, remove: { + updateState { current in + var current = current + current.location = nil + return current + } }) let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in diff --git a/Telegram-Mac/BusinessQuickReplyController.swift b/Telegram-Mac/BusinessQuickReplyController.swift index 34d93f341..1a55601c5 100644 --- a/Telegram-Mac/BusinessQuickReplyController.swift +++ b/Telegram-Mac/BusinessQuickReplyController.swift @@ -34,9 +34,14 @@ private final class QuickReplyRowItem : GeneralRowItem { let attr = NSMutableAttributedString() attr.append(string: "/\(reply.name)", color: theme.colors.text, font: .normal(.text)) attr.append(string: " ", color: theme.colors.text, font: .normal(.text)) - attr.append(string: reply.messages[0], color: theme.colors.grayText, font: .normal(.text)) - self.textLayout = TextViewLayout(attr) + let texts = reply.messages.map { + chatListText(account: context.account, for: $0).string + }.joined(separator: ", ") + + attr.append(string: texts, color: theme.colors.grayText, font: .normal(.text)) + + self.textLayout = TextViewLayout(attr, maximumNumberOfLines: 4) super.init(initialSize, stableId: stableId, viewType: viewType) } @@ -78,7 +83,7 @@ private final class QuickReplyRowItem : GeneralRowItem { } override var height: CGFloat { - return 44 + return max(44, 10 + textLayout.layoutSize.height) } var leftInset: CGFloat { @@ -88,7 +93,7 @@ private final class QuickReplyRowItem : GeneralRowItem { private final class QuickReplyRowItemView: GeneralContainableRowView { private let textView = TextView() - private let imageView = ImageView(frame: NSMakeRect(0, 0, 30, 30)) + private let imageView = AvatarControl(font: .avatar(10)) private let container = View() private var remove: ImageButton? @@ -99,11 +104,18 @@ private final class QuickReplyRowItemView: GeneralContainableRowView { container.addSubview(imageView) container.addSubview(textView) - imageView.layer?.backgroundColor = theme.colors.grayIcon.cgColor + imageView.setFrameSize(NSMakeSize(30, 30)) + imageView.layer?.cornerRadius = imageView.frame.height / 2 textView.userInteractionEnabled = false textView.isSelectable = false + + containerView.set(handler: { [weak self] _ in + if let item = self?.item as? QuickReplyRowItem { + item.open(item.reply) + } + }, for: .Click) } required init?(coder: NSCoder) { @@ -117,6 +129,8 @@ private final class QuickReplyRowItemView: GeneralContainableRowView { return } + imageView.setPeer(account: item.context.account, peer: item.context.myPeer) + textView.update(item.textLayout) if item.editing { @@ -204,7 +218,7 @@ private final class QuickReplyRowItemView: GeneralContainableRowView { } } - self.updateLayout(size: frame.size, transition: animated ? .animated(duration: 0.2, curve: .easeOut) : .immediate) +// self.updateLayout(size: nsmakesi, transition: animated ? .animated(duration: 0.2, curve: .easeOut) : .immediate) } override func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { @@ -229,7 +243,7 @@ private final class QuickReplyRowItemView: GeneralContainableRowView { transition.updateFrame(view: sort, frame: sort.centerFrameY(x: containerView.frame.width - sort.frame.width - item.viewType.innerInset.right)) } - transition.updateFrame(view: imageView, frame: imageView.centerFrameY(x: 0)) + transition.updateFrame(view: imageView, frame: CGRect(origin: NSMakePoint(0, 7), size: imageView.frame.size)) transition.updateFrame(view: textView, frame: textView.centerFrameY(x: imageView.frame.maxX + 10)) } } @@ -239,21 +253,23 @@ private final class Arguments { let context: AccountContext let add:()->Void let edit:(State.Reply)->Void + let open:(State.Reply)->Void let editName:(State.Reply)->Void let remove:(State.Reply)->Void - init(context: AccountContext, add:@escaping()->Void, edit:@escaping(State.Reply)->Void, remove:@escaping(State.Reply)->Void, editName:@escaping(State.Reply)->Void) { + init(context: AccountContext, add:@escaping()->Void, edit:@escaping(State.Reply)->Void, remove:@escaping(State.Reply)->Void, editName:@escaping(State.Reply)->Void, open:@escaping(State.Reply)->Void) { self.context = context self.add = add self.edit = edit self.remove = remove self.editName = editName + self.open = open } } private struct State : Equatable { struct Reply : Equatable { var name: String - var messages: [String] + var messages: [Message] var id: Int64 } @@ -319,7 +335,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { for tuple in tuples { entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_reply(tuple.reply.id), equatable: .init(tuple), comparable: nil, item: { initialSize, stableId in - return QuickReplyRowItem(initialSize, stableId: stableId, reply: tuple.reply, context: arguments.context, editing: tuple.editing, viewType: tuple.viewType, open: arguments.edit, editName: arguments.editName, remove: arguments.remove) + return QuickReplyRowItem(initialSize, stableId: stableId, reply: tuple.reply, context: arguments.context, editing: tuple.editing, viewType: tuple.viewType, open: arguments.open, editName: arguments.editName, remove: arguments.remove) })) } @@ -343,10 +359,12 @@ func BusinessQuickReplyController(context: AccountContext) -> InputDataControlle } let nextTransactionNonAnimated = Atomic(value: false) + + let arguments = Arguments(context: context, add: { - showModal(with: BusinessAddQuickReply(context: context, stateSignal: statePromise.get(), stateValue: stateValue, updateState: updateState, reply: nil), for: context.window) + showModal(with: BusinessAddQuickReply(context: context, actionsDisposable: actionsDisposable, stateSignal: statePromise.get(), stateValue: stateValue, updateState: updateState, reply: nil), for: context.window) }, edit: { reply in }, remove: { reply in @@ -356,7 +374,28 @@ func BusinessQuickReplyController(context: AccountContext) -> InputDataControlle return current } }, editName: { reply in - showModal(with: BusinessAddQuickReply(context: context, stateSignal: statePromise.get(), stateValue: stateValue, updateState: updateState, reply: reply), for: context.window) + showModal(with: BusinessAddQuickReply(context: context, actionsDisposable: actionsDisposable, stateSignal: statePromise.get(), stateValue: stateValue, updateState: updateState, reply: reply), for: context.window) + }, open: { reply in + var reply = reply + + let messages = GreetingMessageSetupChatContents(context: context, messages: reply.messages.map(EngineMessage.init), kind: .quickReplyMessageInput(shortcut: reply.name)) + + context.bindings.rootNavigation().push(ChatAdditionController(context: context, chatLocation: .peer(context.peerId),mode: .customChatContents(contents: messages))) + + actionsDisposable.add(messages.messages.start(next: { messages in + updateState { current in + var current = current + reply.messages = messages + if messages.isEmpty { + current.replies.removeAll(where: { $0.id == reply.id }) + } else if let index = current.replies.firstIndex(where: { $0.id == reply.id }) { + current.replies[index] = reply + } else { + current.replies.append(reply) + } + return current + } + })) }) let signal = statePromise.get() |> deliverOnMainQueue |> map { state in @@ -457,12 +496,10 @@ private func newReplyEntries(_ state: State) -> [InputDataEntry] { return entries } -private func BusinessAddQuickReply(context: AccountContext, stateSignal: Signal, stateValue: Atomic, updateState: @escaping((State) -> State) -> Void, reply: State.Reply?) -> InputDataModalController { +private func BusinessAddQuickReply(context: AccountContext, actionsDisposable: DisposableSet, stateSignal: Signal, stateValue: Atomic, updateState: @escaping((State) -> State) -> Void, reply: State.Reply?) -> InputDataModalController { var close:(()->Void)? = nil - let actionsDisposable = DisposableSet() - updateState { current in var current = current current.creatingName = reply?.name @@ -473,12 +510,9 @@ private func BusinessAddQuickReply(context: AccountContext, stateSignal: Signal< return InputDataSignalValue(entries: newReplyEntries(state)) } - let controller = InputDataController(dataSignal: signal, title: "New Quick Reply") - controller.onDeinit = { - actionsDisposable.dispose() - } - + let controller = InputDataController(dataSignal: signal, title: reply == nil ? "New Quick Reply" : "Edit Quick Reply") + let modalInteractions = ModalInteractions(acceptTitle: strings().modalDone, accept: { [weak controller] in _ = controller?.returnKeyAction() }, singleButton: true) @@ -503,6 +537,10 @@ private func BusinessAddQuickReply(context: AccountContext, stateSignal: Signal< let value = data[_id_input]?.stringValue + guard let value else { + return .fail(.fields([_id_input : .shake])) + } + let replies = stateValue.with { $0.replies } let contains = replies.contains(where: { $0.name == value }) @@ -515,21 +553,46 @@ private func BusinessAddQuickReply(context: AccountContext, stateSignal: Signal< return .fail(.fields([_id_input : .shake])) } - if value?.isEmpty == true { + if value.isEmpty { return .fail(.fields([_id_input : .shake])) } - updateState { current in - var current = current - if let input = current.creatingName, !input.isEmpty { - if let index = current.replies.firstIndex(where: { $0.id == reply?.id }) { - current.replies[index].name = input - } else { - current.replies.append(.init(name: input, messages: ["text"], id: arc4random64())) + if reply == nil { + + var reply: State.Reply = .init(name: value, messages: [], id: arc4random64()) + + let messages = GreetingMessageSetupChatContents(context: context, messages: [], kind: .quickReplyMessageInput(shortcut: value)) + + context.bindings.rootNavigation().push(ChatAdditionController(context: context, chatLocation: .peer(context.peerId),mode: .customChatContents(contents: messages))) + + actionsDisposable.add(messages.messages.start(next: { messages in + updateState { current in + var current = current + reply.messages = messages + if messages.isEmpty { + current.replies.removeAll(where: { $0.id == reply.id }) + } else if let index = current.replies.firstIndex(where: { $0.id == reply.id }) { + current.replies[index] = reply + } else { + current.replies.append(reply) + } + return current } + })) + + } else { + updateState { current in + var current = current + if let input = current.creatingName, !input.isEmpty { + if let index = current.replies.firstIndex(where: { $0.id == reply?.id }) { + current.replies[index].name = input + } + } + return current } - return current } + + close?() return .none } diff --git a/Telegram-Mac/ChatController.swift b/Telegram-Mac/ChatController.swift index 8e37a9238..b4c57e10d 100644 --- a/Telegram-Mac/ChatController.swift +++ b/Telegram-Mac/ChatController.swift @@ -56,11 +56,89 @@ enum ReplyThreadMode : Equatable { } } +public enum ChatCustomContentsKind: Equatable { + case greetingMessageInput + case awayMessageInput + case quickReplyMessageInput(shortcut: String) + + //TODOLANG + var text: String { + switch self { + case .greetingMessageInput: + return "Greeting Messages" + case .awayMessageInput: + return "Away Messages" + case .quickReplyMessageInput(let shortcut): + return shortcut + } + } +} + + +public protocol ChatCustomContentsProtocol: AnyObject { + var kind: ChatCustomContentsKind { get } + var messages: Signal<[Message], NoError> { get } + var messageLimit: Int? { get } + + func enqueueMessages(messages: [EnqueueMessage]) -> Signal<[MessageId?],NoError> + func deleteMessages(ids: [EngineMessage.Id]) + func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) +} + + + enum ChatMode : Equatable { + static func == (lhs: ChatMode, rhs: ChatMode) -> Bool { + switch lhs { + case .history: + if case .history = rhs { + return true + } else { + return false + } + case .scheduled: + if case .scheduled = rhs { + return true + } else { + return false + } + case .pinned: + if case .pinned = rhs { + return true + } else { + return false + } + case let .thread(data, mode): + if case .thread(data, mode) = rhs { + return true + } else { + return false + } + case .customChatContents: + if case .customChatContents = rhs { + return true + } else { + return false + } + } + } + case history case scheduled case pinned case thread(data: ChatReplyThreadMessage, mode: ReplyThreadMode) + case customChatContents(contents: ChatCustomContentsProtocol) + + + var customChatContents: ChatCustomContentsProtocol? { + switch self { + case let .customChatContents(contents): + return contents + default: + return nil + } + } + var threadId: MessageId? { switch self { case let .thread(data, _): @@ -2086,6 +2164,7 @@ class ChatController: EditableViewController, Notifable, Tab let threadId = chatInteraction.mode.threadId let threadId64 = chatInteraction.mode.threadId64 let isThread = chatInteraction.mode.isThreadMode + let customChatContents = chatInteraction.mode.customChatContents let takeReplyId:()->EngineMessageReplySubject? = { [weak self] in var reply = self?.chatInteraction.presentation.interfaceState.replyMessageId @@ -2745,7 +2824,7 @@ class ChatController: EditableViewController, Notifable, Tab includeJoin = false } - let entries = messageEntries(msgEntries, location: chatLocation, maxReadIndex: maxReadIndex, dayGrouping: true, renderType: chatTheme.bubbled ? .bubble : .list, includeBottom: true, timeDifference: timeDifference, ranks: ranks, pollAnswersLoading: pollAnswersLoading, threadLoading: threadLoading, groupingPhotos: true, autoplayMedia: initialData.autoplayMedia, searchState: searchState, animatedEmojiStickers: bigEmojiEnabled ? animatedEmojiStickers : [:], topFixedMessages: topMessages, customChannelDiscussionReadState: customChannelDiscussionReadState, customThreadOutgoingReadState: customThreadOutgoingReadState, addRepliesHeader: peerId == repliesPeerId && view.earlierId == nil, updatingMedia: updatingMedia, adMessage: ads.fixed, dynamicAdMessages: ads.opportunistic, chatTheme: chatTheme, reactions: reactions, transribeState: uiState.transribe, topicCreatorId: uiState.topicCreatorId, mediaRevealed: uiState.mediaRevealed, translate: uiState.translate, storyState: uiState.storyState, peerStoryStats: view.peerStoryStats, cachedData: peerView?.cachedData, peer: peer, holeLater: view.holeLater, holeEarlier: view.holeEarlier, recommendedChannels: recommendedChannels, includeJoin: includeJoin, earlierId: view.earlierId, laterId: view.laterId, automaticDownload: initialData.autodownloadSettings, savedMessageTags: savedMessageTags).map { ChatWrappedEntry(appearance: AppearanceWrapperEntry(entry: $0, appearance: appearance), tag: view.tag) } + let entries = messageEntries(msgEntries, location: chatLocation, maxReadIndex: maxReadIndex, dayGrouping: customChatContents == nil, renderType: chatTheme.bubbled ? .bubble : .list, includeBottom: true, timeDifference: timeDifference, ranks: ranks, pollAnswersLoading: pollAnswersLoading, threadLoading: threadLoading, groupingPhotos: true, autoplayMedia: initialData.autoplayMedia, searchState: searchState, animatedEmojiStickers: bigEmojiEnabled ? animatedEmojiStickers : [:], topFixedMessages: topMessages, customChannelDiscussionReadState: customChannelDiscussionReadState, customThreadOutgoingReadState: customThreadOutgoingReadState, addRepliesHeader: peerId == repliesPeerId && view.earlierId == nil, updatingMedia: updatingMedia, adMessage: ads.fixed, dynamicAdMessages: ads.opportunistic, chatTheme: chatTheme, reactions: reactions, transribeState: uiState.transribe, topicCreatorId: uiState.topicCreatorId, mediaRevealed: uiState.mediaRevealed, translate: uiState.translate, storyState: uiState.storyState, peerStoryStats: view.peerStoryStats, cachedData: peerView?.cachedData, peer: peer, holeLater: view.holeLater, holeEarlier: view.holeEarlier, recommendedChannels: recommendedChannels, includeJoin: includeJoin, earlierId: view.earlierId, laterId: view.laterId, automaticDownload: initialData.autodownloadSettings, savedMessageTags: savedMessageTags).map { ChatWrappedEntry(appearance: AppearanceWrapperEntry(entry: $0, appearance: appearance), tag: view.tag) } proccesedView = ChatHistoryView(originalView: view, filteredEntries: entries, theme: chatTheme) } } else { @@ -3001,8 +3080,6 @@ class ChatController: EditableViewController, Notifable, Tab self?.chatInteraction.focusMessageId(nil, .init(messageId: messageId, string: nil), .top(id: 0, innerId: nil, animated: true, focus: .init(focus: false), inset: 30)) } })) - case .pinned: - break case .scheduled: var previousItem: ChatRowItem? strongSelf.genericView.tableView.enumerateItems(with: { item -> Bool in @@ -3027,6 +3104,10 @@ class ChatController: EditableViewController, Notifable, Tab if let previousItem = previousItem { self?.genericView.tableView.scroll(to: .top(id: previousItem.stableId, innerId: nil, animated: true, focus: .init(focus: false), inset: 30)) } + case .pinned: + break + case .customChatContents: + break } @@ -3075,6 +3156,19 @@ class ChatController: EditableViewController, Notifable, Tab media = state.editMedia } + if let customChatContents { + customChatContents.editMessage(id: state.message.id, text: inputState.inputText, media: media, entities: TextEntitiesMessageAttribute(entities: inputState.messageTextEntities()), webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: presentation.interfaceState.composeDisableUrlPreview != nil) + self.chatInteraction.beginEditingMessage(nil) + self.chatInteraction.update({ + $0.updatedInterfaceState({ + $0.withUpdatedComposeDisableUrlPreview(nil).updatedEditState({ + $0?.withUpdatedLoadingState(.none) + }) + }) + }) + return + } + if atDate == nil { self.context.account.pendingUpdateMessageManager.add(messageId: state.message.id, text: inputState.inputText, media: media, entities: TextEntitiesMessageAttribute(entities: inputState.messageTextEntities()), inlineStickers: inputState.inlineMedia, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: presentation.interfaceState.composeDisableUrlPreview != nil) @@ -3138,7 +3232,7 @@ class ChatController: EditableViewController, Notifable, Tab setNextToTransaction = true invokeSignal = Sender.enqueue(input: presentation.effectiveInput, context: context, peerId: controller.chatInteraction.peerId, replyId: takeReplyId(), threadId: threadId64, disablePreview: presentation.interfaceState.composeDisableUrlPreview != nil, linkBelowMessage: presentation.interfaceState.linkBelowMessage, largeMedia: presentation.interfaceState.largeMedia, silent: silent, atDate: atDate, sendAsPeerId: currentSendAsPeerId, mediaPreview: presentation.urlPreview?.1, emptyHandler: { [weak strongSelf] in _ = strongSelf?.nextTransaction.execute() - }) |> deliverOnMainQueue |> ignoreValues + }, customChatContents: customChatContents) |> deliverOnMainQueue |> ignoreValues } @@ -3204,7 +3298,7 @@ class ChatController: EditableViewController, Notifable, Tab } else { apply(strongSelf, atDate: nil) } - case .history, .thread, .pinned: + case .history, .thread, .pinned, .customChatContents: delay(0.1, closure: { if atDate != nil { strongSelf.openScheduledChat() @@ -3341,7 +3435,7 @@ class ChatController: EditableViewController, Notifable, Tab let chatInteraction = strongSelf.chatInteraction let presentation = chatInteraction.presentation - let _ = (Sender.enqueue(input: ChatTextInputState(inputText: text), context: context, peerId: chatInteraction.peerId, replyId: takeReplyId(), threadId: threadId64, sendAsPeerId: presentation.currentSendAsPeerId) |> deliverOnMainQueue).start(completed: scrollAfterSend) + let _ = (Sender.enqueue(input: ChatTextInputState(inputText: text), context: context, peerId: chatInteraction.peerId, replyId: takeReplyId(), threadId: threadId64, sendAsPeerId: presentation.currentSendAsPeerId, customChatContents: customChatContents) |> deliverOnMainQueue).start(completed: scrollAfterSend) } } @@ -3405,6 +3499,13 @@ class ChatController: EditableViewController, Notifable, Tab chatInteraction.deleteMessages = { [weak self] messageIds in if let strongSelf = self, let peer = strongSelf.chatInteraction.peer { + + if let customChatContents { + customChatContents.deleteMessages(ids: messageIds) + strongSelf.chatInteraction.update({$0.withoutSelectionState()}) + return + } + if strongSelf.chatInteraction.presentation.reportMode != nil { strongSelf.changeState() return @@ -3730,7 +3831,7 @@ class ChatController: EditableViewController, Notifable, Tab } switch strongSelf.mode { - case .history, .thread: + case .history, .thread, .customChatContents: apply(strongSelf, atDate: nil) case .scheduled: if let peer = strongSelf.chatInteraction.peer { @@ -3868,7 +3969,7 @@ class ChatController: EditableViewController, Notifable, Tab chatInteraction.requestMessageActionCallback = { [weak self] messageId, isGame, data in if let strongSelf = self { switch strongSelf.mode { - case .history, .thread: + case .history, .thread, .customChatContents: let applyResult:(MessageActionCallbackResult) -> Void = { [weak strongSelf] result in if let strongSelf = strongSelf { switch result { @@ -4083,7 +4184,7 @@ class ChatController: EditableViewController, Notifable, Tab } switch strongSelf.mode { - case .history, .thread: + case .history, .thread, .customChatContents: if let fromId = fromId { strongSelf.historyState = strongSelf.historyState.withAddingReply(fromId) } @@ -4294,12 +4395,12 @@ class ChatController: EditableViewController, Notifable, Tab case .scheduled: showModal(with: DateSelectorModalController(context: strongSelf.context, mode: .schedule(peer.id), selectedAt: { [weak strongSelf] date in if let strongSelf = strongSelf { - let _ = (Sender.enqueue(media: media, context: context, peerId: peerId, replyId: takeReplyId(), threadId: threadId64, atDate: date, sendAsPeerId: currentSendAsPeerId) |> deliverOnMainQueue).start(completed: scrollAfterSend) + let _ = (Sender.enqueue(media: media, context: context, peerId: peerId, replyId: takeReplyId(), threadId: threadId64, atDate: date, sendAsPeerId: currentSendAsPeerId, customChatContents: customChatContents) |> deliverOnMainQueue).start(completed: scrollAfterSend) strongSelf.nextTransaction.set(handler: afterSentTransition) } }), for: strongSelf.context.window) - case .history, .thread: - let _ = (Sender.enqueue(media: media, context: context, peerId: peerId, replyId: takeReplyId(), threadId: threadId64, sendAsPeerId: currentSendAsPeerId) |> deliverOnMainQueue).start(completed: scrollAfterSend) + case .history, .thread, .customChatContents: + let _ = (Sender.enqueue(media: media, context: context, peerId: peerId, replyId: takeReplyId(), threadId: threadId64, sendAsPeerId: currentSendAsPeerId, customChatContents: customChatContents) |> deliverOnMainQueue).start(completed: scrollAfterSend) strongSelf.nextTransaction.set(handler: afterSentTransition) case .pinned: break @@ -4412,7 +4513,7 @@ class ChatController: EditableViewController, Notifable, Tab if let strongSelf = self, let peer = strongSelf.chatInteraction.peer, peer.canSendMessage(strongSelf.mode.isThreadMode, media: file, threadData: strongSelf.chatInteraction.presentation.threadInfo, cachedData: strongSelf.chatInteraction.presentation.cachedData) { let hasFwd = !strongSelf.chatInteraction.presentation.interfaceState.forwardMessageIds.isEmpty func apply(_ controller: ChatController, atDate: Date?) { - let _ = (Sender.enqueue(media: file, context: context, peerId: peerId, replyId: takeReplyId(), threadId: threadId64, silent: silent, atDate: atDate, query: query, collectionId: collectionId) |> deliverOnMainQueue).start(completed: scrollAfterSend) + let _ = (Sender.enqueue(media: file, context: context, peerId: peerId, replyId: takeReplyId(), threadId: threadId64, silent: silent, atDate: atDate, query: query, collectionId: collectionId, customChatContents: customChatContents) |> deliverOnMainQueue).start(completed: scrollAfterSend) controller.nextTransaction.set(handler: { if hasFwd { DispatchQueue.main.async { @@ -4453,11 +4554,11 @@ class ChatController: EditableViewController, Notifable, Tab if canSend { func apply(_ controller: ChatController, atDate: Date?) { - let _ = (Sender.enqueue(media: medias, caption: caption, context: context, peerId: controller.chatInteraction.peerId, replyId: takeReplyId(), threadId: threadId64, isCollage: isCollage, additionText: additionText, silent: silent, atDate: atDate, isSpoiler: isSpoiler) |> deliverOnMainQueue).start(completed: scrollAfterSend) + let _ = (Sender.enqueue(media: medias, caption: caption, context: context, peerId: controller.chatInteraction.peerId, replyId: takeReplyId(), threadId: threadId64, isCollage: isCollage, additionText: additionText, silent: silent, atDate: atDate, isSpoiler: isSpoiler, customChatContents: customChatContents) |> deliverOnMainQueue).start(completed: scrollAfterSend) controller.nextTransaction.set(handler: afterSentTransition) } switch strongSelf.mode { - case .history, .thread: + case .history, .thread, .customChatContents: DispatchQueue.main.async { [weak strongSelf] in if let _ = atDate { strongSelf?.openScheduledChat() @@ -4510,7 +4611,7 @@ class ChatController: EditableViewController, Notifable, Tab if controller.chatInteraction.peerId.namespace != Namespaces.Peer.CloudUser { commandText += "@" + (command.peer.username ?? "") } - _ = Sender.enqueue(input: ChatTextInputState(inputText: commandText), context: context, peerId: controller.chatLocation.peerId, replyId: takeReplyId(), threadId: threadId64, atDate: atDate).start(completed: scrollAfterSend) + _ = Sender.enqueue(input: ChatTextInputState(inputText: commandText), context: context, peerId: controller.chatLocation.peerId, replyId: takeReplyId(), threadId: threadId64, atDate: atDate, customChatContents: customChatContents).start(completed: scrollAfterSend) controller.chatInteraction.updateInput(with: "") controller.nextTransaction.set(handler: afterSentTransition) } @@ -4523,7 +4624,7 @@ class ChatController: EditableViewController, Notifable, Tab } }), for: context.window) } - case .history, .thread: + case .history, .thread, .customChatContents: apply(strongSelf, atDate: nil) case .pinned: break @@ -5366,6 +5467,8 @@ class ChatController: EditableViewController, Notifable, Tab } case .pinned: break + case .customChatContents: + break } return present }) @@ -5480,7 +5583,7 @@ class ChatController: EditableViewController, Notifable, Tab let peerView = postboxView as? PeerView self.currentPeerView = peerView switch self.chatInteraction.mode { - case .history, .thread: + case .history, .thread, .customChatContents: var wasGroupChannel: Bool? if let peer = self.chatInteraction.presentation.mainPeer as? TelegramChannel { @@ -5571,7 +5674,7 @@ class ChatController: EditableViewController, Notifable, Tab var sendAsPeerId: PeerId? = nil var isNotAccessible: Bool = false switch mode { - case .history, .thread: + case .history, .thread, .customChatContents: if let cachedData = peerView.cachedData as? CachedChannelData { if !mode.isThreadMode { activeCall = cachedData.activeCall @@ -5674,7 +5777,6 @@ class ChatController: EditableViewController, Notifable, Tab return nil }.updatedMainPeer(peerView != nil ? peerViewMainPeer(peerView!) : nil).withUpdatedCachedData(peerView?.cachedData).withUpdatedThreadInfo(threadInfo) }) - } })) @@ -6422,6 +6524,9 @@ class ChatController: EditableViewController, Notifable, Tab if presentation.reportMode != nil { return false } + if presentation.chatMode.customChatContents != nil { + return false + } var isEmpty: Bool = genericView.tableView.isEmpty if chatInteraction.mode.isThreadMode { isEmpty = genericView.tableView.count == (theme.bubbled ? 4 : 3) @@ -6642,9 +6747,6 @@ class ChatController: EditableViewController, Notifable, Tab } - - - (self.centerBarView as? ChatTitleBarView)?.updateSearchButton(hidden: !searchAvailable, animated: transition.animated) if genericView.tableView.isEmpty, let peer = chatInteraction.peer, peer.isBot { @@ -7075,8 +7177,10 @@ class ChatController: EditableViewController, Notifable, Tab }, itemMode: .destruct, itemImage: MenuAnimation.menu_delete.value)) } } - - + case .customChatContents: + items.append(ContextMenuItem(strings().chatContextEdit1, handler: { [weak self] in + self?.changeState() + }, itemImage: MenuAnimation.menu_edit.value)) } for item in items { menu.addItem(item) @@ -7960,22 +8064,28 @@ class ChatController: EditableViewController, Notifable, Tab if value.selectionState != oldValue.selectionState { if let selectionState = value.selectionState { let ids = Array(selectionState.selectedIds) - loadSelectionMessagesDisposable.set((context.account.postbox.messagesAtIds(ids) |> deliverOnMainQueue).start( next:{ [weak self] messages in - var canDelete:Bool = !messages.isEmpty - var canForward:Bool = !messages.isEmpty - if let chatInteraction = self?.chatInteraction { - for message in messages { - if !canDeleteMessage(message, account: context.account, mode: mode) { - canDelete = false - } - if !canForwardMessage(message, chatInteraction: chatInteraction) { - canForward = false + switch mode { + case .customChatContents: + chatInteraction.update({$0.withUpdatedBasicActions((true, false))}) + default: + loadSelectionMessagesDisposable.set((context.account.postbox.messagesAtIds(ids) |> deliverOnMainQueue).start( next:{ [weak self] messages in + var canDelete:Bool = !messages.isEmpty + var canForward:Bool = !messages.isEmpty + if let chatInteraction = self?.chatInteraction { + for message in messages { + if !canDeleteMessage(message, account: context.account, mode: mode) { + canDelete = false + } + if !canForwardMessage(message, chatInteraction: chatInteraction) { + canForward = false + } } + chatInteraction.update({$0.withUpdatedBasicActions((canDelete, canForward))}) } - chatInteraction.update({$0.withUpdatedBasicActions((canDelete, canForward))}) - } - - })) + + })) + } + } else { DispatchQueue.main.async { [weak self] in self?.chatInteraction.update({$0.withUpdatedBasicActions((false, false))}) diff --git a/Telegram-Mac/ChatEmptyPeerItem.swift b/Telegram-Mac/ChatEmptyPeerItem.swift index 0ea3c1642..497905087 100644 --- a/Telegram-Mac/ChatEmptyPeerItem.swift +++ b/Telegram-Mac/ChatEmptyPeerItem.swift @@ -121,6 +121,8 @@ class ChatEmptyPeerItem: TableRowItem { case .pinned: lineSpacing = nil _ = attr.append(string: strings().chatEmptyChat, color: textColor, font: .medium(.text)) + case let .customChatContents(contents): + _ = attr.append(string: contents.kind.text, color: textColor, font: .medium(.text)) } diff --git a/Telegram-Mac/ChatHistoryViewForLocation.swift b/Telegram-Mac/ChatHistoryViewForLocation.swift index 666c5bb5f..073f5d0e0 100644 --- a/Telegram-Mac/ChatHistoryViewForLocation.swift +++ b/Telegram-Mac/ChatHistoryViewForLocation.swift @@ -133,6 +133,18 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, context: Accoun } else { ignoreRelatedChats = false } + + switch mode { + case .customChatContents(let contents): + return contents.messages |> map { messages in + let entries: [MessageHistoryEntry] = messages.map { + .init(message: $0, isRead: true, location: nil, monthLocation: nil, attributes: .init(authorIsContact: false)) + } + return .HistoryView(view: .init(tag: nil, namespaces: .all, entries: entries, holeEarlier: false, holeLater: false, isLoading: false), type: .Generic(type: .Generic), scrollPosition: nil, initialData: .init()) + } + default: + break + } switch location { @@ -142,7 +154,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, context: Accoun let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> switch mode { - case .history, .thread, .pinned: + case .history, .thread, .pinned, .customChatContents: if let tag = tag { signal = account.viewTracker.aroundMessageHistoryViewForLocation(chatLocationInput, index: .upperBound, anchorIndex: .upperBound, count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: nil, tag: tag, orderStatistics: orderStatistics) } else { @@ -254,7 +266,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, context: Accoun let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> switch mode { - case .history, .thread, .pinned: + case .history, .thread, .pinned, .customChatContents: switch searchLocation { case let .index(index, _): signal = account.viewTracker.aroundMessageHistoryViewForLocation(chatLocationInput, index: MessageHistoryAnchorIndex.message(index), anchorIndex: MessageHistoryAnchorIndex.message(index), count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: nil, tag: tag, orderStatistics: orderStatistics, additionalData: additionalData) @@ -337,7 +349,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, context: Accoun let signal:Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> switch mode { - case .history, .thread, .pinned: + case .history, .thread, .pinned, .customChatContents: signal = account.viewTracker.aroundMessageHistoryViewForLocation(chatLocationInput, index: index, anchorIndex: anchorIndex, count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates?(), tag: tag, orderStatistics: orderStatistics, additionalData: additionalData) case .scheduled: signal = account.viewTracker.scheduledMessagesViewForLocation(chatLocationInput) @@ -364,7 +376,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, context: Accoun let signal:Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> switch mode { - case .history, .thread, .pinned: + case .history, .thread, .pinned, .customChatContents: signal = account.viewTracker.aroundMessageHistoryViewForLocation(chatLocationInput, index: index, anchorIndex: anchorIndex, count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates?(), tag: tag, orderStatistics: orderStatistics, additionalData: additionalData) case .scheduled: signal = account.viewTracker.scheduledMessagesViewForLocation(chatLocationInput) diff --git a/Telegram-Mac/ChatInputView.swift b/Telegram-Mac/ChatInputView.swift index 05c2b8428..594e5a017 100644 --- a/Telegram-Mac/ChatInputView.swift +++ b/Telegram-Mac/ChatInputView.swift @@ -464,8 +464,8 @@ class ChatInputView: View, Notifable { layout.measure(width: frame.width - 40) layout.interactions = globalLinkExecutor textView.update(layout) - current.addSubview(textView) + textView.center() } else if let view = blockText { performSubviewRemoval(view, animated: animated) blockText = nil diff --git a/Telegram-Mac/ChatInterfaceInteraction.swift b/Telegram-Mac/ChatInterfaceInteraction.swift index 16103b507..ba1a7dcdc 100644 --- a/Telegram-Mac/ChatInterfaceInteraction.swift +++ b/Telegram-Mac/ChatInterfaceInteraction.swift @@ -891,6 +891,10 @@ final class ChatInteraction : InterfaceObserver { public func saveState(_ force:Bool = true, scrollState: ChatInterfaceHistoryScrollState? = nil, sync: Bool = false) { + if mode.customChatContents != nil { + return + } + let peerId = self.peerId let context = self.context let timestamp = Int32(Date().timeIntervalSince1970) diff --git a/Telegram-Mac/ChatListController.swift b/Telegram-Mac/ChatListController.swift index 46457edea..a7f5abfed 100644 --- a/Telegram-Mac/ChatListController.swift +++ b/Telegram-Mac/ChatListController.swift @@ -1677,7 +1677,7 @@ class ChatListController : PeersListController { if let item = item as? ChatListRowItem { if !isNew, let controller = navigation.controller as? ChatController, !(item.isForum && !item.isTopic) { switch controller.mode { - case .history, .thread: + case .history, .thread, .customChatContents: if let modalAction = navigation.modalAction { navigation.controller.invokeNavigation(action: modalAction) } diff --git a/Telegram-Mac/ChatMapRowItem.swift b/Telegram-Mac/ChatMapRowItem.swift index 5322b22e7..4f9c8b73b 100644 --- a/Telegram-Mac/ChatMapRowItem.swift +++ b/Telegram-Mac/ChatMapRowItem.swift @@ -64,11 +64,7 @@ class ChatMapRowItem: ChatMediaItem { //let resource = HttpReferenceMediaResource(url: "https://maps.googleapis.com/maps/api/staticmap?center=\(map.latitude),\(map.longitude)&zoom=15&size=\(isVenue ? 60 * Int(2.0) : 320 * Int(2.0))x\(isVenue ? 60 * Int(2.0) : 120 * Int(2.0))&sensor=true", size: 0) self.parameters = ChatMediaMapLayoutParameters(map: map, resource: resource, presentation: .make(for: object.message!, account: context.account, renderType: object.renderType, theme: theme), automaticDownload: downloadSettings.isDownloable(object.message!), execute: { - if #available(OSX 10.13, *) { - showModal(with: LocationModalPreview(context, map: map, peer: object.message!.effectiveAuthor, messageId: object.message!.id), for: context.window) - } else { - execute(inapp: .external(link: "https://maps.google.com/maps?q=\(String(format:"%f", map.latitude)),\(String(format:"%f", map.longitude))", false)) - } + showModal(with: LocationModalPreview(context, map: map, peer: object.message!.effectiveAuthor, messageId: object.message!.id), for: context.window) }) if isLiveLocationView { diff --git a/Telegram-Mac/ChatMessageItem.swift b/Telegram-Mac/ChatMessageItem.swift index 2521ca3f0..3d85ad23c 100644 --- a/Telegram-Mac/ChatMessageItem.swift +++ b/Telegram-Mac/ChatMessageItem.swift @@ -639,8 +639,10 @@ class ChatMessageItem: ChatRowItem { - let textBlockWidth: CGFloat = isBubbled ? min(webpageLayout?.size.width ?? width, width) : width - + var textBlockWidth: CGFloat = isBubbled ? min(webpageLayout?.size.width ?? width, width) : width + if textLayout.hasBlockQuotes { + textBlockWidth -= 40 + } textLayout.measure(width: textBlockWidth, isBigEmoji: containsBigEmoji) if isTranslateLoading { self.block = textLayout.generateBlock(backgroundColor: .blackTransparent) @@ -821,28 +823,6 @@ class ChatMessageItem: ChatRowItem { let header: (TextNodeLayout, TextNode)? header = TextNode.layoutText(.initialize(string: lg.prefixWithDots(15), color: color, font: .medium(.text)), nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) - - let tetriary: NSColor - let dark: Bool -// if bubbled { -// -// if isIncoming { -// tetriary = blockColor.main -// dark = isDark -// } else { -// if isDark { -// tetriary = NSColor.black -// dark = isDark -// } else { -// tetriary = blockColor.main -// dark = false -// } -// } -// } else { -// tetriary = blockColor.main -// dark = isDark -// } - string.addAttribute(TextInputAttributes.quote, value: TextViewBlockQuoteData(id: Int(arc4random64()), colors: .init(main: color, secondary: nil, tertiary: color), isCode: true, space: 4, header: header), range: range) fontAttributes.append((range, .monospace)) diff --git a/Telegram-Mac/ChatMessageMenuItems.swift b/Telegram-Mac/ChatMessageMenuItems.swift index 9ffeba705..44817e7fe 100644 --- a/Telegram-Mac/ChatMessageMenuItems.swift +++ b/Telegram-Mac/ChatMessageMenuItems.swift @@ -449,7 +449,7 @@ func chatMenuItems(for message: Message, entry: ChatHistoryEntry?, textLayout: ( } // if !data.message.isCopyProtected() { - if let textLayout = data.textLayout?.0 { + if let textLayout = data.textLayout?.0, mode.customChatContents == nil { if !textLayout.selectedRange.hasSelectText { let text = message.text diff --git a/Telegram-Mac/ChatPresentationInterfaceState.swift b/Telegram-Mac/ChatPresentationInterfaceState.swift index 79a4f67b2..3b927084f 100644 --- a/Telegram-Mac/ChatPresentationInterfaceState.swift +++ b/Telegram-Mac/ChatPresentationInterfaceState.swift @@ -656,6 +656,11 @@ class ChatPresentationInterfaceState: Equatable { // }, nil) // } // #endif + if chatMode.customChatContents != nil, let count = self.historyCount { + if count == 20 { + return .block("Limit of \(count) messages reached") + } + } if searchMode.tag != nil { return .action(!searchMode.showAll ? strings().chatStateShowOtherMessages : strings().chatStateHideOtherMessages, { chatInteraction in diff --git a/Telegram-Mac/ChatRowItem.swift b/Telegram-Mac/ChatRowItem.swift index 2de82dfa9..dae7f66f9 100644 --- a/Telegram-Mac/ChatRowItem.swift +++ b/Telegram-Mac/ChatRowItem.swift @@ -276,6 +276,9 @@ class ChatRowItem: TableRowItem { if let _ = message?.adAttribute { return .zero } + if let _ = chatInteraction.mode.customChatContents { + return .zero + } if let frames = rightFrames { return NSMakeSize(frames.width, rightHeight) } else { @@ -2135,7 +2138,7 @@ class ChatRowItem: TableRowItem { } } - if message.timestamp != scheduleWhenOnlineTimestamp && message.adAttribute == nil { + if message.timestamp != scheduleWhenOnlineTimestamp && message.adAttribute == nil, chatInteraction.mode.customChatContents == nil { var time:TimeInterval = TimeInterval(message.timestamp) time -= context.timeDifference @@ -2301,7 +2304,7 @@ class ChatRowItem: TableRowItem { } } - if message.adAttribute == nil { + if message.adAttribute == nil, chatInteraction.mode.customChatContents == nil { self.fullDate = fullDate self.originalFullDate = fullDate } diff --git a/Telegram-Mac/ChatRowView.swift b/Telegram-Mac/ChatRowView.swift index 53eef879c..94333130f 100644 --- a/Telegram-Mac/ChatRowView.swift +++ b/Telegram-Mac/ChatRowView.swift @@ -738,6 +738,9 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDeleg if !item.isBubbled { point.x += max(0, item.statusSize - 2) } + if !item.isBubbled, let boostBadge = item.boostBadge { + point.x += boostBadge.layoutSize.width + } if item.isBubbled { point.y -= item.topInset diff --git a/Telegram-Mac/ChatSwitchInlineController.swift b/Telegram-Mac/ChatSwitchInlineController.swift index 8261a1195..944cc8367 100644 --- a/Telegram-Mac/ChatSwitchInlineController.swift +++ b/Telegram-Mac/ChatSwitchInlineController.swift @@ -52,6 +52,8 @@ class ChatSwitchInlineController: ChatController { controller = ChatController(context: context, chatLocation: .thread(data), mode: .thread(data: data, mode: mode), initialAction: .inputText(text: text, behavior: .automatic), chatLocationContextHolder: Atomic(value: nil)) case .scheduled: controller = ChatScheduleController(context: context, chatLocation: .peer(fallbackId), initialAction: .inputText(text: text, behavior: .automatic)) + case .customChatContents: + fatalError() } self.navigationController?.push(controller) } diff --git a/Telegram-Mac/ChatTitleBarView.swift b/Telegram-Mac/ChatTitleBarView.swift index a62794326..eb4478ece 100644 --- a/Telegram-Mac/ChatTitleBarView.swift +++ b/Telegram-Mac/ChatTitleBarView.swift @@ -646,7 +646,7 @@ class ChatTitleBarView: TitledBarView, InteractionContentViewProtocol { let mode = chatInteraction.mode - self.hasPhoto = (!mode.isTopicMode && !mode.isThreadMode && mode != .pinned && mode != .scheduled) + self.hasPhoto = (!mode.isTopicMode && !mode.isThreadMode && mode != .pinned && mode != .scheduled) && mode.customChatContents == nil self.photoContainer.isHidden = !hasPhoto @@ -774,7 +774,9 @@ class ChatTitleBarView: TitledBarView, InteractionContentViewProtocol { if let peerView = self.peerView, let peer = peerViewMainPeer(peerView) { var result = stringStatus(for: peerView, context: chatInteraction.context, theme: PeerStatusStringTheme(titleFont: .medium(.title)), onlineMemberCount: self.counters.online, ignoreActivity: chatInteraction.mode.isSavedMessagesThread) - if chatInteraction.mode == .pinned { + if let customChatContents = chatInteraction.mode.customChatContents { + result = result.withUpdatedTitle(customChatContents.kind.text).withUpdatedStatus("") + } else if chatInteraction.mode == .pinned { result = result.withUpdatedTitle(strings().chatTitlePinnedMessagesCountable(presentation.pinnedMessageId?.totalCount ?? 0)).withUpdatedStatus("") } else if chatInteraction.mode == .scheduled { result = result.withUpdatedTitle(strings().chatTitleScheduledMessages) diff --git a/Telegram-Mac/CoreExtension.swift b/Telegram-Mac/CoreExtension.swift index 6ecb723a5..185a813ee 100644 --- a/Telegram-Mac/CoreExtension.swift +++ b/Telegram-Mac/CoreExtension.swift @@ -966,6 +966,9 @@ fileprivate let edit_limit_time:Int32 = 48*60*60 func canDeleteMessage(_ message:Message, account:Account, mode: ChatMode) -> Bool { + if mode.customChatContents != nil { + return true + } if mode.threadId == message.id { return false } @@ -976,6 +979,7 @@ func canDeleteMessage(_ message:Message, account:Account, mode: ChatMode) -> Boo return false } + if let channel = message.peers[message.id.peerId] as? TelegramChannel { if case .broadcast = channel.info { if !message.flags.contains(.Incoming) { @@ -1009,6 +1013,10 @@ func canForwardMessage(_ message:Message, chatInteraction: ChatInteraction) -> B return false } + if chatInteraction.mode.customChatContents != nil { + return false + } + if message.isExpiredStory { return false } @@ -1128,10 +1136,13 @@ func canReplyMessage(_ message: Message, peerId: PeerId, mode: ChatMode, threadD if message.isScheduledMessage { return false } + if mode.customChatContents != nil, message.id.namespace == Namespaces.Message.Local { + return false + } if peerId == message.id.peerId, !message.flags.contains(.Unsent) && !message.flags.contains(.Failed) && (message.id.namespace != Namespaces.Message.Local || message.id.peerId.namespace == Namespaces.Peer.SecretChat) { switch mode { - case .history: + case .history, .customChatContents: if let channel = peer as? TelegramChannel, channel.hasPermission(.sendSomething) { return true } else { @@ -1164,6 +1175,12 @@ func canReplyMessage(_ message: Message, peerId: PeerId, mode: ChatMode, threadD } func canEditMessage(_ message:Message, chatInteraction: ChatInteraction, context: AccountContext, ignorePoll: Bool = false) -> Bool { + + + if chatInteraction.mode.customChatContents != nil { + return true + } + if message.forwardInfo != nil { return false } @@ -3277,6 +3294,12 @@ struct CachedDataEquatable: Equatable { init?(_ data: CachedPeerData?) { self.init(data: data) } + init(data: CachedPeerData) { + self.data = data + } + init(_ data: CachedPeerData) { + self.init(data: data) + } static func ==(lhs: CachedDataEquatable, rhs: CachedDataEquatable) -> Bool { return lhs.data.isEqual(to: rhs.data) } diff --git a/Telegram-Mac/FastSettings.swift b/Telegram-Mac/FastSettings.swift index a7e192786..77dd77386 100644 --- a/Telegram-Mac/FastSettings.swift +++ b/Telegram-Mac/FastSettings.swift @@ -636,7 +636,8 @@ class FastSettings { PremiumValue.saved_tags.rawValue, PremiumValue.last_seen.rawValue, PremiumValue.message_privacy.rawValue, - PremiumValue.business.rawValue] + PremiumValue.business.rawValue, + PremiumValue.folder_tags.rawValue] let dismissedPerks = UserDefaults.standard.value(forKey: "dismissedPerks") as? [String] ?? [] return perks.filter { !dismissedPerks.contains($0) } } diff --git a/Telegram-Mac/GalleryViewer.swift b/Telegram-Mac/GalleryViewer.swift index 716699167..94f7d051a 100644 --- a/Telegram-Mac/GalleryViewer.swift +++ b/Telegram-Mac/GalleryViewer.swift @@ -603,6 +603,23 @@ class GalleryViewer: NSResponder { signal = context.account.viewTracker.aroundIdMessageHistoryViewForLocation(.peer(peerId: message.id.peerId, threadId: nil), count: 50, ignoreRelatedChats: false, messageId: index.id, tag: .tag(.pinned), orderStatistics: [.combinedLocation], additionalData: []) case .scheduled: signal = context.account.viewTracker.scheduledMessagesViewForLocation(.peer(peerId: message.id.peerId, threadId: nil)) + case let .customChatContents(contents): + signal = contents.messages |> map { messages in + let entries: [MessageHistoryEntry] = messages.filter { message in + if let tags = tags { + if let msgTag = tagsForMessage(message) { + return .tag(msgTag) == tags + } else { + return false + } + } else { + return true + } + }.map { + .init(message: $0, isRead: true, location: nil, monthLocation: nil, attributes: .init(authorIsContact: false)) + } + return (MessageHistoryView(tag: nil, namespaces: .all, entries: entries, holeEarlier: false, holeLater: false, isLoading: false), ViewUpdateType.Generic, nil) + } } diff --git a/Telegram-Mac/GeneralRowView.swift b/Telegram-Mac/GeneralRowView.swift index 530951b50..489e11368 100644 --- a/Telegram-Mac/GeneralRowView.swift +++ b/Telegram-Mac/GeneralRowView.swift @@ -77,11 +77,12 @@ class GeneralContainableRowView : TableRowView { guard let item = item as? GeneralRowItem else { return } - let blockWidth = min(maxBlockWidth, frame.width - item.inset.left - item.inset.right) + let blockWidth = min(maxBlockWidth, size.width - item.inset.left - item.inset.right) - transition.updateFrame(view: self.containerView, frame: NSMakeRect(floorToScreenPixels(backingScaleFactor, (maxWidth - blockWidth) / 2), item.inset.top, blockWidth, maxHeight - item.inset.bottom - item.inset.top)) + let rect = NSMakeRect(floorToScreenPixels(backingScaleFactor, (size.width - blockWidth) / 2), item.inset.top, blockWidth, size.height - item.inset.bottom - item.inset.top) + transition.updateFrame(view: self.containerView, frame: rect) - self.containerView.setCorners(item.viewType.corners) + self.containerView.setCorners(item.viewType.corners, animated: transition.isAnimated, frame: rect) transition.updateFrame(view: borderView, frame: NSMakeRect(item.viewType.innerInset.left + additionBorderInset, containerView.frame.height - .borderSize, containerView.frame.width - item.viewType.innerInset.left - item.viewType.innerInset.right - additionBorderInset, .borderSize)) } diff --git a/Telegram-Mac/GreetingMessageSetupChatContents.swift b/Telegram-Mac/GreetingMessageSetupChatContents.swift new file mode 100644 index 000000000..960d59a0d --- /dev/null +++ b/Telegram-Mac/GreetingMessageSetupChatContents.swift @@ -0,0 +1,227 @@ +// +// GreetingMessageSetupChatContents.swift +// Telegram +// +// Created by Mikhail Filimonov on 19.02.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit + + +final class GreetingMessageSetupChatContents: ChatCustomContentsProtocol { + private final class Impl { + let queue: Queue + let context: AccountContext + + private var messages: [Message] = [] + private var nextMessageId: Int32 = 1000 + let messagesPromise = Promise<[Message]>([]) + + private var nextGroupingId: UInt32 = 0 + private var groupingKeyToGroupId: [Int64: UInt32] = [:] + + init(queue: Queue, context: AccountContext, messages: [EngineMessage]) { + self.queue = queue + self.context = context + self.messages = messages.map { $0._asMessage() } + self.notifyMessagesUpdated() + + if let maxMessageId = messages.map(\.id).max() { + self.nextMessageId = maxMessageId.id + 1 + } + if let maxGroupingId = messages.compactMap(\.groupInfo?.stableId).max() { + self.nextGroupingId = maxGroupingId + 1 + } + } + + deinit { + } + + private func notifyMessagesUpdated() { + self.messages.sort(by: { $0.index > $1.index }) + self.messagesPromise.set(.single(self.messages)) + } + + func enqueueMessages(messages: [EnqueueMessage]) -> [MessageId?] { + var ids: [MessageId?] = [] + for message in messages { + switch message { + case let .message(text, attributes, _, mediaReference, _, _, _, localGroupingKey, correlationId, _): + let _ = attributes + let _ = mediaReference + let _ = correlationId + + let messageId = self.nextMessageId + self.nextMessageId += 1 + + var attributes: [MessageAttribute] = [] + attributes.append(OutgoingMessageInfoAttribute( + uniqueId: Int64.random(in: Int64.min ... Int64.max), + flags: [], + acknowledged: true, + correlationId: correlationId, + bubbleUpEmojiOrStickersets: [] + )) + + var media: [Media] = [] + if let mediaReference { + media.append(mediaReference.media) + } + + let mappedMessage = Message( + stableId: UInt32(messageId), + stableVersion: 0, + id: MessageId( + peerId: context.peerId, + namespace: Namespaces.Message.Local, + id: Int32(messageId) + ), + globallyUniqueId: nil, + groupingKey: localGroupingKey, + groupInfo: localGroupingKey.flatMap { value in + if let current = self.groupingKeyToGroupId[value] { + return MessageGroupInfo(stableId: current) + } else { + let groupId = self.nextGroupingId + self.nextGroupingId += 1 + self.groupingKeyToGroupId[value] = groupId + return MessageGroupInfo(stableId: groupId) + } + }, + threadId: nil, + timestamp: messageId, + flags: [], + tags: [], + globalTags: [], + localTags: [], + customTags: [], + forwardInfo: nil, + author: context.myPeer, + text: text, + attributes: attributes, + media: media, + peers: context.myPeer.flatMap { SimpleDictionary([$0.id : $0]) } ?? SimpleDictionary(), + associatedMessages: SimpleDictionary(), + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: nil, + associatedStories: [:] + ) + self.messages.append(mappedMessage) + ids.append(mappedMessage.id) + case .forward: + break + } + } + self.notifyMessagesUpdated() + return ids + } + + func deleteMessages(ids: [EngineMessage.Id]) { + self.messages = self.messages.filter({ !ids.contains($0.id) }) + self.notifyMessagesUpdated() + } + + func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) { + guard let index = self.messages.firstIndex(where: { $0.id == id }) else { + return + } + let originalMessage = self.messages[index] + + var mappedMedia = originalMessage.media + switch media { + case .keep: + break + case let .update(value): + mappedMedia = [value.media] + } + + var mappedAtrributes = originalMessage.attributes + mappedAtrributes.removeAll(where: { $0 is TextEntitiesMessageAttribute }) + if let entities { + mappedAtrributes.append(entities) + } + + let mappedMessage = Message( + stableId: originalMessage.stableId, + stableVersion: originalMessage.stableVersion + 1, + id: originalMessage.id, + globallyUniqueId: originalMessage.globallyUniqueId, + groupingKey: originalMessage.groupingKey, + groupInfo: originalMessage.groupInfo, + threadId: originalMessage.threadId, + timestamp: originalMessage.timestamp, + flags: originalMessage.flags, + tags: originalMessage.tags, + globalTags: originalMessage.globalTags, + localTags: originalMessage.localTags, + customTags: originalMessage.customTags, + forwardInfo: originalMessage.forwardInfo, + author: originalMessage.author, + text: text, + attributes: mappedAtrributes, + media: mappedMedia, + peers: originalMessage.peers, + associatedMessages: originalMessage.associatedMessages, + associatedMessageIds: originalMessage.associatedMessageIds, + associatedMedia: originalMessage.associatedMedia, + associatedThreadInfo: originalMessage.associatedThreadInfo, + associatedStories: originalMessage.associatedStories + ) + + self.messages[index] = mappedMessage + self.notifyMessagesUpdated() + } + } + + let kind: ChatCustomContentsKind + + var messages: Signal<[Message], NoError> { + return self.impl.signalWith({ impl, subscriber in + return impl.messagesPromise.get().start(next: subscriber.putNext) + }) + } + + var messageLimit: Int? { + return 20 + } + + private let queue: Queue + private let impl: QueueLocalObject + + init(context: AccountContext, messages: [EngineMessage], kind: ChatCustomContentsKind) { + self.kind = kind + + let queue = Queue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, context: context, messages: messages) + }) + } + + func enqueueMessages(messages: [EnqueueMessage]) -> Signal<[MessageId?],NoError> { + return self.impl.signalWith { impl, subscriber in + let ids = impl.enqueueMessages(messages: messages) + subscriber.putNext(ids) + subscriber.putCompletion() + return EmptyDisposable + } + } + + func deleteMessages(ids: [EngineMessage.Id]) { + self.impl.with { impl in + impl.deleteMessages(ids: ids) + } + } + + func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) { + self.impl.with { impl in + impl.editMessage(id: id, text: text, media: media, entities: entities, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview) + } + } +} + diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 0bcc1a9db..344a009fd 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259882 + 259985 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/LiveLocationViewController.swift b/Telegram-Mac/LiveLocationViewController.swift index ce4d87acf..31d89930b 100644 --- a/Telegram-Mac/LiveLocationViewController.swift +++ b/Telegram-Mac/LiveLocationViewController.swift @@ -68,7 +68,7 @@ private func entries(_ state:LocationPreviewState, arguments: LocationPreviewArg return entries } @available(macOS 10.13, *) -func LocationModalPreview(_ context: AccountContext, map mapValue: TelegramMediaMap, peer: Peer?, messageId: MessageId) -> InputDataModalController { +func LocationModalPreview(_ context: AccountContext, map mapValue: TelegramMediaMap, peer: Peer?, messageId: MessageId?) -> InputDataModalController { let initialState = LocationPreviewState(map: mapValue, peer: peer) @@ -80,8 +80,13 @@ func LocationModalPreview(_ context: AccountContext, map mapValue: TelegramMedia let arguments = LocationPreviewArguments(context: context) - let messageView = context.account.postbox.messageView(messageId) |> map { - $0.message + let messageView: Signal + if let messageId { + messageView = context.account.postbox.messageView(messageId) |> map { + $0.message + } + } else { + messageView = .complete() } let disposable = messageView.start(next: { message in diff --git a/Telegram-Mac/NetworkStatusManager.swift b/Telegram-Mac/NetworkStatusManager.swift index 7d88d4124..7bf8ad998 100644 --- a/Telegram-Mac/NetworkStatusManager.swift +++ b/Telegram-Mac/NetworkStatusManager.swift @@ -135,7 +135,7 @@ private final class ConnectingStatusView: View { self.imageView.center() // self.visualEffect.frame = bounds self.backgroundView?.frame = bounds - updateAnimation() + // updateAnimation() } @@ -218,7 +218,7 @@ private final class ConnectingStatusView: View { }) imageView.sizeToFit() - updateAnimation() +// updateAnimation() // self.visualEffect.bgColor = .clear diff --git a/Telegram-Mac/PeerInfoBusinessItems.swift b/Telegram-Mac/PeerInfoBusinessItems.swift new file mode 100644 index 000000000..7608285f8 --- /dev/null +++ b/Telegram-Mac/PeerInfoBusinessItems.swift @@ -0,0 +1,385 @@ +// +// PeerInfoLocationRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 20.02.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import TGUIKit + +final class PeerInfoLocationRowItem : GeneralRowItem { + + + let context: AccountContext + let peer: Peer + let cachedData: CachedUserData + let addressLayout: TextViewLayout + let titleLayout: TextViewLayout + let arguments: TransformImageArguments + let media: TelegramMediaImage + let resource: MapSnapshotMediaResource + let open:()->Void + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, peer: Peer, cachedData: CachedUserData, viewType: GeneralViewType, open:@escaping()->Void) { + self.context = context + self.peer = peer + self.open = open + self.cachedData = cachedData + //TODOLANG + self.titleLayout = .init(.initialize(string: "location", color: theme.colors.text, font: .normal(.text))) + self.addressLayout = .init(.initialize(string: "Unit R201, The Residences at Marina Gate 2, Dubai", color: theme.colors.text, font: .normal(.text))) + let resource = MapSnapshotMediaResource(latitude: 25.08405406819793, longitude: 55.13948416803165, width: 320 * 2, height: 120 * 2, zoom: 15) + + self.resource = resource + + let imageSize = NSMakeSize(50, 50) + + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) + + self.media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: MediaId.Id((resource.latitude * resource.longitude).hashValue)), representations: [representation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + + self.arguments = TransformImageArguments(corners: ImageCorners(radius: 8), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: NSEdgeInsets()) + + + + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + var textWidth: CGFloat = blockWidth + textWidth -= viewType.innerInset.left * 2 + textWidth -= 50 + textWidth -= 10 + titleLayout.measure(width: textWidth) + addressLayout.measure(width: textWidth) + + return true + } + + override var height: CGFloat { + return max(viewType.innerInset.top * 2 + 50, viewType.innerInset.top * 2 + titleLayout.layoutSize.height + 6 + addressLayout.layoutSize.height) + } + + override func viewClass() -> AnyClass { + return PeerInfoLocationRowView.self + } +} + +private final class PeerInfoLocationRowView: GeneralContainableRowView { + private let title = TextView() + private let text = TextView() + private let imageView = TransformImageView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(title) + addSubview(text) + addSubview(imageView) + imageView.setFrameSize(NSMakeSize(50, 50)) + + imageView.layer?.cornerRadius = 4 + + title.userInteractionEnabled = false + title.isSelectable = false + + + containerView.set(handler: { [weak self] _ in + if let item = self?.item as? PeerInfoLocationRowItem { + item.open() + } + }, for: .Click) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? PeerInfoLocationRowItem else { + return + } + + let arguments = item.arguments + let media = item.media + + imageView.setSignal(signal: cachedMedia(media: item.media, arguments: item.arguments, scale: backingScaleFactor, positionFlags: nil), clearInstantly: false) + + imageView.setSignal( chatMapSnapshotImage(account: item.context.account, resource: item.resource), clearInstantly: false, animate: false, cacheImage: { result in + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale, positionFlags: nil) + }) + + imageView.set(arguments: arguments) + + title.update(item.titleLayout) + text.update(item.addressLayout) + } + + override func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + super.updateLayout(size: size, transition: transition) + + guard let item = item as? GeneralRowItem else { + return + } + + transition.updateFrame(view: title, frame: NSMakeRect(item.viewType.innerInset.left, item.viewType.innerInset.top, title.frame.width, title.frame.height)) + + transition.updateFrame(view: text, frame: NSMakeRect(item.viewType.innerInset.left, title.frame.maxY + 6, text.frame.width, text.frame.height)) + + transition.updateFrame(view: imageView, frame: imageView.centerFrameY(x: containerView.frame.width - imageView.frame.width - item.viewType.innerInset.right)) + + } +} + + + +final class PeerInfoHoursRowItem : GeneralRowItem { + + struct Day { + let day: TextViewLayout + let hours: [TextViewLayout] + init(day: String, hours: [String]) { + self.day = .init(.initialize(string: day, color: theme.colors.text, font: .normal(.text)), maximumNumberOfLines: 1) + var list:[TextViewLayout] = [] + for hour in hours { + list.append(.init(.initialize(string: hour, color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1)) + } + self.hours = list + } + + var height: CGFloat { + return max(30, CGFloat(hours.count) * 20 + 10) + } + } + + let days:[Day] + let context: AccountContext + let peer: Peer + let cachedData: CachedUserData + let titleLayout: TextViewLayout + let statusLayout: TextViewLayout + let todayHoursLayout: TextViewLayout + let revealed: Bool + let open:()->Void + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, revealed: Bool, peer: Peer, cachedData: CachedUserData, viewType: GeneralViewType, open:@escaping()->Void) { + self.context = context + self.peer = peer + self.open = open + self.cachedData = cachedData + self.revealed = revealed + //TODOLANG + self.titleLayout = .init(.initialize(string: "business hours", color: theme.colors.text, font: .normal(.text)), maximumNumberOfLines: 1) + self.statusLayout = .init(.initialize(string: "Open", color: theme.colors.greenUI, font: .normal(.text)), maximumNumberOfLines: 1) + + self.todayHoursLayout = .init(.initialize(string: "09:00 - 13:00", color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + + var days: [Day] = [] + + days.append(.init(day: "Monday", hours: ["09:00 - 15:00"])) + days.append(.init(day: "Tuesday", hours: ["09:00 - 15:00"])) + days.append(.init(day: "Wednesday", hours: ["09:00 - 15:00"])) + days.append(.init(day: "Thrusday", hours: ["09:00 - 15:00", "16:00 - 19:00"])) + days.append(.init(day: "Friday", hours: ["09:00 - 15:00", "16:00 - 19:00", "22:00 - 23:00"])) + days.append(.init(day: "Saturday", hours: ["closed"])) + days.append(.init(day: "Sunday", hours: ["closed"])) + self.days = days + + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + var textWidth: CGFloat = blockWidth + textWidth -= viewType.innerInset.left * 2 + titleLayout.measure(width: textWidth) + statusLayout.measure(width: textWidth) + todayHoursLayout.measure(width: textWidth - statusLayout.layoutSize.width - 5) + + for day in days { + day.day.measure(width: textWidth) + for hour in day.hours { + hour.measure(width: textWidth - day.day.layoutSize.width - 5) + } + } + return true + } + + override var height: CGFloat { + var height = basicHeight + if revealed { + height += days.reduce(0, { $0 + $1.height }) + height += viewType.innerInset.top + } + return height + + } + + var basicHeight: CGFloat { + return viewType.innerInset.top * 2 + titleLayout.layoutSize.height + 6 + statusLayout.layoutSize.height + } + + override func viewClass() -> AnyClass { + return PeerInfoHoursRowView.self + } +} + +private final class PeerInfoHoursRowView: GeneralContainableRowView { + + private class Day: View { + private let status = TextView() + private let hours = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(status) + addSubview(hours) + + status.userInteractionEnabled = false + status.isSelectable = false + + hours.layer?.masksToBounds = false + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(day: PeerInfoHoursRowItem.Day) { + self.status.update(day.day) + + while hours.subviews.count > day.hours.count { + hours.subviews.last?.removeFromSuperview() + } + while hours.subviews.count < day.hours.count { + let view = TextView() + hours.addSubview(view) + } + + var y: CGFloat = 0 + var w: CGFloat = 0 + for (i, hour) in day.hours.enumerated() { + let view = hours.subviews[i] as! TextView + view.update(hour) + view.frame = NSMakeRect(0, y, view.frame.width, view.frame.height) + y += view.frame.height + 4 + w = max(w, view.frame.width) + } + hours.setFrameSize(NSMakeSize(w, y)) + needsLayout = true + } + + override func layout() { + super.layout() + self.status.setFrameOrigin(NSMakePoint(0, 10)) + self.hours.setFrameOrigin(NSMakePoint(frame.width - hours.frame.width, 10)) + } + } + + private let title = TextView() + private let status = TextView() + private let today = TextView() + private let daysContainer: View = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(title) + addSubview(status) + addSubview(today) + addSubview(daysContainer) + title.userInteractionEnabled = false + title.isSelectable = false + + today.userInteractionEnabled = false + today.isSelectable = false + + status.userInteractionEnabled = false + status.isSelectable = false + + containerView.set(handler: { [weak self] _ in + if let item = self?.item as? PeerInfoHoursRowItem { + item.open() + } + }, for: .Click) + + daysContainer.layer?.masksToBounds = false + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? PeerInfoHoursRowItem else { + return + } + + title.update(item.titleLayout) + status.update(item.statusLayout) + today.update(item.todayHoursLayout) + + if item.revealed { + while daysContainer.subviews.count > item.days.count { + daysContainer.subviews.last?.removeFromSuperview() + } + let subviews = daysContainer.subviews + for subview in subviews { + if subview.identifier == .init("removed") { + subview.removeFromSuperview() + } + } + while daysContainer.subviews.count < item.days.count { + let view = Day(frame: .zero) + daysContainer.addSubview(view) + if animated { + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + + var y: CGFloat = 0 + for (i, day) in item.days.enumerated() { + let view = daysContainer.subviews[i] as! Day + view.update(day: day) + view.frame = NSMakeRect(0, y, daysContainer.frame.width, day.height) + y += view.frame.height + } + + } else { + for subview in daysContainer.subviews { + performSubviewRemoval(subview, animated: animated) + subview.identifier = .init("removed") + } + } + } + + override func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + super.updateLayout(size: size, transition: transition) + + guard let item = item as? PeerInfoHoursRowItem else { + return + } + + + transition.updateFrame(view: title, frame: NSMakeRect(item.viewType.innerInset.left, item.viewType.innerInset.top, title.frame.width, title.frame.height)) + + transition.updateFrame(view: status, frame: NSMakeRect(item.viewType.innerInset.left, title.frame.maxY + 6, status.frame.width, status.frame.height)) + + transition.updateFrame(view: today, frame: NSMakeRect(containerView.frame.width - today.frame.width - item.viewType.innerInset.right, status.frame.minY, today.frame.width, today.frame.height)) + + transition.updateFrame(view: daysContainer, frame: NSMakeRect(item.viewType.innerInset.left, item.basicHeight, containerView.frame.width - item.viewType.innerInset.left * 2, 0)) + + + } +} diff --git a/Telegram-Mac/PremiumBoardingController.swift b/Telegram-Mac/PremiumBoardingController.swift index 651cbf65f..2b448a877 100644 --- a/Telegram-Mac/PremiumBoardingController.swift +++ b/Telegram-Mac/PremiumBoardingController.swift @@ -278,6 +278,8 @@ enum PremiumValue : String { case business_away_messages case business_chatbots + case folder_tags + var isBusiness: Bool { switch self { case .business_location, .business_hours, .business_quick_replies, .business_greeting_messages, .business_away_messages, .business_chatbots: @@ -302,6 +304,7 @@ enum PremiumValue : String { NSColor(rgb: 0x7861ff), NSColor(rgb: 0x8958ff), NSColor(rgb: 0x676bff), + NSColor(rgb: 0x4e8aea), NSColor(rgb: 0x5b79ff), NSColor(rgb: 0x4492ff), NSColor(rgb: 0x429bd5), @@ -416,6 +419,8 @@ enum PremiumValue : String { return NSImage(resource: .iconPremiumBusinessAway).precomposed(presentation.colors.accent) case .business_chatbots: return NSImage(resource: .iconPremiumBusinessBot).precomposed(presentation.colors.accent) + case .folder_tags: + return NSImage(resource: .iconPremiumBoardingTag).precomposed(presentation.colors.accent) } } @@ -474,6 +479,8 @@ enum PremiumValue : String { return "Away Messages" case .business_chatbots: return "ChatBots" + case .folder_tags: + return "Tag Your Chats" } } func info(_ limits: PremiumLimitConfig) -> String { @@ -531,6 +538,8 @@ enum PremiumValue : String { return "Define messages that are automatically sent when you are off." case .business_chatbots: return "Add any third party chatbots that will process customer interactions." + case .folder_tags: + return "Add colorful labels to chats for faster access in chat list." } } } @@ -597,7 +606,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("header"), equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in let status = ChatMessageItem.applyMessageEntities(with: [TextEntitiesMessageAttribute(entities: state.premiumConfiguration.statusEntities)], for: state.premiumConfiguration.status, message: nil, context: arguments.context, fontSize: 13, openInfo: arguments.openInfo, isDark: theme.colors.isDark, bubbled: theme.bubbled) - return PremiumBoardingHeaderItem(initialSize, stableId: stableId, context: arguments.context, presentation: arguments.presentation, isPremium: state.isPremium, peer: state.peer?.peer, emojiStatus: state.status, source: state.source, premiumText: status, viewType: .legacy) + return PremiumBoardingHeaderItem(initialSize, stableId: stableId, context: arguments.context, presentation: arguments.presentation, isPremium: state.isPremium, peer: state.peer?.peer, emojiStatus: state.status, source: state.source, premiumText: status, viewType: .legacy, sceneType: state.source == .business_standalone || state.source == .business ? .coin : .star) })) index += 1 @@ -1167,6 +1176,7 @@ final class PremiumBoardingController : ModalViewController { return true } + override func loadView() { if self.source == .business_standalone { self.leftBarView = getLeftBarViewOnce() diff --git a/Telegram-Mac/PremiumBoardingFeaturesController.swift b/Telegram-Mac/PremiumBoardingFeaturesController.swift index a6173373d..8349276f6 100644 --- a/Telegram-Mac/PremiumBoardingFeaturesController.swift +++ b/Telegram-Mac/PremiumBoardingFeaturesController.swift @@ -291,6 +291,14 @@ final class PremiumBoardingFeaturesView: View { }) slideView.addSlide(messagesPrivacy) + let folderTags = PremiumFeatureSlideView(frame: slideView.bounds, presentation: presentation) + folderTags.setup(context: context, type: .folder_tags, decoration: .badgeStars, getView: { _ in + let view = PremiumDemoLegacyPhoneView(frame: .zero) + view.setup(context: context, video: configuration.videos[PremiumValue.folder_tags.rawValue], position: .top) + return view + }) + slideView.addSlide(folderTags) + switch value { case .stories: slideView.displaySlide(at: 0, animated: false) @@ -332,6 +340,8 @@ final class PremiumBoardingFeaturesView: View { slideView.displaySlide(at: 18, animated: false) case .message_privacy: slideView.displaySlide(at: 19, animated: false) + case .folder_tags: + slideView.displaySlide(at: 20, animated: false) case .business_location: fatalError() case .business_hours: diff --git a/Telegram-Mac/PremiumBoardingHeaderItem.swift b/Telegram-Mac/PremiumBoardingHeaderItem.swift index a09c5e2da..4e78d73b1 100644 --- a/Telegram-Mac/PremiumBoardingHeaderItem.swift +++ b/Telegram-Mac/PremiumBoardingHeaderItem.swift @@ -14,99 +14,120 @@ import TGModernGrowingTextView import TelegramCore final class PremiumBoardingHeaderItem : GeneralRowItem { + + enum SceneType { + case coin + case star + } + fileprivate let titleLayout: TextViewLayout fileprivate let infoLayout: TextViewLayout let peer: Peer? let context: AccountContext let status: PremiumEmojiStatusInfo? let presentation: TelegramPresentationTheme - init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, presentation: TelegramPresentationTheme, isPremium: Bool, peer: Peer?, emojiStatus: PremiumEmojiStatusInfo?, source: PremiumLogEventsSource, premiumText: NSAttributedString?, viewType: GeneralViewType) { + let sceneType: SceneType + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, presentation: TelegramPresentationTheme, isPremium: Bool, peer: Peer?, emojiStatus: PremiumEmojiStatusInfo?, source: PremiumLogEventsSource, premiumText: NSAttributedString?, viewType: GeneralViewType, sceneType: SceneType) { self.context = context self.peer = peer self.status = emojiStatus self.presentation = presentation + self.sceneType = sceneType let title: NSAttributedString - if let peer = peer { - if case let .gift(from, _, months, _, _) = source { - let text: String - if from == context.peerId { - text = strings().premiumBoardingPeerGiftYouTitle(peer.displayTitle, "\(months)") - } else { - text = strings().premiumBoardingPeerGiftTitle(peer.displayTitle, "\(months)") - } - title = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.text), bold: MarkdownAttributeSet(font: .bold(.text), textColor: presentation.colors.text), link: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.peerAvatarVioletBottom), linkAttribute: { contents in - return (NSAttributedString.Key.link.rawValue, contents) - })) - } else if let status = emojiStatus { - - if let info = status.info { - let packName: String = info.title - let packFile: TelegramMediaFile = status.items.first?.file ?? status.file - - let attr = parseMarkdownIntoAttributedString(strings().premiumBoardingPeerStatusCustomTitle(peer.displayTitle, packName), attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.text), bold: MarkdownAttributeSet(font: .bold(.text), textColor: presentation.colors.text), link: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.peerAvatarVioletBottom), linkAttribute: { contents in - return (NSAttributedString.Key.link.rawValue, inAppLink.callback("", { _ in - showModal(with: StickerPackPreviewModalController(context, peerId: nil, references: [.emoji(.name(info.shortName))]), for: context.window) - })) - })) as! NSMutableAttributedString - - let range = attr.string.nsstring.range(of: "🤡") - if range.location != NSNotFound { - attr.addAttribute(TextInputAttributes.embedded, value: InlineStickerItem(source: .attribute(.init(fileId: packFile.fileId.id, file: packFile, emoji: ""))), range: range) + var info = NSMutableAttributedString() + + //TODOLANG + switch sceneType { + case .coin: + title = .initialize(string: "Telegram Business", color: presentation.colors.text, font: .medium(.header)) + if isPremium { + _ = info.append(string: "You have now unlocked these additional business features.", color: presentation.colors.text, font: .normal(.text)) + } else { + _ = info.append(string: "Turn your account into a business page with these additional features.", color: presentation.colors.text, font: .normal(.text)) + } + case .star: + if let peer = peer { + if case let .gift(from, _, months, _, _) = source { + let text: String + if from == context.peerId { + text = strings().premiumBoardingPeerGiftYouTitle(peer.displayTitle, "\(months)") + } else { + text = strings().premiumBoardingPeerGiftTitle(peer.displayTitle, "\(months)") } + title = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.text), bold: MarkdownAttributeSet(font: .bold(.text), textColor: presentation.colors.text), link: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.peerAvatarVioletBottom), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, contents) + })) + } else if let status = emojiStatus { - title = attr + if let info = status.info { + let packName: String = info.title + let packFile: TelegramMediaFile = status.items.first?.file ?? status.file + + let attr = parseMarkdownIntoAttributedString(strings().premiumBoardingPeerStatusCustomTitle(peer.displayTitle, packName), attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.text), bold: MarkdownAttributeSet(font: .bold(.text), textColor: presentation.colors.text), link: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.peerAvatarVioletBottom), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback("", { _ in + showModal(with: StickerPackPreviewModalController(context, peerId: nil, references: [.emoji(.name(info.shortName))]), for: context.window) + })) + })) as! NSMutableAttributedString + + let range = attr.string.nsstring.range(of: "🤡") + if range.location != NSNotFound { + attr.addAttribute(TextInputAttributes.embedded, value: InlineStickerItem(source: .attribute(.init(fileId: packFile.fileId.id, file: packFile, emoji: ""))), range: range) + } + + title = attr + } else { + title = .initialize(string: strings().premiumBoardingPeerStatusDefaultTitle(peer.displayTitle), color: presentation.colors.text, font: .medium(.header)) + } } else { - title = .initialize(string: strings().premiumBoardingPeerStatusDefaultTitle(peer.displayTitle), color: presentation.colors.text, font: .medium(.header)) + title = parseMarkdownIntoAttributedString(strings().premiumBoardingPeerTitle(peer.displayTitle), attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.text), bold: MarkdownAttributeSet(font: .bold(.text), textColor: presentation.colors.text), link: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.peerAvatarVioletBottom), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, contents) + })) } + } else { - title = parseMarkdownIntoAttributedString(strings().premiumBoardingPeerTitle(peer.displayTitle), attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.text), bold: MarkdownAttributeSet(font: .bold(.text), textColor: presentation.colors.text), link: MarkdownAttributeSet(font: .medium(.header), textColor: presentation.colors.peerAvatarVioletBottom), linkAttribute: { contents in - return (NSAttributedString.Key.link.rawValue, contents) - })) + if isPremium { + title = .initialize(string: strings().premiumBoardingGotTitle, color: presentation.colors.text, font: .medium(.header)) + } else { + title = .initialize(string: strings().premiumBoardingTitle, color: presentation.colors.text, font: .medium(.header)) + } } - - } else { - if isPremium { - title = .initialize(string: strings().premiumBoardingGotTitle, color: presentation.colors.text, font: .medium(.header)) + if let _ = peer { + if case let .gift(from, _, _, slug, _) = source { + let text: String + if from == context.peerId { + text = strings().premiumBoardingPeerGiftYouInfo + } else { + if let _ = slug { + text = strings().premiumBoardingPeerGiftLinkInfo + } else { + text = strings().premiumBoardingPeerGiftInfo + } + } + _ = info.append(string: text, color: presentation.colors.text, font: .normal(.text)) + } else if let _ = peer?.emojiStatus { + _ = info.append(string: strings().premiumBoardingPeerStatusInfo, color: presentation.colors.text, font: .normal(.text)) + } else { + _ = info.append(string: strings().premiumBoardingPeerInfo, color: presentation.colors.text, font: .normal(.text)) + } + info.detectBoldColorInString(with: .medium(.text)) + } else { - title = .initialize(string: strings().premiumBoardingTitle, color: presentation.colors.text, font: .medium(.header)) + if isPremium, let premiumText = premiumText { + info = premiumText.mutableCopy() as! NSMutableAttributedString + } else { + _ = info.append(string: strings().premiumBoardingInfo, color: presentation.colors.text, font: .normal(.text)) + info.detectBoldColorInString(with: .medium(.text)) + } } + } + self.titleLayout = .init(title, alignment: .center) self.titleLayout.interactions = globalLinkExecutor - var info = NSMutableAttributedString() - if let _ = peer { - - if case let .gift(from, _, _, slug, _) = source { - let text: String - if from == context.peerId { - text = strings().premiumBoardingPeerGiftYouInfo - } else { - if let _ = slug { - text = strings().premiumBoardingPeerGiftLinkInfo - } else { - text = strings().premiumBoardingPeerGiftInfo - } - } - _ = info.append(string: text, color: presentation.colors.text, font: .normal(.text)) - } else if let _ = peer?.emojiStatus { - _ = info.append(string: strings().premiumBoardingPeerStatusInfo, color: presentation.colors.text, font: .normal(.text)) - } else { - _ = info.append(string: strings().premiumBoardingPeerInfo, color: presentation.colors.text, font: .normal(.text)) - } - info.detectBoldColorInString(with: .medium(.text)) - - } else { - if isPremium, let premiumText = premiumText { - info = premiumText.mutableCopy() as! NSMutableAttributedString - } else { - _ = info.append(string: strings().premiumBoardingInfo, color: presentation.colors.text, font: .normal(.text)) - info.detectBoldColorInString(with: .medium(.text)) - } - } self.infoLayout = .init(info, alignment: .center) self.infoLayout.interactions = globalLinkExecutor super.init(initialSize, stableId: stableId) @@ -138,7 +159,7 @@ final class PremiumBoardingHeaderItem : GeneralRowItem { private final class PremiumBoardingHeaderView : TableRowView { - private var premiumView: PremiumStarSceneView? + private var premiumView: (PremiumSceneView & NSView)? private var statusView: InlineStickerView? private let titleView = TextView() private let infoView = TextView() @@ -228,11 +249,16 @@ private final class PremiumBoardingHeaderView : TableRowView { performSubviewRemoval(view, animated: animated) self.statusView = nil } - let current: PremiumStarSceneView + let current: (PremiumSceneView & NSView) if let view = self.premiumView { current = view } else { - current = PremiumStarSceneView(frame: NSMakeRect(0, 0, frame.width, 150)) + switch item.sceneType { + case .coin: + current = PremiumCoinSceneView(frame: NSMakeRect(0, 0, frame.width, 150)) + case .star: + current = PremiumStarSceneView(frame: NSMakeRect(0, 0, frame.width, 150)) + } addSubview(current) self.premiumView = current } diff --git a/Telegram-Mac/PremiumCoinSceneView.swift b/Telegram-Mac/PremiumCoinSceneView.swift new file mode 100644 index 000000000..7fceb802e --- /dev/null +++ b/Telegram-Mac/PremiumCoinSceneView.swift @@ -0,0 +1,311 @@ +// +// PremiumCoinSceneView.swift +// Telegram +// +// Created by Mikhail Filimonov on 19.02.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SceneKit +import SwiftSignalKit +import GZIP + +private let sceneVersion: Int = 0 + + +final class PremiumCoinSceneView: View, SCNSceneRendererDelegate, PremiumSceneView { + + private let sceneView: SCNView + + private let tapDelay = MetaDisposable() + private let appearanceDelay = MetaDisposable() + + deinit { + appearanceDelay.dispose() + tapDelay.dispose() + } + + required init(frame: CGRect) { + self.sceneView = SCNView(frame: frame) + self.sceneView.backgroundColor = .clear +// self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) +// self.sceneView.isUserInteractionEnabled = false + + super.init(frame: frame) + sceneView.wantsLayer = true + self.addSubview(self.sceneView) + self.layer?.masksToBounds = false + sceneView.layer?.masksToBounds = false + self.setup() + + let panGestureRecoginzer = NSPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + self.addGestureRecognizer(panGestureRecoginzer) + + let tapGestureRecoginzer = NSClickGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.addGestureRecognizer(tapGestureRecoginzer) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func handleTap(_ gesture: NSGestureRecognizer) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + var left = true + if let view = gesture.view { + let point = gesture.location(in: view) + let distanceFromCenter = abs(point.x - view.frame.size.width / 2.0) + if distanceFromCenter > 60.0 { + return + } + if point.x > view.frame.width / 2.0 { + left = false + } + } + + if node.animationKeys.contains("tapRotate") { + self.playAppearanceAnimation(velocity: nil, mirror: left, explode: true) + return + } + + let initial = node.rotation + let target = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: left ? -0.6 : 0.6) + + let animation = CASpringAnimation(keyPath: "rotation") + animation.fromValue = NSValue(scnVector4: initial) + animation.toValue = NSValue(scnVector4: target) + animation.duration = 0.25 + animation.timingFunction = CAMediaTimingFunction(name: .easeOut) + animation.fillMode = .forwards + node.addAnimation(animation, forKey: "tapRotate") + + node.rotation = target + + tapDelay.set(delaySignal(0.25).start(completed: { + node.rotation = initial + let springAnimation = CASpringAnimation(keyPath: "rotation") + springAnimation.fromValue = NSValue(scnVector4: target) + springAnimation.toValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0)) + springAnimation.mass = 1.0 + springAnimation.stiffness = 21.0 + springAnimation.damping = 5.8 + springAnimation.duration = springAnimation.settlingDuration * 0.8 + node.addAnimation(springAnimation, forKey: "tapRotate") + })) +// delay(0.25, closure: { +// +// }) +// + } + + private var previousAngle: Float = 0.0 + @objc private func handlePan(_ gesture: NSPanGestureRecognizer) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + if #available(macOS 10.13, *) { + node.removeAnimation(forKey: "rotate", blendOutDuration: 0.1) + node.removeAnimation(forKey: "tapRotate", blendOutDuration: 0.1) + } else { + node.removeAllAnimations() + } + + switch gesture.state { + case .began: + self.previousAngle = 0.0 + case .changed: + let translation = gesture.translation(in: gesture.view) +// let anglePan = deg2rad(Float(translation.x)) + + let x = Float(translation.x) + let y = Float(-translation.y) + + let anglePan = sqrt(pow(x,2)+pow(y,2))*(Float)(Float.pi)/180.0 + + var rotationVector = SCNVector4() + rotationVector.x = CGFloat(-y) + rotationVector.y = CGFloat(x) + rotationVector.z = 0 + rotationVector.w = CGFloat(anglePan) + + self.previousAngle = anglePan + node.rotation = rotationVector//SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: CGFloat(self.previousAngle)) + case .ended: + + if self.previousAngle == 0 { + handleTap(gesture) + return; + } + let velocity = gesture.velocity(in: gesture.view) + + var smallAngle = false + if (self.previousAngle < .pi / 2 && self.previousAngle > -.pi / 2) && abs(velocity.x) < 200 { + smallAngle = true + } + + self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600) + node.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0) + default: + break + } + } + + private func setup() { + guard let url = Bundle.main.url(forResource: "coin", withExtension: "scn") else { + return + } + guard let scene = try? SCNScene(url: url, options: nil) else { + return + } + +// self.sceneView.col = .bgra8Unorm_srgb + self.sceneView.backgroundColor = .clear + self.sceneView.preferredFramesPerSecond = 60 + self.sceneView.isJitteringEnabled = true + + self.sceneView.scene = scene + self.sceneView.delegate = self + + let _ = self.sceneView.snapshot() + } + + private var didSetReady = false + func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { + if !self.didSetReady { + self.didSetReady = true + + self.onReady() + } + } + + private func onReady() { + self.setupGradientAnimation() + self.setupShineAnimation() + + self.playAppearanceAnimation(explode: true) + } + + private func setupGradientAnimation() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else { + return + } + + let animation = CABasicAnimation(keyPath: "contentsTransform") + animation.duration = 4.5 + animation.fromValue = NSValue(scnMatrix4: initial) + animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -0.35, 0.35, 0)) + animation.timingFunction = CAMediaTimingFunction(name: .linear) + animation.autoreverses = true + animation.repeatCount = .infinity + + node.geometry?.materials.first?.diffuse.addAnimation(animation, forKey: "gradient") + } + + private func setupShineAnimation() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + guard let initial = node.geometry?.materials.first?.emission.contentsTransform else { + return + } + + let animation = CABasicAnimation(keyPath: "contentsTransform") + animation.fillMode = .forwards + animation.fromValue = NSValue(scnMatrix4: initial) + animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -1.6, 0.0, 0.0)) + animation.timingFunction = CAMediaTimingFunction(name: .easeOut) + animation.beginTime = 0.6 + animation.duration = 0.9 + + let group = CAAnimationGroup() + group.animations = [animation] + group.beginTime = 1.0 + group.duration = 3.0 + group.repeatCount = .infinity + + node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer") + + if #available(macOS 14.0, *), let material = node.geometry?.materials.first { + material.metalness.intensity = 0.2 + } + } + + func showStar() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + node.isHidden = false + } + func hideStar() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + node.isHidden = true + } + + private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + + if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) { + let particleSystem = particles.particleSystems?.first + particleSystem?.particleColorVariation = SCNVector4(0.15, 0.2, 0.35, 0.3) + particleSystem?.particleVelocity = 2.2 + particleSystem?.birthRate = 4.5 + particleSystem?.particleLifeSpan = 2.0 + + node.physicsField?.isActive = true + appearanceDelay.set(delaySignal(1.0).start(completed: { + node.physicsField?.isActive = false + particles.particleSystems?.first?.birthRate = 1.2 + particleSystem?.particleVelocity = 1.65 + particleSystem?.particleLifeSpan = 4.0 + })) + } + + let from = node.presentation.rotation + node.removeAnimation(forKey: "tapRotate") + + var toValue: Float = smallAngle ? 0.0 : .pi * 2.0 + if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 { + toValue *= -1 + } + if mirror { + toValue *= -1 + } + let to = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: CGFloat(toValue)) + let distance = rad2deg(Float(to.w - from.w)) + + let springAnimation = CASpringAnimation(keyPath: "rotation") + springAnimation.fromValue = NSValue(scnVector4: from) + springAnimation.toValue = NSValue(scnVector4: to) + springAnimation.mass = 1.0 + springAnimation.stiffness = 21.0 + springAnimation.damping = 5.8 + springAnimation.duration = springAnimation.settlingDuration * 0.75 + springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7 + + node.addAnimation(springAnimation, forKey: "rotate") + } + + func playAgain() { + self.playAppearanceAnimation(explode: true) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: size.width * 2.0, height: size.height * 2.0)) +// self.sceneView.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + } +} diff --git a/Telegram-Mac/PremiumLimitConfig.swift b/Telegram-Mac/PremiumLimitConfig.swift index 7ba382d65..981cd3f5f 100644 --- a/Telegram-Mac/PremiumLimitConfig.swift +++ b/Telegram-Mac/PremiumLimitConfig.swift @@ -27,11 +27,8 @@ final class PremiumPromoOrder { } #if DEBUG - if !premiumValues.contains(.last_seen) { - premiumValues.append(.last_seen) - } - if !premiumValues.contains(.message_privacy) { - premiumValues.append(.message_privacy) + if !premiumValues.contains(.folder_tags) { + premiumValues.append(.folder_tags) } #endif } diff --git a/Telegram-Mac/PremiumStarSceneView.swift b/Telegram-Mac/PremiumStarSceneView.swift index 4b1a60689..915e9af6b 100644 --- a/Telegram-Mac/PremiumStarSceneView.swift +++ b/Telegram-Mac/PremiumStarSceneView.swift @@ -16,15 +16,12 @@ private let sceneVersion: Int = 2 -private func deg2rad(_ number: Float) -> Float { - return number * .pi / 180 +protocol PremiumSceneView { + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) + func playAgain() } -private func rad2deg(_ number: Float) -> Float { - return number * 180.0 / .pi -} - -final class PremiumStarSceneView: View, SCNSceneRendererDelegate { +final class PremiumStarSceneView: View, SCNSceneRendererDelegate, PremiumSceneView { private let sceneView: SCNView diff --git a/Telegram-Mac/PreviewSenderController.swift b/Telegram-Mac/PreviewSenderController.swift index 9a13c5240..3d559546f 100644 --- a/Telegram-Mac/PreviewSenderController.swift +++ b/Telegram-Mac/PreviewSenderController.swift @@ -989,7 +989,7 @@ class PreviewSenderController: ModalViewController, Notifable { self?.sendCurrentMedia?(silent, date, asSpoiler) }), for: context.window) } - case .history, .thread: + case .history, .thread, .customChatContents: sendCurrentMedia?(silent, atDate, asSpoiler) case .pinned: break diff --git a/Telegram-Mac/PrivacyAndSecurityViewController.swift b/Telegram-Mac/PrivacyAndSecurityViewController.swift index 278749c23..d62d9248f 100644 --- a/Telegram-Mac/PrivacyAndSecurityViewController.swift +++ b/Telegram-Mac/PrivacyAndSecurityViewController.swift @@ -392,7 +392,7 @@ private enum PrivacyAndSecurityEntry: Comparable, Identifiable { arguments.openTwoStepVerification(configuration) }) case let .globalTimer(_, text, viewType): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: strings().privacySettingsGlobalTimer, icon: theme.icons.privacy_settings_autodelete, type: .context(text), viewType: viewType, action: arguments.setupGlobalAutoremove) + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: strings().privacySettingsGlobalTimer, icon: theme.icons.privacy_settings_autodelete, type: .nextContext(text), viewType: viewType, action: arguments.setupGlobalAutoremove) case .globalTimerInfo: return GeneralTextRowItem(initialSize, stableId: stableId, text: strings().privacySettingsGlobalTimerInfo, viewType: .textBottomItem) case let .activeSessions(_, sessions, viewType): diff --git a/Telegram-Mac/SenderController.swift b/Telegram-Mac/SenderController.swift index 84ce1fff4..4c4a2d485 100644 --- a/Telegram-Mac/SenderController.swift +++ b/Telegram-Mac/SenderController.swift @@ -164,7 +164,7 @@ class Sender: NSObject { return preview } - public static func enqueue( input:ChatTextInputState, context: AccountContext, peerId:PeerId, replyId:EngineMessageReplySubject?, threadId: Int64?, replyStoryId: StoryId? = nil, disablePreview:Bool = false, linkBelowMessage: Bool = false, largeMedia: Bool? = nil, silent: Bool = false, atDate:Date? = nil, sendAsPeerId: PeerId? = nil, mediaPreview: TelegramMediaWebpage? = nil, emptyHandler:(()->Void)? = nil) ->Signal<[MessageId?],NoError> { + public static func enqueue( input:ChatTextInputState, context: AccountContext, peerId:PeerId, replyId:EngineMessageReplySubject?, threadId: Int64?, replyStoryId: StoryId? = nil, disablePreview:Bool = false, linkBelowMessage: Bool = false, largeMedia: Bool? = nil, silent: Bool = false, atDate:Date? = nil, sendAsPeerId: PeerId? = nil, mediaPreview: TelegramMediaWebpage? = nil, emptyHandler:(()->Void)? = nil, customChatContents: ChatCustomContentsProtocol? = nil) -> Signal<[MessageId?],NoError> { var inset:Int = 0 let dynamicEmojiOrder = context.stickerSettings.dynamicPackOrder @@ -236,24 +236,30 @@ class Sender: NSObject { } } - if !mapped.isEmpty { - let inlineMedia = input.inlineMedia.map { $0.key } - return enqueueMessages(account: context.account, peerId: peerId, messages: mapped) |> mapToSignal { value in - if !emojis.isEmpty { - let es = saveUsedEmoji(emojis, postbox: context.account.postbox) - let aes = saveAnimatedUsedEmoji(inlineMedia, postbox: context.account.postbox) - return combineLatest(es, aes) |> map { _ in - return value + if let customChatContents = customChatContents { + return customChatContents.enqueueMessages(messages: mapped) + } else { + + if !mapped.isEmpty { + let inlineMedia = input.inlineMedia.map { $0.key } + return enqueueMessages(account: context.account, peerId: peerId, messages: mapped) |> mapToSignal { value in + if !emojis.isEmpty { + let es = saveUsedEmoji(emojis, postbox: context.account.postbox) + let aes = saveAnimatedUsedEmoji(inlineMedia, postbox: context.account.postbox) + return combineLatest(es, aes) |> map { _ in + return value + } } + return .single(value) + } |> deliverOnMainQueue + } else { + DispatchQueue.main.async { + emptyHandler?() } - return .single(value) - } |> deliverOnMainQueue - } else { - DispatchQueue.main.async { - emptyHandler?() + return .complete() } - return .complete() } + } public static func enqueue(message:EnqueueMessage, context: AccountContext, peerId:PeerId) ->Signal<[MessageId?],NoError> { @@ -511,7 +517,7 @@ class Sender: NSObject { return enqueueMessages(account: context.account, peerId: peerId, messages: [EnqueueMessage.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: media), threadId: threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } - public static func enqueue(media:[MediaSenderContainer], context: AccountContext, peerId:PeerId, replyId: EngineMessageReplySubject?, threadId: Int64?, replyStoryId: StoryId? = nil, silent: Bool = false, atDate:Date? = nil, sendAsPeerId:PeerId? = nil, query: String? = nil, isSpoiler: Bool = false) ->Signal<[MessageId?], NoError> { + public static func enqueue(media:[MediaSenderContainer], context: AccountContext, peerId:PeerId, replyId: EngineMessageReplySubject?, threadId: Int64?, replyStoryId: StoryId? = nil, silent: Bool = false, atDate:Date? = nil, sendAsPeerId:PeerId? = nil, query: String? = nil, isSpoiler: Bool = false, customChatContents: ChatCustomContentsProtocol? = nil) ->Signal<[MessageId?], NoError> { var senders:[Signal<[MessageId?], NoError>] = [] @@ -542,7 +548,12 @@ class Sender: NSObject { } senders.append(generateMedia(for: path, account: context.account, isSecretRelated: peerId.namespace == Namespaces.Peer.SecretChat) |> mapToSignal { media, caption -> Signal< [MessageId?], NoError> in - return enqueueMessages(account: context.account, peerId: peerId, messages: [EnqueueMessage.message(text: caption, attributes:attributes, inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: media), threadId: threadId, replyToMessageId: replyId, replyToStoryId: replyStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + let message = EnqueueMessage.message(text: caption, attributes:attributes, inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: media), threadId: threadId, replyToMessageId: replyId, replyToStoryId: replyStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + if let customChatContents { + return customChatContents.enqueueMessages(messages: [message]) + } else { + return enqueueMessages(account: context.account, peerId: peerId, messages: [message]) + } }) } @@ -557,11 +568,11 @@ class Sender: NSObject { } |> take(1) } - public static func enqueue(media:Media, context: AccountContext, peerId:PeerId, replyId:EngineMessageReplySubject?, threadId: Int64?, replyStoryId: StoryId? = nil, silent: Bool = false, atDate: Date? = nil, query: String? = nil, collectionId: ItemCollectionId? = nil) ->Signal<[MessageId?],NoError> { - return enqueue(media: [media], caption: ChatTextInputState(), context: context, peerId: peerId, replyId: replyId, threadId: threadId, replyStoryId: replyStoryId, silent: silent, atDate: atDate, query: query, collectionId: collectionId) + public static func enqueue(media:Media, context: AccountContext, peerId:PeerId, replyId:EngineMessageReplySubject?, threadId: Int64?, replyStoryId: StoryId? = nil, silent: Bool = false, atDate: Date? = nil, query: String? = nil, collectionId: ItemCollectionId? = nil, customChatContents: ChatCustomContentsProtocol? = nil) ->Signal<[MessageId?],NoError> { + return enqueue(media: [media], caption: ChatTextInputState(), context: context, peerId: peerId, replyId: replyId, threadId: threadId, replyStoryId: replyStoryId, silent: silent, atDate: atDate, query: query, collectionId: collectionId, customChatContents: customChatContents) } - public static func enqueue(media:[Media], caption: ChatTextInputState, context: AccountContext, peerId:PeerId, replyId:EngineMessageReplySubject?, threadId: Int64?, replyStoryId: StoryId? = nil, isCollage: Bool = false, additionText: ChatTextInputState? = nil, silent: Bool = false, atDate: Date? = nil, sendAsPeerId: PeerId? = nil, query: String? = nil, collectionId: ItemCollectionId? = nil, isSpoiler: Bool = false) ->Signal<[MessageId?],NoError> { + public static func enqueue(media:[Media], caption: ChatTextInputState, context: AccountContext, peerId:PeerId, replyId:EngineMessageReplySubject?, threadId: Int64?, replyStoryId: StoryId? = nil, isCollage: Bool = false, additionText: ChatTextInputState? = nil, silent: Bool = false, atDate: Date? = nil, sendAsPeerId: PeerId? = nil, query: String? = nil, collectionId: ItemCollectionId? = nil, isSpoiler: Bool = false, customChatContents: ChatCustomContentsProtocol? = nil) ->Signal<[MessageId?],NoError> { let dynamicEmojiOrder: Bool = context.stickerSettings.dynamicPackOrder @@ -639,7 +650,11 @@ class Sender: NSObject { } messages.insert(contentsOf: mapped, at: 0) } - return enqueueMessages(account: context.account, peerId: peerId, messages: messages) |> deliverOnMainQueue |> take(1) + if let customChatContents { + return customChatContents.enqueueMessages(messages: messages) |> deliverOnMainQueue |> take(1) + } else { + return enqueueMessages(account: context.account, peerId: peerId, messages: messages) |> deliverOnMainQueue |> take(1) + } } diff --git a/Telegram-Mac/UserInfoEntries.swift b/Telegram-Mac/UserInfoEntries.swift index d2236fef6..476df81ac 100644 --- a/Telegram-Mac/UserInfoEntries.swift +++ b/Telegram-Mac/UserInfoEntries.swift @@ -48,12 +48,13 @@ final class UserInfoState : PeerInfoState { let savingData: Bool let updatingPhotoState:PeerInfoUpdatingPhotoState? let suggestingPhotoState:PeerInfoUpdatingPhotoState? - - init(editingState: UserInfoEditingState?, savingData: Bool, updatingPhotoState:PeerInfoUpdatingPhotoState?, suggestingPhotoState:PeerInfoUpdatingPhotoState?) { + let businessHoursRevealed: Bool + init(editingState: UserInfoEditingState?, savingData: Bool, updatingPhotoState:PeerInfoUpdatingPhotoState?, suggestingPhotoState:PeerInfoUpdatingPhotoState?, businessHoursRevealed: Bool) { self.editingState = editingState self.savingData = savingData self.updatingPhotoState = updatingPhotoState self.suggestingPhotoState = suggestingPhotoState + self.businessHoursRevealed = businessHoursRevealed } override init() { @@ -61,6 +62,7 @@ final class UserInfoState : PeerInfoState { self.savingData = false self.updatingPhotoState = nil self.suggestingPhotoState = nil + self.businessHoursRevealed = false } func isEqual(to: PeerInfoState) -> Bool { @@ -83,30 +85,35 @@ final class UserInfoState : PeerInfoState { if lhs.suggestingPhotoState != rhs.suggestingPhotoState { return false } - + if lhs.businessHoursRevealed != rhs.businessHoursRevealed { + return false + } return true } func withUpdatedSavingData(_ savingData: Bool) -> UserInfoState { - return UserInfoState(editingState: self.editingState, savingData: savingData, updatingPhotoState: self.updatingPhotoState, suggestingPhotoState: self.suggestingPhotoState) + return UserInfoState(editingState: self.editingState, savingData: savingData, updatingPhotoState: self.updatingPhotoState, suggestingPhotoState: self.suggestingPhotoState, businessHoursRevealed: self.businessHoursRevealed) } func withUpdatedEditingState(_ editingState: UserInfoEditingState?) -> UserInfoState { - return UserInfoState(editingState: editingState, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, suggestingPhotoState: self.suggestingPhotoState) + return UserInfoState(editingState: editingState, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, suggestingPhotoState: self.suggestingPhotoState, businessHoursRevealed: self.businessHoursRevealed) } func withUpdatedUpdatingPhotoState(_ f: (PeerInfoUpdatingPhotoState?) -> PeerInfoUpdatingPhotoState?) -> UserInfoState { - return UserInfoState(editingState: self.editingState, savingData: self.savingData, updatingPhotoState: f(self.updatingPhotoState), suggestingPhotoState: self.suggestingPhotoState) + return UserInfoState(editingState: self.editingState, savingData: self.savingData, updatingPhotoState: f(self.updatingPhotoState), suggestingPhotoState: self.suggestingPhotoState, businessHoursRevealed: self.businessHoursRevealed) } func withoutUpdatingPhotoState() -> UserInfoState { - return UserInfoState(editingState: self.editingState, savingData: self.savingData, updatingPhotoState: nil, suggestingPhotoState: self.suggestingPhotoState) + return UserInfoState(editingState: self.editingState, savingData: self.savingData, updatingPhotoState: nil, suggestingPhotoState: self.suggestingPhotoState, businessHoursRevealed: self.businessHoursRevealed) } func withUpdatedSuggestingPhotoState(_ f: (PeerInfoUpdatingPhotoState?) -> PeerInfoUpdatingPhotoState?) -> UserInfoState { - return UserInfoState(editingState: self.editingState, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, suggestingPhotoState: f(self.updatingPhotoState)) + return UserInfoState(editingState: self.editingState, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, suggestingPhotoState: f(self.updatingPhotoState), businessHoursRevealed: self.businessHoursRevealed) } func withoutSuggestingPhotoState() -> UserInfoState { - return UserInfoState(editingState: self.editingState, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, suggestingPhotoState: nil) + return UserInfoState(editingState: self.editingState, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, suggestingPhotoState: nil, businessHoursRevealed: self.businessHoursRevealed) + } + func withBusinessHoursRevealed(_ revealed: Bool) -> UserInfoState { + return UserInfoState(editingState: self.editingState, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, suggestingPhotoState: self.suggestingPhotoState, businessHoursRevealed: revealed) } } @@ -305,6 +312,18 @@ class UserInfoArguments : PeerInfoArguments { } + func openLocation(_ peer: Peer, _ cachedData: CachedUserData) { + showModal(with: LocationModalPreview(context, map: .init(latitude: 25.08405406819793, longitude: 55.13948416803165, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil), peer: peer, messageId: nil), for: context.window) + } + func openHours(_ peer: Peer, _ cachedData: CachedUserData) { + let updateState:((UserInfoState)->UserInfoState)->Void = { [weak self] f in + self?.updateState(f) + } + updateState { state in + return state.withBusinessHoursRevealed(!state.businessHoursRevealed) + } + } + func reportReaction(_ messageId: MessageId) { let block: Signal = context.blockedPeersContext.add(peerId: peerId) |> `catch` { _ in .complete() } let report = context.engine.peers.reportPeerReaction(authorId: self.peerId, messageId: messageId) |> ignoreValues @@ -924,6 +943,8 @@ enum UserInfoEntry: PeerInfoEntry { case scam(sectionId:Int, title: String, text: String, viewType: GeneralViewType) case phoneNumber(sectionId:Int, index: Int, value: PhoneNumberWithLabel, canCopy: Bool, viewType: GeneralViewType) case userName(sectionId:Int, value: [String], viewType: GeneralViewType) + case businessLocation(sectionId:Int, peer: EnginePeer, cachedData: CachedDataEquatable, viewType: GeneralViewType) + case businessHours(sectionId:Int, peer: EnginePeer, cachedData: CachedDataEquatable, revealed: Bool, viewType: GeneralViewType) case reportReaction(sectionId: Int, value: MessageId, viewType: GeneralViewType) case sendMessage(sectionId:Int, viewType: GeneralViewType) case shareContact(sectionId:Int, viewType: GeneralViewType) @@ -964,6 +985,8 @@ enum UserInfoEntry: PeerInfoEntry { case let .scam(sectionId, title, text, _): return .scam(sectionId: sectionId, title: title, text: text, viewType: viewType) case let .phoneNumber(sectionId, index, value, canCopy, _): return .phoneNumber(sectionId: sectionId, index: index, value: value, canCopy: canCopy, viewType: viewType) case let .userName(sectionId, value, _): return .userName(sectionId: sectionId, value: value, viewType: viewType) + case let .businessLocation(sectionId, peer, cachedData, _): return .businessLocation(sectionId: sectionId, peer: peer, cachedData: cachedData, viewType: viewType) + case let .businessHours(sectionId, peer, cachedData, revealed, _): return .businessHours(sectionId: sectionId, peer: peer, cachedData: cachedData, revealed: revealed, viewType: viewType) case let .reportReaction(sectionId, value, _): return .reportReaction(sectionId: sectionId, value: value, viewType: viewType) case let .sendMessage(sectionId, _): return .sendMessage(sectionId: sectionId, viewType: viewType) case let .shareContact(sectionId, _): return .shareContact(sectionId: sectionId, viewType: viewType) @@ -1139,6 +1162,20 @@ enum UserInfoEntry: PeerInfoEntry { default: return false } + case let .businessLocation(sectionId, peer, cachedData, viewType): + switch entry { + case .businessLocation(sectionId, peer: peer, cachedData: cachedData, viewType): + return true + default: + return false + } + case let .businessHours(sectionId, peer, cachedData, revealed, viewType): + switch entry { + case .businessHours(sectionId, peer: peer, cachedData: cachedData, revealed, viewType): + return true + default: + return false + } case let .reportReaction(sectionId, value, viewType): switch entry { case .reportReaction(sectionId, value, viewType): @@ -1343,52 +1380,56 @@ enum UserInfoEntry: PeerInfoEntry { return 111 case .userName: return 112 - case .sendMessage: + case .businessHours: return 113 - case .botAddToGroup: + case .businessLocation: return 114 - case .botAddToGroupInfo: + case .sendMessage: return 115 - case .botShare: + case .botAddToGroup: return 116 - case .botSettings: + case .botAddToGroupInfo: return 117 - case .botHelp: + case .botShare: return 118 - case .botPrivacy: + case .botSettings: return 119 - case .shareContact: + case .botHelp: return 120 - case .shareMyInfo: + case .botPrivacy: return 121 - case .addContact: + case .shareContact: return 122 - case .startSecretChat: + case .shareMyInfo: return 123 - case .sharedMedia: + case .addContact: return 124 - case .notifications: + case .startSecretChat: return 125 - case .encryptionKey: + case .sharedMedia: return 126 - case .groupInCommon: + case .notifications: return 127 + case .encryptionKey: + return 128 + case .groupInCommon: + return 129 case let .setPhoto(_, _, type, _, _): - return 128 + type.rawValue + return 130 + type.rawValue case .resetPhoto: - return 131 + return 134 case .setPhotoInfo: - return 132 + return 135 case .block: - return 133 + return 136 case .reportReaction: - return 134 + return 137 case .deleteChat: - return 135 + return 138 case .deleteContact: - return 136 + return 139 case .media: - return 137 + return 140 case let .section(id): return (id + 1) * 1000 - id } @@ -1420,6 +1461,10 @@ enum UserInfoEntry: PeerInfoEntry { return (sectionId * 1000) + stableIndex case let .userName(sectionId, _, _): return (sectionId * 1000) + stableIndex + case let .businessHours(sectionId, _, _, _, _): + return (sectionId * 1000) + stableIndex + case let .businessLocation(sectionId, _, _, _): + return (sectionId * 1000) + stableIndex case let .reportReaction(sectionId, _, _): return (sectionId * 1000) + stableIndex case let .scam(sectionId, _, _, _): @@ -1573,6 +1618,14 @@ enum UserInfoEntry: PeerInfoEntry { return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label: strings().peerInfoUsername, copyMenuText: strings().textCopyLabelUsername, labelColor: theme.colors.text, text: text, context: arguments.context, viewType: viewType, detectLinks: true, isTextSelectable: value.count > 1, _copyToClipboard: { arguments.copy(link) }, linkInteractions: interactions) + case let .businessLocation(_, peer, cachedData, viewType): + return PeerInfoLocationRowItem(initialSize, stableId: stableId.hashValue, context: arguments.context, peer: peer._asPeer(), cachedData: cachedData.data as! CachedUserData, viewType: viewType, open: { + arguments.openLocation(peer._asPeer(), cachedData.data as! CachedUserData) + }) + case let .businessHours(_, peer, cachedData, revealed, viewType): + return PeerInfoHoursRowItem(initialSize, stableId: stableId.hashValue, context: arguments.context, revealed: revealed, peer: peer._asPeer(), cachedData: cachedData.data as! CachedUserData, viewType: viewType, open: { + arguments.openHours(peer._asPeer(), cachedData.data as! CachedUserData) + }) case let .reportReaction(_, value, viewType): return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: strings().peerInfoReportReaction, nameStyle: redActionButton, type: .none, viewType: viewType, action: { arguments.reportReaction(value) @@ -1754,6 +1807,13 @@ func userInfoEntries(view: PeerView, arguments: PeerInfoArguments, mediaTabsData infoBlock.append(.userName(sectionId: sectionId, value: usernames, viewType: .singleItem)) } + #if DEBUG + if let cachedUserData = view.cachedData as? CachedUserData { + infoBlock.append(.businessHours(sectionId: sectionId, peer: .init(peer), cachedData: .init(cachedUserData), revealed: state.businessHoursRevealed, viewType: .singleItem)) + infoBlock.append(.businessLocation(sectionId: sectionId, peer: .init(peer), cachedData: .init(cachedUserData), viewType: .singleItem)) + } + #endif + if !user.isBot { if !view.peerIsContact, user.id != arguments.context.peerId { infoBlock.append(.addContact(sectionId: sectionId, viewType: .singleItem)) diff --git a/Telegram-Mac/premium/coin.png b/Telegram-Mac/premium/coin.png new file mode 100644 index 0000000000000000000000000000000000000000..90f45f3bd103815072a399448597c11408949617 GIT binary patch literal 16191 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 literal 0 HcmV?d00001 diff --git a/Telegram-Mac/premium/coin.scn b/Telegram-Mac/premium/coin.scn new file mode 100644 index 0000000000000000000000000000000000000000..734babbbc17154a879bb89ebe2acc6effbd8f699 GIT binary patch literal 844313 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⁡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 z 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

{O0eEu@-z$A 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

K^>!N7;6*xS}2y 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 D3^EyCTY=ryP3xFy9qX4n5`2 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 ImdMeZiPLXUHu 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 literal 0 HcmV?d00001 diff --git a/Telegram-Mac/premium/coin_anim.png b/Telegram-Mac/premium/coin_anim.png new file mode 100644 index 0000000000000000000000000000000000000000..8ce4e406ab32a2f94c22a8950160c5d89998b0e2 GIT binary patch literal 61046 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 literal 0 HcmV?d00001 diff --git a/Telegram-Mac/premium/coin_edge.png b/Telegram-Mac/premium/coin_edge.png new file mode 100644 index 0000000000000000000000000000000000000000..f0bf2761f183db46f90c4a82363772e2de528d34 GIT binary patch literal 332 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 Hf!qHD+!%aU literal 0 HcmV?d00001 diff --git a/Telegram-Mac/premium/diagonal_shine.png b/Telegram-Mac/premium/diagonal_shine.png new file mode 100644 index 0000000000000000000000000000000000000000..1194211fb7d5dd881f3fe6bc891947e93545e653 GIT binary patch literal 11771 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 literal 0 HcmV?d00001 diff --git a/Telegram-Mac/premium/lighterTexture.jpg b/Telegram-Mac/premium/lighterTexture.jpg new file mode 100644 index 0000000000000000000000000000000000000000..23808170f1b6e28692ca215ddcf5cd1a2502e9ae GIT binary patch literal 9269 zcmeG?3vg4{mG?=OY+14``4dx;ZDb+a5YX2$wzYM!Wf}a6r8u&JfpL>ade4%Kq^C$v zV3^%0xF#t{b_UA<0vpd5V!L>kNrBRqX0s`f^plWI4R%r>`?1@yThDHGX%f1fc4xHb z-Y3bjq1|kDc4lYlt9kcx&bjAz?>+C{`;@wonwR{feIVF}ki&r$S2xv?Qa#RIRj53pYIn+m<4Whhs-_uaeqQd{&k5H{LQ?@dk zmsNKKIG&3KxTxA}(Ko2qIy}B$1Vh##v(ap^8qHSIMzyKlYBpNy>w%AWj1pw0QV5NS zGBPMMCSI%q9Zph8oYqsCYYU}Ww1s*k*A@|#-2DTozM zwmFa}^>3({v=Fl&)1^K@Lr76jP#`N%$YctYyil&HC{ZX%Dpr-1Rg{&jQYlDE7eZrs zP8G@JMa4zR;$mfaadB}u&Wg*0E~-z%Aax0;3eXsMhteRCiV~}+)Zc@Vg{ilwO`xTO zA_)}6NsK5_oR@#fkW#}8gO^49DxKAHH|_uspHcU#}?XO2#O`v>p;)2|!0^!WEYd+h7q z`QcCh`9D-hBm!;oh)(%ZNj*`a%2b*M8a%Bjlb8<-mtzesyRO}-(|YG)gUPn;3SZpv zt7<7$Ait*m%5_lUl}e`l@r^wDv#bigUxV636;ij5LQG_;&?adHM>oFfs6D{et!{w+=4UI1ls|j!ZgV>KPpCp6sYO{CdUAq_eD} zSDu=_L2HlxmfBhBJYF$3Wo4326fS6!LHU6vT%DtqgLAJYnS~3lRns%Ij;eu2oMliX zFl^i);YVs6>o0#Lb?pXiW#2ekF{2%=o;yc3bv}2;{c;U0x(GsO%fVm2M(2(6?8Hhu zQT*Hy?b|iVi~7TzFRN{LdYon2muz!Srmo)vEqjeT%gPPuXkcKdut!VhCG~_xrw#7RG6f-#||c)-P(&4VvNQIh%jP^IW1tsN=;m)TL^_j*NbXEIMO;TH}#U$WhKGBtY(7`*Pu(#c8;3YUjfFzzI|DsmuSgOHH{LdP=q1T+*@U*BSFU$;mQ5=EmIiLOp5#*R zf3R+3LcVt#IDjY5bl1`qm(C1>Jrd|#)8bCj*LvmUFxYcbjq8%F0~?aEXJs(v6*KR+ zlKo)9Pd{)TxBf%mjE$br9^OW47YulARLl`3gW%ERocp+yPD5WEZMR|vBs))F`=>vE zG5ENh2YoK||KzuSe)@-R$RyF-uYN=Frjy{T1a}*9 zB|JFvAtaB+AVNQy;G+p1n)v;Iy5fAdm*YZ&f{ySk8%cyQ0@rcb7K-6^3F+G%9KaN| zAdmZCw7`>%IXz+4-M-xoAVHYWp)1C5ecdddh(7A=_W@FYx=;*p@ascr)QwogqXdd# zR;VzVrG^$^V+-+-v_=YVA{gR>5yBGiGD6+v*s)dc#-CyqA{}Il7MJE&{AiR9a*;UT zD=}qTl#gVL03*GzY{TV`JF<kZpZN6Rp zv?ByBA=egL8)$>y+sMVJ)eM*Lws3Q}Nz#^B*2i-(yNCB+3y6|k{;qgtR^U1&h7zldI@vyf%`&lW*cRMR zY_~5Yz`J6;mK~S_NlIUgi}nOq7+jd4!H7TOzG94UflfA#=ecl*i}=%B6d4RwAqQTR zfxCkKKrTX_L4a;qdc0P|9|}68HwjdxB5H_+!$RksLCL%w5=8;Z&GjIshhm-6hZtWa#SyObBw=3_+sOkdwzxWFck( z-wAn!25-42gx5nFLU{fL+6b>VLF?tY_?H;H94fT`H#p*p0?a$JIz=t8TH#6i&5mmX zha+6e65QtjgUI$ySm~>AQ;g@IU>R-_*OBR(4-th$10=4*peCRQO-`0xei(wQIn6ZD zy^yTxm?Dx0;rNGRvDC{YB20YQy7uuVz>)Bc7ejW&0dU!jMYinB7KFO86PL85@lepm z#g7g#s~#U<@7htD*S6wqKlxg_gWpi1U|19C82B*N@Z_;@uwq5~^Y! z8w$DE0iKR`xE-CD09TL!FCmr%xY)kdP|%<8REc1GM+OX*=QC`dCqV+PY&RR@??b*V z171ol_xjto5EsjFu1er<+mb~<7v$t3*w-?ii^7XL&gMq6!VrYMd!Pz036=N2X{tuEm1j%?#6d(;ZS5)|rabDIrGX;iI82q4P;EOpo%;L564PnD9_@h}r;EIC!ms zgVf4FYvrJ|a?n~iX#IaWXbG{c3W5|uJ@7z$gX0ZqMGPEDys%+V!x1F_bsnBLslj#$ zmH#&sYGg)6cpl1qP4DBvh5=91VA2~6=nI<$ zq8{HamREaOctba9{{3&hr%?x)W=&7M!{~^%v4LR6z8KrR&*}E<+vRIwG@HJt{=$HD zARG;|yhl9{4n^YDfo2V1ZiPAq8#HP{!tZLSU^E-dW*t!I;(H^!XFwN;ugP$*vvFT6NKR#H%n?o;3BFka+6rcbqgl51 z^;d?F@nz|vuvH+!8J~d-v0*mC$HBKI>;l3xnz^*hc3HwYizWk-=>+a|IPTveGj`!{ zRz#d{-wkH`p^=W`*S^8U#<@hy$F}bVHP>KaQLBSvf_;18_{BDB@GYv&Xx5oFx-++^ zIwRbx8U@m}80&$fnj4O3nQ>jbKUjVxo708Oa<9`Fn{+0l+t>t5t##&H?hcqZ44eY9 z#J^7!#%Jy0Vqp)M91MH>tf4>3`ZemTC#_^V19pK$bwmw^!%9cuyeHygX?wE<%+?2k zjMdxdZSeHf`E))@U!%^_$TB+b#>PG!+vIIDv8D!JW4$-i-_H3GcyKfQng3ybVY}-I zeP%v*+2L7X?FhzU5cZ}QMK}9&x)utm$waVveE8a-Srhl{W*JQy82rrk3rzRGQTY!} z5P>Yt_3?W=F}BqYzWOwiMCj%Vcp`ol!j3^>0CNm@aSE0hmK#lQISV;M4$%Quv!UsW zneeevu6SU@11la_@xY1)Ry^=W2#U41iz<2km-0$vUq!f$9Id{5n6OBK` zE9G*zLatOOl$9k#C6y1BE0yIBK3s{@!=L6$I;AQpQI)AGN=qv~`B z{99-j933ISc`utNkvRS3JqHXIAd)<>lF*XxJOVj|lxpMtF;xyFL5!6yhwK+VSX1zw z9$(3Q@~metaO8P+;Saiw9J7jcgwgD&&%L)*-9MpUbMsx_ZYA| zC3gLYo!TOvc~|zlP9hmUfBkAz{f0}s=y*&2ghk>yv3fSlkC*HaBg!u8bH~lk5ON&okmdFODJHE6dP;+Q1 zEL9#kF#V7E4HwFb$1I8?{en^Is{R}Al%0I_sm>i$>$)Xl?(;UAZT9@F!tM3;L&to* z)8kWvHV~jb$BzB=BeP{2zCE~2TNLV@9up8PCr#|w;TN%?8z1-{3{e3kfCFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259882 + 259985 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/TGUIKit/Sources/Extensions.swift b/packages/TGUIKit/Sources/Extensions.swift index 576fa9362..e34e464e9 100644 --- a/packages/TGUIKit/Sources/Extensions.swift +++ b/packages/TGUIKit/Sources/Extensions.swift @@ -2880,3 +2880,11 @@ public final class TransformImageResult { } } + +public func deg2rad(_ number: Float) -> Float { + return number * .pi / 180 +} + +public func rad2deg(_ number: Float) -> Float { + return number * 180.0 / .pi +} diff --git a/packages/TGUIKit/Sources/TextView.swift b/packages/TGUIKit/Sources/TextView.swift index 71efdbb72..cff9bb085 100644 --- a/packages/TGUIKit/Sources/TextView.swift +++ b/packages/TGUIKit/Sources/TextView.swift @@ -783,6 +783,13 @@ public final class TextViewLayout : Equatable { return blockImage.1 != nil } + public var hasBlockQuotes: Bool { + return self.attributedString.containsAttribute(attributeName: TextInputAttributes.quote) is TextViewBlockQuoteData + } + public var string: String { + return self.attributedString.string + } + public private(set) var layoutSize:NSSize = NSZeroSize public private(set) var perfectSize:NSSize = NSZeroSize public var alwaysStaticItems: Bool @@ -3324,259 +3331,3 @@ public class TextView: Control, NSViewToolTipOwner, ViewDisplayDelegate { } - - -/* - for spoiler in layout.spoilers.filter({ !$0.isRevealed }) { - if let spoilerRange = spoiler.range.intersection(line.range) { - let range = spoilerRange.intersection(layout.selectedRange.range) - - var ranges:[(NSRange, NSColor)] = [] - if let range = range { - ranges.append((NSMakeRange(spoiler.range.lowerBound, range.lowerBound - spoiler.range.lowerBound), spoiler.color)) - ranges.append((NSMakeRange(spoiler.range.upperBound, range.upperBound - spoiler.range.upperBound), spoiler.color)) - } else { - ranges.append((spoilerRange, spoiler.color)) - } - for range in ranges { - let startOffset = CTLineGetOffsetForStringIndex(line.line, range.0.lowerBound, nil); - let endOffset = CTLineGetOffsetForStringIndex(line.line, range.0.upperBound, nil); - - var ascent:CGFloat = 0 - var descent:CGFloat = 0 - var leading:CGFloat = 0 - - _ = CGFloat(CTLineGetTypographicBounds(line.line, &ascent, &descent, &leading)); - - var rect:NSRect = line.frame - - rect.size.width = endOffset - startOffset - rect.origin.x = startOffset - rect.origin.y = rect.minY - rect.height - rect.size.height += ceil(descent - leading) - - - ctx.setFillColor(range.1.cgColor) - ctx.fill(rect) - } - } - } - */ - - -/* - func calculateLayoutV2(isBigEmoji: Bool = false, lineSpacing: CGFloat? = nil, saveRTL: Bool = false) -> Void { - let blockQuoteLeftInset: CGFloat = 7.0 - let blockQuoteRightInset: CGFloat = 0.0 - let blockQuoteIconInset: CGFloat = 12.0 - - struct StringSegment { - let substring: NSAttributedString - let firstCharacterOffset: Int - let isBlockQuote: Bool - let tintColor: NSColor? - } - var stringSegments: [StringSegment] = [] - - let rawWholeString = attributedString.string as NSString - let wholeStringLength = rawWholeString.length - - var segmentCharacterOffset = 0 - while true { - var found = false - attributedString.enumerateAttribute(NSAttributedString.Key("Attribute__Blockquote"), in: NSRange(location: segmentCharacterOffset, length: wholeStringLength - segmentCharacterOffset), using: { value, effectiveRange, _ in - found = true - if segmentCharacterOffset != effectiveRange.location { - stringSegments.append(StringSegment( - substring: attributedString.attributedSubstring(from: NSRange( - location: segmentCharacterOffset, - length: effectiveRange.location - segmentCharacterOffset - )), - firstCharacterOffset: segmentCharacterOffset, - isBlockQuote: false, - tintColor: nil - )) - } - - if let value = value as? TextViewBlockQuoteData { - if effectiveRange.length != 0 { - stringSegments.append(StringSegment( - substring: attributedString.attributedSubstring(from: effectiveRange), - firstCharacterOffset: effectiveRange.location, - isBlockQuote: true, - tintColor: value.color - )) - } - } else { - stringSegments.append(StringSegment( - substring: attributedString.attributedSubstring(from: effectiveRange), - firstCharacterOffset: effectiveRange.location, - isBlockQuote: false, - tintColor: nil - )) - } - - segmentCharacterOffset = effectiveRange.location + effectiveRange.length - }) - if !found { - if segmentCharacterOffset != wholeStringLength { - stringSegments.append(StringSegment( - substring: attributedString.attributedSubstring(from: NSRange( - location: segmentCharacterOffset, - length: wholeStringLength - segmentCharacterOffset - )), - firstCharacterOffset: segmentCharacterOffset, - isBlockQuote: false, - tintColor: nil - )) - } - - break - } - } - - struct CalculatedSegment { - var lines: [TextViewLine] = [] - var tintColor: NSColor? - var isBlockQuote: Bool = false - var additionalWidth: CGFloat = 0.0 - } - - - var calculatedSegments: [CalculatedSegment] = [] - - for segment in stringSegments { - var calculatedSegment = CalculatedSegment() - calculatedSegment.isBlockQuote = segment.isBlockQuote - calculatedSegment.tintColor = segment.tintColor - - let rawSubstring = segment.substring.string as NSString - let substringLength = rawSubstring.length - let typesetter = CTTypesetterCreateWithAttributedString(segment.substring as CFAttributedString) - - var currentLineStartIndex = 0 - - var constrainedSegmentWidth = constrainedWidth - var additionalOffsetX: CGFloat = 0.0 - if segment.isBlockQuote { - constrainedSegmentWidth -= blockQuoteLeftInset + blockQuoteRightInset - additionalOffsetX += blockQuoteLeftInset - calculatedSegment.additionalWidth += blockQuoteLeftInset + blockQuoteRightInset - } - - var additionalSegmentRightInset = blockQuoteIconInset - - while true { - let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, currentLineStartIndex, constrainedSegmentWidth + additionalSegmentRightInset) - - if lineCharacterCount != 0 { - let line = CTTypesetterCreateLine(typesetter, CFRange(location: currentLineStartIndex, length: lineCharacterCount)) - var lineAscent: CGFloat = 0.0 - var lineDescent: CGFloat = 0.0 - let lineWidth = CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, nil) - - var isRTL = false - let glyphRuns = CTLineGetGlyphRuns(line) as NSArray - if glyphRuns.count != 0 { - let run = glyphRuns[0] as! CTRun - if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { - isRTL = true - } - } - var penFlush = self.penFlush - if penFlush == 0 { - if isRTL { - penFlush = 1 - } - } else if isRTL, penFlush == 1 { - penFlush = 0 - } - - calculatedSegment.lines.append(TextViewLine( - line: line, - frame: CGRect(origin: CGPoint(x: additionalOffsetX, y: 0.0), size: CGSize(width: lineWidth + additionalSegmentRightInset, height: lineAscent + lineDescent)), - range: NSRange(location: segment.firstCharacterOffset + currentLineStartIndex, length: lineCharacterCount), - lineRange: NSMakeRange(currentLineStartIndex, lineCharacterCount), - penFlush: penFlush, - isRTL: isRTL, - strikethrough: [], - embeddedItems: [] - )) - } - - additionalSegmentRightInset = 0.0 - - currentLineStartIndex += lineCharacterCount - - if currentLineStartIndex >= substringLength { - break - } - } - - calculatedSegments.append(calculatedSegment) - } - - var size = CGSize() - let isTruncated = false - - for segment in calculatedSegments { - - for line in segment.lines { - size.width = max(size.width, line.frame.origin.x + line.frame.width + segment.additionalWidth) - } - } - - var lines: [TextViewLine] = [] - var blockQuotes: [TextViewBlockQuote] = [] - for i in 0 ..< calculatedSegments.count { - let segment = calculatedSegments[i] - if i != 0 { - if segment.isBlockQuote { - size.height += 6.0 - } - } else { - if segment.isBlockQuote { - size.height += 5.0 - } - } - - let blockMinY = size.height - insets.height - var blockWidth: CGFloat = 0.0 - - - for line in segment.lines { - line.frame = CGRect(origin: CGPoint(x: line.frame.origin.x, y: size.height + line.frame.size.height), size: line.frame.size) - line.frame.size.width += max(0.0, segment.additionalWidth - 2.0) - size.height += line.frame.height - blockWidth = max(blockWidth, line.frame.origin.x + line.frame.width) - - lines.append(line) - } - - let blockMaxY = size.height - - if i != calculatedSegments.count - 1 { - if segment.isBlockQuote { - size.height += 8.0 - } - } else { - if segment.isBlockQuote { - size.height += 6.0 - } - } - - if segment.isBlockQuote, let tintColor = segment.tintColor { - blockQuotes.append(TextViewBlockQuote(frame: CGRect(origin: CGPoint(x: 0.0, y: blockMinY), size: CGSize(width: size.width, height: blockMaxY - (blockMinY) + 8.0)), tintColor: tintColor)) - } - } - - self.lines = lines - self.blockQuotes = blockQuotes - let rawTextSize = size -// size.width += insets.left + insets.right - size.height += 4.0 - self.layoutSize = size - - } - - */ diff --git a/submodules/telegram-ios b/submodules/telegram-ios index f4e351ab2..91fa64acf 160000 --- a/submodules/telegram-ios +++ b/submodules/telegram-ios @@ -1 +1 @@ -Subproject commit f4e351ab2ff87d8a83b60ec834c9bc1802b3493a +Subproject commit 91fa64acff523b55566a29e6ba3e54ea9b3a2f9b From 230f2758be8214c485885dc9b28bce72a7acc8f1 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 21 Feb 2024 10:04:17 +0000 Subject: [PATCH 20/50] - bugfixes --- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- submodules/telegram-ios | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 8360889d6..1988c7924 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259860 + 259861 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 74700d0b4..27cf7337f 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259860 + 259861 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/submodules/telegram-ios b/submodules/telegram-ios index 180e1f814..64767e6c4 160000 --- a/submodules/telegram-ios +++ b/submodules/telegram-ios @@ -1 +1 @@ -Subproject commit 180e1f8141aed2af4d2a7dc859590f96b4d09bfd +Subproject commit 64767e6c418e18a8d780d0d930046ae5c22cfd9f From 65522e7239b1723836fe47875b485cb971066d7e Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 21 Feb 2024 10:14:27 +0000 Subject: [PATCH 21/50] - bugfixes --- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- packages/TGUIKit/Sources/Window.swift | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 1988c7924..4b20e7d41 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 259861 + 259862 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 27cf7337f..ec9461c0f 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 259861 + 259862 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/TGUIKit/Sources/Window.swift b/packages/TGUIKit/Sources/Window.swift index 1684dec8f..0116f88e6 100644 --- a/packages/TGUIKit/Sources/Window.swift +++ b/packages/TGUIKit/Sources/Window.swift @@ -542,6 +542,9 @@ open class Window: NSWindow { if let event = event, let code = KeyboardKey(rawValue: event.keyCode), observer.ignoreKeys.contains(code) { continue } + if observer.object.value == nil { + continue + } if let responder = observer.handler() { if self.firstResponder != responder { let _ = self.resignFirstResponder() @@ -556,8 +559,8 @@ open class Window: NSWindow { responder.setCursorToEnd() } } - break } + break } } From 9cbc96d9c0b88e4041240b2a6c9e7c99e54d5e06 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 1 Mar 2024 11:22:08 +0400 Subject: [PATCH 22/50] - bugfixes --- Telegram.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Telegram.xcodeproj/project.pbxproj b/Telegram.xcodeproj/project.pbxproj index f40dbf7ce..fd19324c1 100644 --- a/Telegram.xcodeproj/project.pbxproj +++ b/Telegram.xcodeproj/project.pbxproj @@ -7897,7 +7897,7 @@ "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.8; + MARKETING_VERSION = 10.9; ONLY_ACTIVE_ARCH = NO; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = ( @@ -7949,7 +7949,7 @@ "$(PROJECT_DIR)/core-xprojects/openssl/build/openssl/lib", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.8; + MARKETING_VERSION = 10.9; ONLY_ACTIVE_ARCH = YES; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/macosx/libswiftAppKit.dylib"; @@ -7996,7 +7996,7 @@ "$(PROJECT_DIR)/core-xprojects/openssl/build/openssl/lib", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.8; + MARKETING_VERSION = 10.9; ONLY_ACTIVE_ARCH = NO; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/macosx/libswiftAppKit.dylib"; @@ -8113,7 +8113,7 @@ "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.8; + MARKETING_VERSION = 10.9; ONLY_ACTIVE_ARCH = YES; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = ( From 354bf29e49efc928e5e07967881b1a5968d1ceee Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 1 Mar 2024 11:23:18 +0400 Subject: [PATCH 23/50] - bugfixes --- submodules/tgcalls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/tgcalls b/submodules/tgcalls index 72cf45945..867e271bc 160000 --- a/submodules/tgcalls +++ b/submodules/tgcalls @@ -1 +1 @@ -Subproject commit 72cf4594553d368f57e617be0a929bbd7388fd7c +Subproject commit 867e271bce1d063065dc035d5e169461a20c0042 From 80f0fd27fdca7da16b5c96f7234a6d2b25eeb13e Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 1 Mar 2024 12:19:41 +0400 Subject: [PATCH 24/50] - bugfixes --- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/PremiumBoardingController.swift | 6 +++--- TelegramShare/Info.plist | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 93a721664..e8657aef4 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260309 + 260318 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/PremiumBoardingController.swift b/Telegram-Mac/PremiumBoardingController.swift index e62805909..0cb7bff8c 100644 --- a/Telegram-Mac/PremiumBoardingController.swift +++ b/Telegram-Mac/PremiumBoardingController.swift @@ -317,7 +317,7 @@ enum PremiumValue : String { NSColor(rgb: 0x3eb26d), NSColor(rgb: 0x3dbd4a), NSColor(rgb: 0x51c736)] - return [colors[index]] + return [colors[min(index, colors.count - 1)]] } func businessGradient(_ index: Int) -> [NSColor] { @@ -1220,8 +1220,8 @@ final class PremiumBoardingController : ModalViewController { - let business = context.premiumOrder.premiumValues.filter { $0.isBusiness } - let rest = context.premiumOrder.premiumValues.filter { !$0.isBusiness } + let business = context.premiumOrder.premiumValues.filter { $0.isBusiness }.uniqueElements + let rest = context.premiumOrder.premiumValues.filter { !$0.isBusiness }.uniqueElements var initialState = State(values: rest, businessValues: business, source: source, isPremium: context.isPremium, premiumConfiguration: PremiumPromoConfiguration.defaultValue, stickers: [], canMakePayment: canMakePayment, newPerks: FastSettings.premiumPerks) diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index ee0425301..6c389b7a9 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260309 + 260318 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion From 06b2fe84573aca1332ea131e4033b461a961943c Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 1 Mar 2024 14:14:32 +0400 Subject: [PATCH 25/50] - bugfixes --- Telegram-Mac/ChannelCommentsControls.swift | 3 ++- Telegram-Mac/ChatController.swift | 13 ---------- Telegram-Mac/ChatRowItem.swift | 11 -------- Telegram-Mac/ChatRowView.swift | 6 ++++- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- packages/TGUIKit/Sources/TableView.swift | 30 ++++++++++++++++------ 7 files changed, 31 insertions(+), 36 deletions(-) diff --git a/Telegram-Mac/ChannelCommentsControls.swift b/Telegram-Mac/ChannelCommentsControls.swift index 96c195a81..93fab1c23 100644 --- a/Telegram-Mac/ChannelCommentsControls.swift +++ b/Telegram-Mac/ChannelCommentsControls.swift @@ -437,6 +437,7 @@ class ChannelCommentsBubbleControl: CommentsBasicControl { super.update(data: data, size: size, animated: animated) + let (removed, inserted, updated) = mergeListsStableWithUpdates(leftList: self.peers, rightList: data.peers) for removed in removed.reversed() { @@ -499,7 +500,7 @@ class ChannelCommentsBubbleControl: CommentsBasicControl { case backward } - arrowView.isHidden = data.isLoading + arrowView.isHidden = data.isLoading || !data.bubbleMode if animated { var f = size.bounds.focus(arrowView.frame.size) diff --git a/Telegram-Mac/ChatController.swift b/Telegram-Mac/ChatController.swift index 72a9d7c58..b26856e6a 100644 --- a/Telegram-Mac/ChatController.swift +++ b/Telegram-Mac/ChatController.swift @@ -3157,19 +3157,6 @@ class ChatController: EditableViewController, Notifable, Tab media = state.editMedia } - if let customChatContents { - customChatContents.editMessage(id: state.message.id, text: inputState.inputText, media: media, entities: TextEntitiesMessageAttribute(entities: inputState.messageTextEntities()), webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: presentation.interfaceState.composeDisableUrlPreview != nil) - self.chatInteraction.beginEditingMessage(nil) - self.chatInteraction.update({ - $0.updatedInterfaceState({ - $0.withUpdatedComposeDisableUrlPreview(nil).updatedEditState({ - $0?.withUpdatedLoadingState(.none) - }) - }) - }) - return - } - if atDate == nil { self.context.account.pendingUpdateMessageManager.add(messageId: state.message.id, text: inputState.inputText, media: media, entities: TextEntitiesMessageAttribute(entities: inputState.messageTextEntities()), inlineStickers: inputState.inlineMedia, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: presentation.interfaceState.composeDisableUrlPreview != nil) diff --git a/Telegram-Mac/ChatRowItem.swift b/Telegram-Mac/ChatRowItem.swift index 21de490ea..f2b883cb6 100644 --- a/Telegram-Mac/ChatRowItem.swift +++ b/Telegram-Mac/ChatRowItem.swift @@ -423,17 +423,6 @@ class ChatRowItem: TableRowItem { return id } var id: String = super.identifier - id += "_message" - if self.topicLinkLayout != nil { - id += "_topicLinkLayout" - } - if self.forwardNameLayout != nil { - id += "_forwardNameLayout" - } - if self.replyMarkupModel != nil { - id += "_replyMarkupModel" - } - _id = id return id } diff --git a/Telegram-Mac/ChatRowView.swift b/Telegram-Mac/ChatRowView.swift index 36ba820b5..1b25b4bb0 100644 --- a/Telegram-Mac/ChatRowView.swift +++ b/Telegram-Mac/ChatRowView.swift @@ -1282,7 +1282,11 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDeleg guard let _ = item.commentsBubbleData else { return .zero } - return NSMakeRect(0, 0, item.isBubbled ? item.bubbleFrame.width : item.contentSize.width, ChatRowItem.channelCommentsBubbleHeight) + var x: CGFloat = 0 + if !item.isBubbled, let _ = forwardLine { + x += 10 + } + return NSMakeRect(x, 0, item.isBubbled ? item.bubbleFrame.width : item.contentSize.width - x, ChatRowItem.channelCommentsBubbleHeight) } func channelCommentsOverlayFrame(_ item: ChatRowItem) -> CGRect { guard let commentsData = item.commentsBubbleDataOverlay else { diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index e8657aef4..c2526f7c2 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260318 + 260348 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 6c389b7a9..306c8f544 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260318 + 260348 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/TGUIKit/Sources/TableView.swift b/packages/TGUIKit/Sources/TableView.swift index 72e39e2a4..c9551c062 100644 --- a/packages/TGUIKit/Sources/TableView.swift +++ b/packages/TGUIKit/Sources/TableView.swift @@ -947,7 +947,7 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele self.tableView.columnAutoresizingStyle = .noColumnAutoresizing // let tableColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "column")) // tableColumn.width = frame.width -// + // self.tableView.addTableColumn(tableColumn) @@ -1791,15 +1791,28 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele } } }, completionHandler: { + + CATransaction.begin() + + self.beginTableUpdates() let view = controller.resortView controller.clear() if let view = view { view.frame = self.tableView.convert(view.frame, from: view.superview) self.tableView.addSubview(view) } + self.moveItem(from: start, to: current, redraw: false, animation: .none) + self.tableView.removeRows(at: IndexSet(integer: start), withAnimation: .none) + self.tableView.insertRows(at: IndexSet(integer: current), withAnimation: .none) + self.endTableUpdates() + CATransaction.commit() + if controller.resortRange.location != NSNotFound { - controller.complete(start, current) + delay(0.2, closure: { + controller.complete(start, current) + }) } + }) @@ -1978,20 +1991,21 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele transition.updateFrame(view: view, frame: rect) NSAnimationContext.current.duration = animated ? duration : 0.0 NSAnimationContext.current.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.tableView.beginUpdates() self.tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row)) - self.tableView.endUpdates() return } } - if let _ = self.optionalItem(at: row) { - self.tableView.beginUpdates() + if let item = self.optionalItem(at: row) { + let animated = visibleRows().contains(row) && item.view != nil && animated NSAnimationContext.current.duration = animated ? duration : 0.0 NSAnimationContext.current.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row)) + self.tableView.beginUpdates() + self.tableView.removeRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none]) + self.tableView.insertRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none]) self.tableView.endUpdates() } + } fileprivate func reloadHeightItems() { @@ -3023,7 +3037,7 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele listhash[item.stableId] = item item.table = self item._index = index - reloadData(row: index, animated: animated, presentAsNew: prev.identifier != item.identifier) + reloadData(row: index, animated: animated, presentAsNew: false) } } From 07c6481be59810ad7b0e38f6c17c61bf3216bb30 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 1 Mar 2024 15:23:26 +0400 Subject: [PATCH 26/50] - bugfixes --- Telegram-Mac/AccountViewController.swift | 2 -- Telegram-Mac/ChatRowItem.swift | 3 ++- Telegram-Mac/ChatRowView.swift | 4 ++-- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- .../CallVideoLayer/Sources/MetalCallVideoView.swift | 4 ++-- packages/TGUIKit/Sources/ContainableController.swift | 10 +++++++--- 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Telegram-Mac/AccountViewController.swift b/Telegram-Mac/AccountViewController.swift index db1cce889..000c1687f 100644 --- a/Telegram-Mac/AccountViewController.swift +++ b/Telegram-Mac/AccountViewController.swift @@ -617,10 +617,8 @@ private func accountInfoEntries(peerView:PeerView, context: AccountContext, acco if !context.premiumIsBlocked { entries.append(.premium(index: index, viewType: .singleItem)) index += 1 - #if DEBUG entries.append(.business(index: index, viewType: .singleItem)) index += 1 - #endif entries.append(.giftPremium(index: index, viewType: .singleItem)) index += 1 diff --git a/Telegram-Mac/ChatRowItem.swift b/Telegram-Mac/ChatRowItem.swift index f2b883cb6..ccd0870d4 100644 --- a/Telegram-Mac/ChatRowItem.swift +++ b/Telegram-Mac/ChatRowItem.swift @@ -2731,7 +2731,8 @@ class ChatRowItem: TableRowItem { } var bubbleFrame: NSRect { - + + let nameWidth:CGFloat if hasBubble { nameWidth = (authorText?.layoutSize.width ?? 0) + statusSize + (adminBadge?.layoutSize.width ?? 0) + (boostBadge?.layoutSize.width ?? 0) diff --git a/Telegram-Mac/ChatRowView.swift b/Telegram-Mac/ChatRowView.swift index 1b25b4bb0..327193c94 100644 --- a/Telegram-Mac/ChatRowView.swift +++ b/Telegram-Mac/ChatRowView.swift @@ -1279,14 +1279,14 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDeleg } func channelCommentsBubbleFrame(_ item: ChatRowItem) -> CGRect { - guard let _ = item.commentsBubbleData else { + guard let comments = item.commentsBubbleData else { return .zero } var x: CGFloat = 0 if !item.isBubbled, let _ = forwardLine { x += 10 } - return NSMakeRect(x, 0, item.isBubbled ? item.bubbleFrame.width : item.contentSize.width - x, ChatRowItem.channelCommentsBubbleHeight) + return NSMakeRect(x, 0, item.isBubbled ? item.bubbleFrame.width : max(item.contentSize.width - x, comments.size(false).width + 10), ChatRowItem.channelCommentsBubbleHeight) } func channelCommentsOverlayFrame(_ item: ChatRowItem) -> CGRect { guard let commentsData = item.commentsBubbleDataOverlay else { diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index c2526f7c2..ec59dc85e 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260348 + 260357 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 306c8f544..3cee1baf1 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260348 + 260357 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/CallVideoLayer/Sources/MetalCallVideoView.swift b/packages/CallVideoLayer/Sources/MetalCallVideoView.swift index 3b09fbe99..e45ff50fa 100644 --- a/packages/CallVideoLayer/Sources/MetalCallVideoView.swift +++ b/packages/CallVideoLayer/Sources/MetalCallVideoView.swift @@ -125,8 +125,8 @@ public class MetalCallVideoView : LayerBackedView { let frame = NSMakeRect(size.width * 0.5, size.height * 0.5, size.width, size.height) - transition.updateFrame(layer: self.videoLayer, frame: frame) - transition.updateFrame(layer: self.videoLayer.blurredLayer, frame: frame) + transition.updateFrame(layer: self.videoLayer, frame: frame, updatePosition: true) + transition.updateFrame(layer: self.videoLayer.blurredLayer, frame: frame, updatePosition: true) self.videoLayer.blurredLayer.frame = size.bounds self.videoLayer.frame = size.bounds diff --git a/packages/TGUIKit/Sources/ContainableController.swift b/packages/TGUIKit/Sources/ContainableController.swift index 349e05cab..d641690a3 100644 --- a/packages/TGUIKit/Sources/ContainableController.swift +++ b/packages/TGUIKit/Sources/ContainableController.swift @@ -150,7 +150,7 @@ public extension ContainedViewLayoutTransition { } } } - func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil, save: Bool = true) { + func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil, save: Bool = true, updatePosition: Bool = false) { switch self { case .immediate: layer.frame = frame @@ -185,8 +185,12 @@ public extension ContainedViewLayoutTransition { animateSize(layer) } if save { - layer.frame = frame.size.bounds - layer.position = frame.origin + if updatePosition { + layer.frame = frame.size.bounds + layer.position = frame.origin + } else { + layer.frame = frame + } } } From a3396bd54f20fdbbb2a5fa2313382a11fc2e5508 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 1 Mar 2024 17:16:34 +0400 Subject: [PATCH 27/50] - bugfixes --- Telegram-Mac/BusinessLocationController.swift | 4 ++ .../BusinessQuickReplyController.swift | 3 +- Telegram-Mac/ChannelCommentsControls.swift | 2 +- .../ChatListFiltersListController.swift | 40 ++++++++++++++++++- Telegram-Mac/ChatRowItem.swift | 2 +- Telegram-Mac/ChatRowView.swift | 2 +- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/LottieLocalAnimations.swift | 12 ++++++ TelegramShare/Info.plist | 2 +- packages/TGUIKit/Sources/TableView.swift | 5 ++- submodules/telegram-ios | 2 +- 11 files changed, 66 insertions(+), 10 deletions(-) diff --git a/Telegram-Mac/BusinessLocationController.swift b/Telegram-Mac/BusinessLocationController.swift index a97907992..68cdbf3e0 100644 --- a/Telegram-Mac/BusinessLocationController.swift +++ b/Telegram-Mac/BusinessLocationController.swift @@ -379,6 +379,10 @@ func BusinessLocationController(context: AccountContext) -> InputDataController controller.validateData = { data in let state = stateValue.with { $0 } + + if state.address == nil || state.address?.isEmpty == true, state.location != nil { + return .fail(.fields([_id_input : .shake])) + } if state.initial != state.mapped { _ = context.engine.accountData.updateAccountBusinessLocation(businessLocation: state.mapped).start() showModalText(for: context.window, text: strings().businessUpdated) diff --git a/Telegram-Mac/BusinessQuickReplyController.swift b/Telegram-Mac/BusinessQuickReplyController.swift index 22b4f4045..9ddf751d3 100644 --- a/Telegram-Mac/BusinessQuickReplyController.swift +++ b/Telegram-Mac/BusinessQuickReplyController.swift @@ -470,6 +470,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { var reply: ShortcutMessageList.Item var viewType: GeneralViewType var editing: Bool + var index: Int } var tuples: [Tuple] = [] @@ -482,7 +483,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { viewType = .lastItem } } - tuples.append(.init(reply: reply, viewType: viewType, editing: state.editing)) + tuples.append(.init(reply: reply, viewType: viewType, editing: state.editing, index: i)) } for tuple in tuples { diff --git a/Telegram-Mac/ChannelCommentsControls.swift b/Telegram-Mac/ChannelCommentsControls.swift index 93fab1c23..5e27f6b3c 100644 --- a/Telegram-Mac/ChannelCommentsControls.swift +++ b/Telegram-Mac/ChannelCommentsControls.swift @@ -499,7 +499,7 @@ class ChannelCommentsBubbleControl: CommentsBasicControl { case forward case backward } - + arrowView.isHidden = data.isLoading || !data.bubbleMode if animated { diff --git a/Telegram-Mac/ChatListFiltersListController.swift b/Telegram-Mac/ChatListFiltersListController.swift index ed1c90489..c46b24e3d 100644 --- a/Telegram-Mac/ChatListFiltersListController.swift +++ b/Telegram-Mac/ChatListFiltersListController.swift @@ -92,13 +92,49 @@ private func chatListPresetEntries(filtersWithCounts: [(ChatListFilter, Int)], s viewType = .innerItem } - switch filter { case .allChats: entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_preset(filter), data: .init(name: filter.title, color: theme.colors.text, icon: FolderIcon(emoticon: .allChats).icon(for: .preview), type: .none, viewType: viewType))) index += 1 case let .filter(_, title, _, data): - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_preset(filter), data: .init(name: title, color: theme.colors.text, icon: FolderIcon(filter).icon(for: .preview), type: data.isShared ? .nextImage(sharedImage) : .nextContext(count > 0 ? "\(count)" : ""), viewType: viewType, enabled: true, description: nil, action: { + + var image: CGImage? + if let color = data.color, showTags { + + let colors = [theme.colors.peerColors(0).bottom, + theme.colors.peerColors(1).bottom, + theme.colors.peerColors(2).bottom, + theme.colors.peerColors(3).bottom, + theme.colors.peerColors(4).bottom, + theme.colors.peerColors(5).bottom, + theme.colors.peerColors(6).bottom] + + image = generateImage(NSMakeSize(20, 20), contextGenerator: { size, ctx in + ctx.clear(size.bounds) + ctx.setFillColor(colors[Int(color.rawValue) % 7].cgColor) + ctx.fillEllipse(in: size.bounds) + }) + + if data.isShared { + image = generateImage(NSMakeSize(20 + 3 + sharedImage.backingSize.width, 20), contextGenerator: { size, ctx in + ctx.clear(size.bounds) + var rect = size.bounds.focus(sharedImage.backingSize) + rect.origin.x = 0 + ctx.draw(sharedImage, in: rect) + + var rect2 = size.bounds.focus(image!.backingSize) + rect2.origin.x = rect.maxX + 3 + ctx.draw(image!, in: rect2) + }) + } + + } else if data.isShared { + image = sharedImage + } else { + image = nil + } + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_preset(filter), data: .init(name: title, color: theme.colors.text, icon: FolderIcon(filter).icon(for: .preview), type: image != nil ? .nextImage(image!) : .nextContext(count > 0 ? "\(count)" : ""), viewType: viewType, enabled: true, description: nil, action: { arguments.openPreset(filter, false) }, menuItems: { return filterContextMenuItems(filter, unreadCount: nil, context: arguments.context) diff --git a/Telegram-Mac/ChatRowItem.swift b/Telegram-Mac/ChatRowItem.swift index ccd0870d4..a15c74c71 100644 --- a/Telegram-Mac/ChatRowItem.swift +++ b/Telegram-Mac/ChatRowItem.swift @@ -2805,7 +2805,7 @@ class ChatRowItem: TableRowItem { } if let commentsBubbleData = commentsBubbleData { - rect.size.width = max(rect.size.width, commentsBubbleData.size(hasBubble, false).width) + rect.size.width = max(rect.size.width, commentsBubbleData.size(hasBubble, false).width + (isBubbled ? 0 : 10)) } return rect diff --git a/Telegram-Mac/ChatRowView.swift b/Telegram-Mac/ChatRowView.swift index 327193c94..bce5fb28a 100644 --- a/Telegram-Mac/ChatRowView.swift +++ b/Telegram-Mac/ChatRowView.swift @@ -1286,7 +1286,7 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDeleg if !item.isBubbled, let _ = forwardLine { x += 10 } - return NSMakeRect(x, 0, item.isBubbled ? item.bubbleFrame.width : max(item.contentSize.width - x, comments.size(false).width + 10), ChatRowItem.channelCommentsBubbleHeight) + return NSMakeRect(x, 0, item.isBubbled ? item.bubbleFrame.width : max(item.contentSize.width - x, comments.size(true).width + 10), ChatRowItem.channelCommentsBubbleHeight) } func channelCommentsOverlayFrame(_ item: ChatRowItem) -> CGRect { guard let commentsData = item.commentsBubbleDataOverlay else { diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index ec59dc85e..414bb2933 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260357 + 260383 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/LottieLocalAnimations.swift b/Telegram-Mac/LottieLocalAnimations.swift index 1c98571b2..58ed0fce0 100644 --- a/Telegram-Mac/LottieLocalAnimations.swift +++ b/Telegram-Mac/LottieLocalAnimations.swift @@ -439,6 +439,18 @@ enum LocalAnimatedSticker : String { playPolicy = .onceEnd case .show_status_profile: playPolicy = .onceEnd + case .business_hours: + playPolicy = .onceEnd + case .business_location: + playPolicy = .onceEnd + case .business_quick_reply: + playPolicy = .onceEnd + case .business_chatbot: + playPolicy = .onceEnd + case .business_away_message: + playPolicy = .onceEnd + case .business_greeting_message: + playPolicy = .onceEnd default: playPolicy = .loop hidePlayer = false diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 3cee1baf1..7d1d0f5ff 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260357 + 260383 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/TGUIKit/Sources/TableView.swift b/packages/TGUIKit/Sources/TableView.swift index c9551c062..1f88d0913 100644 --- a/packages/TGUIKit/Sources/TableView.swift +++ b/packages/TGUIKit/Sources/TableView.swift @@ -1794,17 +1794,20 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele CATransaction.begin() - self.beginTableUpdates() let view = controller.resortView controller.clear() if let view = view { view.frame = self.tableView.convert(view.frame, from: view.superview) self.tableView.addSubview(view) } + + self.beginTableUpdates() + self.moveItem(from: start, to: current, redraw: false, animation: .none) self.tableView.removeRows(at: IndexSet(integer: start), withAnimation: .none) self.tableView.insertRows(at: IndexSet(integer: current), withAnimation: .none) self.endTableUpdates() + CATransaction.commit() if controller.resortRange.location != NSNotFound { diff --git a/submodules/telegram-ios b/submodules/telegram-ios index d8332b0d8..0f7d073db 160000 --- a/submodules/telegram-ios +++ b/submodules/telegram-ios @@ -1 +1 @@ -Subproject commit d8332b0d836eb87958609522b15834cfbda618ec +Subproject commit 0f7d073db134aa3dac36ce353cc25f16125de6a9 From a9259c858c38459358675ddb09ed941476132e10 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 1 Mar 2024 17:53:45 +0400 Subject: [PATCH 28/50] - bugfixes --- Telegram-Mac/BusinessMessageController.swift | 20 ++++++++++--------- .../BusinessQuickReplyController.swift | 12 ++++++++++- Telegram-Mac/ChatMessageMenuItems.swift | 2 +- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Telegram-Mac/BusinessMessageController.swift b/Telegram-Mac/BusinessMessageController.swift index 5ed2a224c..c606bf8e5 100644 --- a/Telegram-Mac/BusinessMessageController.swift +++ b/Telegram-Mac/BusinessMessageController.swift @@ -758,14 +758,14 @@ func BusinessMessageController(context: AccountContext, type: BusinessMessageTyp if let awayMessage { current.shortcut = shortcuts.items.first(where: { $0.id == awayMessage.shortcutId }) } else { - current.shortcut = shortcuts.items.first(where: { $0.shortcut == "away" }) + current.shortcut = shortcuts.items.first(where: { $0.shortcut == "away" || $0.shortcut == "_away" }) current.recepient = .all } case .greetings: if let greetingMessage { current.shortcut = shortcuts.items.first(where: { $0.id == greetingMessage.shortcutId }) } else { - current.shortcut = shortcuts.items.first(where: { $0.shortcut == "greeting" }) + current.shortcut = shortcuts.items.first(where: { $0.shortcut == "greeting" || $0.shortcut == "_greeting" }) current.recepient = .all } } @@ -1025,13 +1025,15 @@ func BusinessMessageController(context: AccountContext, type: BusinessMessageTyp controller.updateDoneValue = { data in return { f in let state = stateValue.with { $0 } - let isEnabled: Bool = state.enabled -// switch type { -// case .away: -// isEnabled = state.initialAway != state.mappedAway -// case .greetings: -// isEnabled = state.initialGreeting != state.mappedGreeting -// } + var isEnabled: Bool = state.enabled + if state.shortcut != nil { + switch type { + case .away: + isEnabled = state.initialAway != state.mappedAway + case .greetings: + isEnabled = state.initialGreeting != state.mappedGreeting + } + } if isEnabled { f(.enabled(strings().navigationDone)) } else { diff --git a/Telegram-Mac/BusinessQuickReplyController.swift b/Telegram-Mac/BusinessQuickReplyController.swift index 9ddf751d3..f6a0160f8 100644 --- a/Telegram-Mac/BusinessQuickReplyController.swift +++ b/Telegram-Mac/BusinessQuickReplyController.swift @@ -453,7 +453,17 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 - let limit = arguments.context.appConfiguration.getGeneralValue("quick_replies_limit", orElse: 20) - 2 + var limit = arguments.context.appConfiguration.getGeneralValue("quick_replies_limit", orElse: 20) - 2 + + let hasAway = state.replies?.items.contains(where: { $0.shortcut == "away" || $0.shortcut == "_away" }) ?? false + let hasGreeting = state.replies?.items.contains(where: { $0.shortcut == "greeting" || $0.shortcut == "_greeting" }) ?? false + + if hasGreeting { + limit += 1 + } + if hasAway { + limit += 1 + } let count = state.replies?.items.count ?? 0 let isFull = limit <= count diff --git a/Telegram-Mac/ChatMessageMenuItems.swift b/Telegram-Mac/ChatMessageMenuItems.swift index ac6716b98..e92c5aaf4 100644 --- a/Telegram-Mac/ChatMessageMenuItems.swift +++ b/Telegram-Mac/ChatMessageMenuItems.swift @@ -600,7 +600,7 @@ func chatMenuItems(for message: Message, entry: ChatHistoryEntry?, textLayout: ( } - if !data.message.isScheduledMessage, let peer = peer, !peer.isDeleted, isNotFailed, data.peerId == data.message.id.peerId, !isService { + if !data.message.isScheduledMessage, let peer = peer, !peer.isDeleted, isNotFailed, data.peerId == data.message.id.peerId, !isService, mode.customChatContents == nil { let needUnpin = data.pinnedMessage?.others.contains(data.message.id) == true let pinAndOld: Bool diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 414bb2933..5ac5fa283 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260383 + 260387 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 7d1d0f5ff..6c18e5f6a 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260383 + 260387 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion From d0269bcdeb3429849cd516b5aafe917119715c49 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 1 Mar 2024 18:23:15 +0400 Subject: [PATCH 29/50] - bugfixes --- Telegram-Mac/AccountViewController.swift | 11 ++++++++--- Telegram-Mac/Appearance.swift | 1 + Telegram-Mac/BusinessHoursController.swift | 4 ++-- Telegram-Mac/ChatChannelSuggestView.swift | 1 - .../ChatListFiltersListController.swift | 2 +- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/PremiumCoinSceneView.swift | 2 +- Telegram-Mac/en.lproj/Localizable.strings | Bin 855498 -> 855618 bytes TelegramShare/Info.plist | 2 +- .../Sources/Localization/Localizable.swift | 6 ++++-- .../Sources/TelegramIconsTheme.swift | 16 ++++++++++++++++ tools/generate-images.swift | 1 + 12 files changed, 36 insertions(+), 12 deletions(-) diff --git a/Telegram-Mac/AccountViewController.swift b/Telegram-Mac/AccountViewController.swift index 000c1687f..c5cbf2bdb 100644 --- a/Telegram-Mac/AccountViewController.swift +++ b/Telegram-Mac/AccountViewController.swift @@ -405,8 +405,7 @@ private enum AccountInfoEntry : TableItemListNodeEntry { arguments.openPremium(false) }, border:[BorderType.Right], inset:NSEdgeInsets(left: 12, right: 12)) case let .business(_, viewType): - //TODO LANG - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "Telegram Business", icon: theme.icons.settingsBusiness, activeIcon: theme.icons.settingsBusiness, type: .next, viewType: viewType, action: { + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: strings().accountSettingsTelegramBusiness, icon: theme.icons.settingsBusiness, activeIcon: theme.icons.settingsBusinessActive, type: .next, viewType: viewType, action: { arguments.openPremium(true) }, border:[BorderType.Right], inset:NSEdgeInsets(left: 12, right: 12)) case let .giftPremium(_, viewType): @@ -878,7 +877,9 @@ class AccountViewController : TelegramGenericViewController Telegra settingsFiltersActive: { generateSettingsActiveIcon(NSImage(named: "Icon_SettingsFilters")!.precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, settingsProfile: { generateSettingsIcon(NSImage(named: "Icon_SettingsProfile")!.precomposed(flipVertical: true)) }, settingsBusiness: { generateSettingsIcon(NSImage(resource: .iconSettingsBusiness).precomposed(flipVertical: true)) }, + settingsBusinessActive: { generateSettingsActiveIcon(NSImage(resource: .iconSettingsBusiness).precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, generalCheck: { #imageLiteral(resourceName: "Icon_Check").precomposed(palette.accentIcon) }, settingsAbout: { #imageLiteral(resourceName: "Icon_SettingsAbout").precomposed(palette.accentIcon) }, settingsLogout: { #imageLiteral(resourceName: "Icon_SettingsLogout").precomposed(palette.redUI) }, diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index 88681c2ef..c028c69c0 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -22,9 +22,9 @@ private extension TimeZoneList.Item { .replacingOccurrences(of: ".0", with: "") if hoursFromGMT >= 0 { - return "\(strings().businessHoursUTC)+\(gmtText), \(title)" + return "\(title), \(strings().businessHoursUTC)+\(gmtText)" } else { - return "\(strings().businessHoursUTC)\(gmtText), \(title)" + return "\(title), \(strings().businessHoursUTC)\(gmtText)" } } } diff --git a/Telegram-Mac/ChatChannelSuggestView.swift b/Telegram-Mac/ChatChannelSuggestView.swift index eec77f143..4a73f10bf 100644 --- a/Telegram-Mac/ChatChannelSuggestView.swift +++ b/Telegram-Mac/ChatChannelSuggestView.swift @@ -286,7 +286,6 @@ final class ChatChannelSuggestView : Control { func set(item: ChatServiceItem, data: ChannelSuggestData, animated: Bool) { - //TODO LANG let layout = TextViewLayout(.initialize(string: strings().peerMediaSimilarChannels, color: item.presentation.colors.text, font: .medium(.text))) layout.measure(width: .greatestFiniteMagnitude) diff --git a/Telegram-Mac/ChatListFiltersListController.swift b/Telegram-Mac/ChatListFiltersListController.swift index c46b24e3d..ee6a886bc 100644 --- a/Telegram-Mac/ChatListFiltersListController.swift +++ b/Telegram-Mac/ChatListFiltersListController.swift @@ -111,7 +111,7 @@ private func chatListPresetEntries(filtersWithCounts: [(ChatListFilter, Int)], s image = generateImage(NSMakeSize(20, 20), contextGenerator: { size, ctx in ctx.clear(size.bounds) - ctx.setFillColor(colors[Int(color.rawValue) % 7].cgColor) + ctx.setFillColor(colors[Int(color.rawValue)].cgColor) ctx.fillEllipse(in: size.bounds) }) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 5ac5fa283..de48a71d0 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260387 + 260392 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/PremiumCoinSceneView.swift b/Telegram-Mac/PremiumCoinSceneView.swift index 7fceb802e..d2f101282 100644 --- a/Telegram-Mac/PremiumCoinSceneView.swift +++ b/Telegram-Mac/PremiumCoinSceneView.swift @@ -236,7 +236,7 @@ final class PremiumCoinSceneView: View, SCNSceneRendererDelegate, PremiumSceneVi node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer") if #available(macOS 14.0, *), let material = node.geometry?.materials.first { - material.metalness.intensity = 0.2 + material.metalness.intensity = 0.3 } } diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index a25b2f9043c65daa8fc25fba04611b42f63abf6e..f1d3964befbd63339712dbfc8535604cbadc7687 100644 GIT binary patch delta 81 zcmV-X0IvVan>50lG=PKwgaU*Egam{Iga(8Mgb0KQgbIWUgbcI|jsgWo08Ic?mq7;& nF_*?Q0v4A}+5`-@<^m1m3X^DjCYQwM0T7qa-V6|zzSRsePMI81 delta 62 zcmX>!$K=#(lZFF7M2#)7Pc1l7LFFq7OocV7M?ACFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260387 + 260392 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index b8c1b3584..29886f58e 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -141,6 +141,8 @@ public final class L10n { public static var accountSettingsStickers: String { return L10n.tr("Localizable", "AccountSettings.Stickers") } /// Stickers and Emoji public static var accountSettingsStickersAndEmoji: String { return L10n.tr("Localizable", "AccountSettings.StickersAndEmoji") } + /// Telegram Business + public static var accountSettingsTelegramBusiness: String { return L10n.tr("Localizable", "AccountSettings.TelegramBusiness") } /// Appearance public static var accountSettingsTheme: String { return L10n.tr("Localizable", "AccountSettings.Theme") } /// Update Status @@ -903,7 +905,7 @@ public final class L10n { public static var businessHoursAddSet: String { return L10n.tr("Localizable", "Business.Hours.AddSet") } /// BUSINESS HOURS public static var businessHoursBusinessHours: String { return L10n.tr("Localizable", "Business.Hours.BusinessHours") } - /// 24 hours + /// Closed public static var businessHoursClosed: String { return L10n.tr("Localizable", "Business.Hours.Closed") } /// Turn this on to show your opening hours schedule to your customers. public static var businessHoursHeader: String { return L10n.tr("Localizable", "Business.Hours.Header") } @@ -913,7 +915,7 @@ public final class L10n { public static var businessHoursTimezone: String { return L10n.tr("Localizable", "Business.Hours.Timezone") } /// Business Hours public static var businessHoursTitle: String { return L10n.tr("Localizable", "Business.Hours.Title") } - /// UTC + /// GMT public static var businessHoursUTC: String { return L10n.tr("Localizable", "Business.Hours.UTC") } /// Specify your working hours during the day. public static var businessHoursAddSetInfo: String { return L10n.tr("Localizable", "Business.Hours.AddSet.Info") } diff --git a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift index ceb416a20..b4ecce753 100644 --- a/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift +++ b/packages/TelegramIconsTheme/Sources/TelegramIconsTheme.swift @@ -3385,6 +3385,19 @@ public final class TelegramIconsTheme { return image } } + public var settingsBusinessActive: CGImage { + if let image = cached.with({ $0["settingsBusinessActive"] }) { + return image + } else { + let image = _settingsBusinessActive() + _ = cached.modify { current in + var current = current + current["settingsBusinessActive"] = image + return current + } + return image + } + } public var generalCheck: CGImage { if let image = cached.with({ $0["generalCheck"] }) { return image @@ -10588,6 +10601,7 @@ public final class TelegramIconsTheme { private let _settingsFiltersActive: ()->CGImage private let _settingsProfile: ()->CGImage private let _settingsBusiness: ()->CGImage + private let _settingsBusinessActive: ()->CGImage private let _generalCheck: ()->CGImage private let _settingsAbout: ()->CGImage private let _settingsLogout: ()->CGImage @@ -11384,6 +11398,7 @@ public final class TelegramIconsTheme { settingsFiltersActive: @escaping()->CGImage, settingsProfile: @escaping()->CGImage, settingsBusiness: @escaping()->CGImage, + settingsBusinessActive: @escaping()->CGImage, generalCheck: @escaping()->CGImage, settingsAbout: @escaping()->CGImage, settingsLogout: @escaping()->CGImage, @@ -12179,6 +12194,7 @@ public final class TelegramIconsTheme { self._settingsFiltersActive = settingsFiltersActive self._settingsProfile = settingsProfile self._settingsBusiness = settingsBusiness + self._settingsBusinessActive = settingsBusinessActive self._generalCheck = generalCheck self._settingsAbout = settingsAbout self._settingsLogout = settingsLogout diff --git a/tools/generate-images.swift b/tools/generate-images.swift index 2ed7d6d9e..c6a098d1a 100644 --- a/tools/generate-images.swift +++ b/tools/generate-images.swift @@ -262,6 +262,7 @@ func initialize() -> [String] { array.append("settingsFiltersActive") array.append("settingsProfile") array.append("settingsBusiness") + array.append("settingsBusinessActive") array.append("generalCheck") array.append("settingsAbout") array.append("settingsLogout") From b2155656eb47c97224888aae083fae025ac7b702 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Sat, 2 Mar 2024 09:28:17 +0400 Subject: [PATCH 30/50] - bugfixes --- Telegram-Mac/BusinessMessageController.swift | 5 ++--- Telegram-Mac/BusinessQuickReplyController.swift | 13 +++++++------ Telegram-Mac/Info.plist | 2 +- Telegram-Mac/InteractiveTextView.swift | 1 + TelegramShare/Info.plist | 2 +- submodules/telegram-ios | 2 +- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Telegram-Mac/BusinessMessageController.swift b/Telegram-Mac/BusinessMessageController.swift index c606bf8e5..ff5a288e2 100644 --- a/Telegram-Mac/BusinessMessageController.swift +++ b/Telegram-Mac/BusinessMessageController.swift @@ -81,7 +81,7 @@ private final class MessageRowItem : GeneralRowItem { } private final class MessageRowItemView: GeneralContainableRowView { - private let textView = TextView() + private let textView = InteractiveTextView(frame: .zero) private let titleView = TextView() private let imageView = AvatarControl(font: .avatar(15)) private let container = View() @@ -104,7 +104,6 @@ private final class MessageRowItemView: GeneralContainableRowView { imageView.layer?.cornerRadius = imageView.frame.height / 2 textView.userInteractionEnabled = false - textView.isSelectable = false titleView.userInteractionEnabled = false titleView.isSelectable = false @@ -132,7 +131,7 @@ private final class MessageRowItemView: GeneralContainableRowView { imageView.setPeer(account: item.context.account, peer: item.myPeer._asPeer()) - textView.update(item.textLayout) + textView.set(text: item.textLayout, context: item.context) titleView.update(item.titleLayout) countView.update(item.count) diff --git a/Telegram-Mac/BusinessQuickReplyController.swift b/Telegram-Mac/BusinessQuickReplyController.swift index f6a0160f8..05bab359f 100644 --- a/Telegram-Mac/BusinessQuickReplyController.swift +++ b/Telegram-Mac/BusinessQuickReplyController.swift @@ -63,7 +63,7 @@ final class QuickReplyRowItem : GeneralRowItem { } } - let texts = chatListText(account: context.account, for: reply.topMessage._asMessage()).string + let texts = chatListText(account: context.account, for: reply.topMessage._asMessage()) if reply.totalCount > 1 { _badge = .init(.initialize(string: strings().businessQuickReplyMore(reply.totalCount - 1), color: theme.colors.grayText, font: .medium(.small)), alignment: .center) @@ -77,8 +77,9 @@ final class QuickReplyRowItem : GeneralRowItem { selected_badge = nil } - attr.append(string: texts, color: theme.colors.grayText, font: .normal(.text)) - + attr.append(texts) + attr.addAttribute(.foregroundColor, value: theme.colors.grayText, range: attr.range) + let selectedAttr = attr.mutableCopy() as! NSMutableAttributedString selectedAttr.addAttribute(.foregroundColor, value: theme.colors.underSelectedColor, range: selectedAttr.range) @@ -157,7 +158,7 @@ final class QuickReplyRowItem : GeneralRowItem { } private final class QuickReplyRowItemView: GeneralContainableRowView { - private let textView = TextView() + private let textView = InteractiveTextView(frame: .zero) private let imageView = AvatarControl(font: .avatar(10)) private let container = View() private var badgeView: TextView? @@ -176,7 +177,7 @@ private final class QuickReplyRowItemView: GeneralContainableRowView { imageView.layer?.cornerRadius = imageView.frame.height / 2 textView.userInteractionEnabled = false - textView.isSelectable = false + // textView.isSelectable = false containerView.set(handler: { [weak self] _ in if let item = self?.item as? QuickReplyRowItem { @@ -245,7 +246,7 @@ private final class QuickReplyRowItemView: GeneralContainableRowView { imageView.setPeer(account: item.context.account, peer: item.context.myPeer) - textView.update(item.textLayout) + textView.set(text: item.textLayout, context: item.context) if let badge = item.badge { let current: TextView diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index de48a71d0..18aa65565 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260392 + 260397 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/InteractiveTextView.swift b/Telegram-Mac/InteractiveTextView.swift index b1e83292d..bf648ca66 100644 --- a/Telegram-Mac/InteractiveTextView.swift +++ b/Telegram-Mac/InteractiveTextView.swift @@ -30,6 +30,7 @@ final class InteractiveTextView : Control { func set(text: TextViewLayout, context: AccountContext) { self.textView.update(text) + self.setFrameSize(text.layoutSize) self.isLite = context.isLite(.emoji) self.updateInlineStickers(context: context, textLayout: text, itemViews: &inlineStickerItemViews) } diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 2a9503dce..0f7854479 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260392 + 260397 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/submodules/telegram-ios b/submodules/telegram-ios index 0f7d073db..ae998eb91 160000 --- a/submodules/telegram-ios +++ b/submodules/telegram-ios @@ -1 +1 @@ -Subproject commit 0f7d073db134aa3dac36ce353cc25f16125de6a9 +Subproject commit ae998eb91ef05dfec5c5fa2dfa3c78371992a482 From 34e78c422f2e76220831d013dc78ac481730f1f2 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Mon, 4 Mar 2024 10:33:06 +0400 Subject: [PATCH 31/50] - bugfixes --- Telegram-Mac/BusinessHoursController.swift | 16 +++++++++- .../BusinessQuickReplyController.swift | 4 +-- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/PremiumBoardingController.swift | 2 ++ Telegram-Mac/en.lproj/Localizable.strings | Bin 855618 -> 856194 bytes TelegramShare/Info.plist | 2 +- .../Sources/Localization/Localizable.swift | 30 ++++++++++++++++-- 7 files changed, 48 insertions(+), 8 deletions(-) diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index c028c69c0..8f80bf9a3 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -239,7 +239,21 @@ private struct State : Equatable { } } - return TelegramBusinessHours(timezoneId: timezone.id, weeklyTimeIntervals: mappedIntervals) + var mergedIntervals: [TelegramBusinessHours.WorkingTimeInterval] = [] + for interval in mappedIntervals { + if mergedIntervals.isEmpty { + mergedIntervals.append(interval) + } else { + if mergedIntervals[mergedIntervals.count - 1].endMinute >= interval.startMinute { + mergedIntervals[mergedIntervals.count - 1] = TelegramBusinessHours.WorkingTimeInterval(startMinute: mergedIntervals[mergedIntervals.count - 1].startMinute, endMinute: interval.endMinute) + } else { + mergedIntervals.append(interval) + } + } + } + + + return TelegramBusinessHours(timezoneId: timezone.id, weeklyTimeIntervals: mergedIntervals) } diff --git a/Telegram-Mac/BusinessQuickReplyController.swift b/Telegram-Mac/BusinessQuickReplyController.swift index 05bab359f..4d2d07a49 100644 --- a/Telegram-Mac/BusinessQuickReplyController.swift +++ b/Telegram-Mac/BusinessQuickReplyController.swift @@ -66,10 +66,10 @@ final class QuickReplyRowItem : GeneralRowItem { let texts = chatListText(account: context.account, for: reply.topMessage._asMessage()) if reply.totalCount > 1 { - _badge = .init(.initialize(string: strings().businessQuickReplyMore(reply.totalCount - 1), color: theme.colors.grayText, font: .medium(.small)), alignment: .center) + _badge = .init(.initialize(string: strings().businessQuickReplyMore1Countable(reply.totalCount - 1), color: theme.colors.grayText, font: .medium(.small)), alignment: .center) _badge?.measure(width: .greatestFiniteMagnitude) - selected_badge = .init(.initialize(string: strings().businessQuickReplyMore(reply.totalCount - 1), color: theme.colors.accentSelect, font: .medium(.small)), alignment: .center) + selected_badge = .init(.initialize(string: strings().businessQuickReplyMore1Countable(reply.totalCount - 1), color: theme.colors.accentSelect, font: .medium(.small)), alignment: .center) selected_badge?.measure(width: .greatestFiniteMagnitude) } else { diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 18aa65565..29f5c08a1 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260397 + 260405 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/PremiumBoardingController.swift b/Telegram-Mac/PremiumBoardingController.swift index 0cb7bff8c..0df1cd8d0 100644 --- a/Telegram-Mac/PremiumBoardingController.swift +++ b/Telegram-Mac/PremiumBoardingController.swift @@ -1207,6 +1207,8 @@ final class PremiumBoardingController : ModalViewController { actionsDisposable.add(context.engine.accountData.keepShortcutMessageListUpdated().startStrict()) actionsDisposable.add(context.engine.accountData.keepCachedTimeZoneListUpdated().startStrict()) + actionsDisposable.add(context.account.viewTracker.peerView(context.peerId, updateData: true).start()) + PremiumLogEvents.promo_screen_show(source).send(context: context) let close: ()->Void = { [weak self] in diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index f1d3964befbd63339712dbfc8535604cbadc7687..36a26de3e518521e62e372c7292dba026d62f90d 100644 GIT binary patch delta 198 zcmX>!$E0bY$p*7C(<@3i_?q3$w7Z>Q1Y#y2W(HywAZ7((HXvpPVh$kY1Y#~A=HBjh zhDS+jx(7S2#B_ykJYwv=4EYR245`x}E@M=k-0+2Cde%1{gUORFh)i!d$HOvx2~f@S xjBh+f(;rnciXd|jupo0kT|kzdeCe_nl3J%EMhOsCYC6|99-ZwkzVSqf0|2{9QuqJ> delta 102 zcmZo#XmV(d$p*7C%_e8sP0la^F%u9o12GE_vjQ<25VHd@2M}`tF&7YXZ#Ox^W27~G o6(6t2^aGE1#HN4v#=|jrl^oafobN#1i?=)yVDar5KY60W0i+=+YybcN diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 0f7854479..7bddacd89 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260397 + 260405 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index 29886f58e..1dd88908e 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -977,9 +977,33 @@ public final class L10n { public static var businessQuickReplyEditName: String { return L10n.tr("Localizable", "Business.QuickReply.EditName") } /// Set up shortcuts with rich text and media to respond to messages faster. public static var businessQuickReplyHeader: String { return L10n.tr("Localizable", "Business.QuickReply.Header") } + /// %d + public static func businessQuickReplyMore1Countable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.QuickReply.More1_countable", p1) + } + /// +%d MORE + public static func businessQuickReplyMore1Few(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.QuickReply.More1_few", p1) + } + /// +%d MORE + public static func businessQuickReplyMore1Many(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.QuickReply.More1_many", p1) + } + /// +%d MORE + public static func businessQuickReplyMore1One(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.QuickReply.More1_one", p1) + } + /// +%d MORE + public static func businessQuickReplyMore1Other(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.QuickReply.More1_other", p1) + } + /// +%d MORE + public static func businessQuickReplyMore1Two(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.QuickReply.More1_two", p1) + } /// +%d MORE - public static func businessQuickReplyMore(_ p1: Int) -> String { - return L10n.tr("Localizable", "Business.QuickReply.More", p1) + public static func businessQuickReplyMore1Zero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.QuickReply.More1_zero", p1) } /// Quick Replies public static var businessQuickReplyTitle: String { return L10n.tr("Localizable", "Business.QuickReply.Title") } @@ -17115,7 +17139,7 @@ public final class L10n { public static var weekdayThursday: String { return L10n.tr("Localizable", "Weekday.Thursday") } /// Tuesday public static var weekdayTuesday: String { return L10n.tr("Localizable", "Weekday.Tuesday") } - /// Weddnesday + /// Wednesday public static var weekdayWednesday: String { return L10n.tr("Localizable", "Weekday.Wednesday") } /// More trending stickers are available in\nSettings ⟶ Stickers ⟶ [Trending Stickers](trending). public static var widgedStickersInfoText: String { return L10n.tr("Localizable", "Widged.Stickers.InfoText") } From 2049fef84e0d1fb00fca24bff178942671302be7 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Mon, 4 Mar 2024 11:34:00 +0400 Subject: [PATCH 32/50] - bugfixes --- Telegram-Mac/ChatGroupedItem.swift | 2 ++ Telegram-Mac/ChatMediaItem.swift | 2 ++ Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Telegram-Mac/ChatGroupedItem.swift b/Telegram-Mac/ChatGroupedItem.swift index 06bf42cdf..5d4fbc44c 100644 --- a/Telegram-Mac/ChatGroupedItem.swift +++ b/Telegram-Mac/ChatGroupedItem.swift @@ -103,6 +103,8 @@ class ChatGroupedItem: ChatRowItem { if hasEntities { caption = ChatMessageItem.applyMessageEntities(with: attributes, for: text, message: message, context: context, fontSize: theme.fontSize, openInfo:chatInteraction.openInfo, botCommand:chatInteraction.sendPlainText, hashtag: context.bindings.globalSearch, applyProxy: chatInteraction.applyProxy, textColor: theme.chat.textColor(isIncoming, entry.renderType == .bubble), linkColor: theme.chat.linkColor(isIncoming, entry.renderType == .bubble), monospacedPre: theme.chat.monospacedPreColor(isIncoming, entry.renderType == .bubble), monospacedCode: theme.chat.monospacedCodeColor(isIncoming, entry.renderType == .bubble), openBank: chatInteraction.openBank, blockColor: theme.chat.blockColor(context.peerNameColors, message: message, isIncoming: message.isIncoming(context.account, entry.renderType == .bubble), bubbled: entry.renderType == .bubble), isDark: theme.colors.isDark, bubbled: entry.renderType == .bubble).mutableCopy() as! NSMutableAttributedString + caption.removeWhitespaceFromQuoteAttribute() + } if !hasEntities || message.flags.contains(.Failed) || message.flags.contains(.Unsent) || message.flags.contains(.Sending) { diff --git a/Telegram-Mac/ChatMediaItem.swift b/Telegram-Mac/ChatMediaItem.swift index 393753c7e..b10be03e2 100644 --- a/Telegram-Mac/ChatMediaItem.swift +++ b/Telegram-Mac/ChatMediaItem.swift @@ -473,6 +473,8 @@ class ChatMediaItem: ChatRowItem { self?.parameters?.showMedia(message) }, openBank: chatInteraction.openBank, blockColor: theme.chat.blockColor(context.peerNameColors, message: message, isIncoming: message.isIncoming(context.account, entry.renderType == .bubble), bubbled: entry.renderType == .bubble), isDark: theme.colors.isDark, bubbled: entry.renderType == .bubble).mutableCopy() as! NSMutableAttributedString + caption.removeWhitespaceFromQuoteAttribute() + var spoilers:[TextViewLayout.Spoiler] = [] for entity in entities { switch entity.type { diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 29f5c08a1..256ad2278 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260405 + 260407 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 7bddacd89..de6b6943e 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260405 + 260407 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion From a3626877129bbb3818ee98465f12328b420036d8 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Mon, 4 Mar 2024 18:29:35 +0400 Subject: [PATCH 33/50] - bugfixes --- Telegram-Mac/BusinessChatbotController.swift | 2 +- Telegram-Mac/BusinessHoursController.swift | 2 +- Telegram-Mac/BusinessLocationController.swift | 2 +- Telegram-Mac/BusinessMessageController.swift | 2 +- .../BusinessQuickReplyController.swift | 2 +- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- .../Sources/PeerCallScreen.swift | 7 ++-- .../Sources/PeerCallScreenView.swift | 32 +++++++++++++++---- 9 files changed, 36 insertions(+), 17 deletions(-) diff --git a/Telegram-Mac/BusinessChatbotController.swift b/Telegram-Mac/BusinessChatbotController.swift index 1072bf976..a67181bbd 100644 --- a/Telegram-Mac/BusinessChatbotController.swift +++ b/Telegram-Mac/BusinessChatbotController.swift @@ -489,7 +489,7 @@ func BusinessChatbotController(context: AccountContext) -> InputDataController { let initialState = State() - let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let statePromise = ValuePromise(ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((State) -> State) -> Void = { f in statePromise.set(stateValue.modify (f)) diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index 8f80bf9a3..4d043c930 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -491,7 +491,7 @@ func BusinessHoursController(context: AccountContext) -> InputDataController { let initialState = State() - let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let statePromise = ValuePromise(ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((State) -> State) -> Void = { f in statePromise.set(stateValue.modify (f)) diff --git a/Telegram-Mac/BusinessLocationController.swift b/Telegram-Mac/BusinessLocationController.swift index 68cdbf3e0..5142ecc64 100644 --- a/Telegram-Mac/BusinessLocationController.swift +++ b/Telegram-Mac/BusinessLocationController.swift @@ -297,7 +297,7 @@ func BusinessLocationController(context: AccountContext) -> InputDataController let initialState = State() - let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let statePromise = ValuePromise(ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((State) -> State) -> Void = { f in statePromise.set(stateValue.modify (f)) diff --git a/Telegram-Mac/BusinessMessageController.swift b/Telegram-Mac/BusinessMessageController.swift index ff5a288e2..c5ad96388 100644 --- a/Telegram-Mac/BusinessMessageController.swift +++ b/Telegram-Mac/BusinessMessageController.swift @@ -701,7 +701,7 @@ func BusinessMessageController(context: AccountContext, type: BusinessMessageTyp let initialState = State(type: type) - let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let statePromise = ValuePromise(ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((State) -> State) -> Void = { f in statePromise.set(stateValue.modify (f)) diff --git a/Telegram-Mac/BusinessQuickReplyController.swift b/Telegram-Mac/BusinessQuickReplyController.swift index 4d2d07a49..7046b71f8 100644 --- a/Telegram-Mac/BusinessQuickReplyController.swift +++ b/Telegram-Mac/BusinessQuickReplyController.swift @@ -518,7 +518,7 @@ func BusinessQuickReplyController(context: AccountContext) -> InputDataControlle let initialState = State() - let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let statePromise = ValuePromise(ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((State) -> State) -> Void = { f in statePromise.set(stateValue.modify (f)) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 256ad2278..765182697 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260407 + 260431 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index de6b6943e..5cd412982 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260407 + 260431 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift index b92290c44..60db593c2 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift @@ -316,15 +316,14 @@ public final class PeerCallScreen : ViewController { private func applyState(_ state: PeerCallState, arguments: Arguments, animated: Bool) { - let previousState = self.previousState var videoViewState: PeerCallVideoViewState = self.videoViewState switch state.externalState.remoteVideoState { case .active: - if videoViewState.outgoingView == nil { + if videoViewState.incomingView == nil { if let video = external.video(true) { let view = MetalVideoMakeView(videoStreamSignal: video) - + view.background = NSColor.black.withAlphaComponent(0.6) view.videoMetricsDidUpdate = { [weak self] _ in self?.applyState(state, arguments: arguments, animated: animated) } @@ -342,7 +341,7 @@ public final class PeerCallScreen : ViewController { if videoViewState.outgoingView == nil { if let video = external.video(false) { let view = MetalVideoMakeView(videoStreamSignal: video) - + view.background = NSColor.black.withAlphaComponent(0.6) view.videoMetricsDidUpdate = { [weak self] _ in self?.applyState(state, arguments: arguments, animated: animated) } diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift index f1c5bbea0..7df693383 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift @@ -122,6 +122,9 @@ final class PeerCallScreenView : Control { private var actionsViews: [PeerCallActionView] = [] private var actionsList: [PeerCallAction] = [] + + private weak var videoLink_incoming:NSView? + private weak var videoLink_outgoing:NSView? required init(frame frameRect: NSRect) { @@ -177,11 +180,11 @@ final class PeerCallScreenView : Control { statusView.updateLayout(size: statusView.frame.size, transition: transition) if let videoViewState { - if let incomingVideoView = videoViewState.incomingView { + if let incomingVideoView = videoLink_incoming { transition.updateFrame(view: incomingVideoView, frame: size.bounds) } - if let outgointVideoView = videoViewState.outgoingView { + if let outgointVideoView = videoLink_outgoing { let videoSize = videoViewState.smallVideoSize transition.updateFrame(view: outgointVideoView, frame: CGRect(origin: NSMakePoint(size.width - videoSize.width - 10, size.height - videoSize.height - 10), size: videoSize)) } @@ -237,23 +240,40 @@ final class PeerCallScreenView : Control { self.statusView.updateState(state, arguments: arguments, transition: transition) self.backgroundLayer.update(stateIndex: state.stateIndex, isEnergySavingEnabled: false, transition: transition) + var videos: [NSView] = [] if let incomingView = videoViewState.incomingView { if videoViewState.incomingInited { - addSubview(incomingView, positioned: .below, relativeTo: actions) + videos.append(incomingView) } - } else if let view = self.videoViewState?.incomingView { + videoLink_incoming = incomingView + } else if let view = self.videoLink_incoming { performSubviewRemoval(view, animated: transition.isAnimated) + self.videoLink_incoming = nil } if let outgoingView = videoViewState.outgoingView { if videoViewState.outgoingInited { - addSubview(outgoingView, positioned: .below, relativeTo: videoViewState.incomingView ?? actions) + videos.append(outgoingView) + outgoingView.layer?.cornerRadius = 10 } - } else if let view = self.videoViewState?.outgoingView { + videoLink_outgoing = outgoingView + } else if let view = self.videoLink_outgoing { performSubviewRemoval(view, animated: transition.isAnimated) + self.videoLink_outgoing = nil } + + CATransaction.begin() + for video in videos { + video.removeFromSuperview() + } + if let index = self.subviews.firstIndex(of: self.actions) { + self.subviews.insert(contentsOf: videos, at: index) + } + CATransaction.commit() + + if let tooltip = state.statusTooltip { if self.statusTooltip?.string != tooltip { if let statusTooltip = self.statusTooltip { From 9b139089a9df1170faa0ac16abde5189d8c6129e Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Mon, 4 Mar 2024 20:05:56 +0400 Subject: [PATCH 34/50] - bugfixes --- Telegram-Mac/BusinessMessageController.swift | 2 +- Telegram-Mac/en.lproj/Localizable.strings | Bin 856194 -> 856932 bytes .../Sources/Localization/Localizable.swift | 28 ++++++++++++++++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Telegram-Mac/BusinessMessageController.swift b/Telegram-Mac/BusinessMessageController.swift index c5ad96388..a11f90de9 100644 --- a/Telegram-Mac/BusinessMessageController.swift +++ b/Telegram-Mac/BusinessMessageController.swift @@ -632,7 +632,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_away_period, equatable: .init(state.awayPeriod), comparable: nil, item: { initialSize, stableId in let values: [Int32] = [7, 14, 21, 28] - return SelectSizeRowItem(initialSize, stableId: stableId, current: Int32(state.awayPeriod), sizes: values, hasMarkers: false, titles: [strings().businessGreetingMessageNoActivityDays(7), strings().businessGreetingMessageNoActivityDays(14), strings().businessGreetingMessageNoActivityDays(21), strings().businessGreetingMessageNoActivityDays(28)], viewType: .singleItem, selectAction: { selected in + return SelectSizeRowItem(initialSize, stableId: stableId, current: Int32(state.awayPeriod), sizes: values, hasMarkers: false, titles: [strings().businessGreetingMessageNoActivityDaysCountable(7), strings().businessGreetingMessageNoActivityDaysCountable(14), strings().businessGreetingMessageNoActivityDaysCountable(21), strings().businessGreetingMessageNoActivityDaysCountable(28)], viewType: .singleItem, selectAction: { selected in arguments.updateAwayPeriod(values[selected]) }) })) diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index 36a26de3e518521e62e372c7292dba026d62f90d..e5c08ae4e4327b7eb56ddec451452ce16a3b7e01 100644 GIT binary patch delta 159 zcmZo#X!2y4Nka=`3sVbo3rh=Y3tJ0&3r7oQ3s(#G79P8I(^+&GwWf2u;xU@8@Qz1r zdXF}v&~%vtJY1|U42cYt(=Rgf%1(R7V>kVjHlxIJzjr)r)3-43N`Zw~bRa@}({C~J hihzVBv+#lyAw;Iv=rBrw^dSlD(qWX@4iqv`0swA{H4Fd% delta 44 zcmaE|%%o|dNka=`3sVbo3rh=Y3tJ0&3r7oQ3s(#G79P8I(>J~6k=t(fkw;Al0B#u& A3;+NC diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index 1dd88908e..df0c6a0ac 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -891,9 +891,33 @@ public final class L10n { public static var businessGreetingMessageHeader: String { return L10n.tr("Localizable", "Business.GreetingMessage.Header") } /// Greeting Message public static var businessGreetingMessageTitle: String { return L10n.tr("Localizable", "Business.GreetingMessage.Title") } + /// %d + public static func businessGreetingMessageNoActivityDaysCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.GreetingMessage.NoActivity.Days_countable", p1) + } + /// %d Days + public static func businessGreetingMessageNoActivityDaysFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.GreetingMessage.NoActivity.Days_few", p1) + } + /// %d Days + public static func businessGreetingMessageNoActivityDaysMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.GreetingMessage.NoActivity.Days_many", p1) + } + /// %d Day + public static func businessGreetingMessageNoActivityDaysOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.GreetingMessage.NoActivity.Days_one", p1) + } + /// %d Days + public static func businessGreetingMessageNoActivityDaysOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.GreetingMessage.NoActivity.Days_other", p1) + } + /// %d Days + public static func businessGreetingMessageNoActivityDaysTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.GreetingMessage.NoActivity.Days_two", p1) + } /// %d Days - public static func businessGreetingMessageNoActivityDays(_ p1: Int) -> String { - return L10n.tr("Localizable", "Business.GreetingMessage.NoActivity.Days", p1) + public static func businessGreetingMessageNoActivityDaysZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Business.GreetingMessage.NoActivity.Days_zero", p1) } /// Choose how many days should pass after your last interaction with a recepient to send them the greeting in response to their message. public static var businessGreetingMessageNoActivityInfo: String { return L10n.tr("Localizable", "Business.GreetingMessage.NoActivity.Info") } From ca7bf7dc7cf25065856f9cd41cdebc83d5cd1a9e Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 5 Mar 2024 08:26:01 +0400 Subject: [PATCH 35/50] - bugfixes --- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- .../PrivateCallScreen/Sources/PeerCallScreen.swift | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 765182697..e810bb169 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260431 + 260434 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 5cd412982..35c536592 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260431 + 260434 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift index 60db593c2..2e7f7ecac 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift @@ -117,11 +117,11 @@ public final class PeerCallScreen : ViewController { break default: if !redial { - actions.append(makeAction(type: .video, text: "Video", resource: .icVideo, enabled: videoEnabled, action: { - + actions.append(makeAction(type: .video, text: "Video", resource: .icVideo, active: external.videoState == .active(true), enabled: videoEnabled, action: { [weak self] in + self?.external.toggleCamera() })) - actions.append(makeAction(type: .video, text: "Screen", resource: .icScreen, enabled: videoEnabled, action: { - + actions.append(makeAction(type: .video, text: "Screen", resource: .icScreen, active: external.videoState == .active(true) && external.isScreenCapture, enabled: videoEnabled, action: { + self?.external.toggleScreencast() })) } } @@ -323,7 +323,7 @@ public final class PeerCallScreen : ViewController { if videoViewState.incomingView == nil { if let video = external.video(true) { let view = MetalVideoMakeView(videoStreamSignal: video) - view.background = NSColor.black.withAlphaComponent(0.6) + view.background = NSColor.black.withAlphaComponent(0.7) view.videoMetricsDidUpdate = { [weak self] _ in self?.applyState(state, arguments: arguments, animated: animated) } @@ -341,7 +341,7 @@ public final class PeerCallScreen : ViewController { if videoViewState.outgoingView == nil { if let video = external.video(false) { let view = MetalVideoMakeView(videoStreamSignal: video) - view.background = NSColor.black.withAlphaComponent(0.6) + view.background = NSColor.black.withAlphaComponent(0.7) view.videoMetricsDidUpdate = { [weak self] _ in self?.applyState(state, arguments: arguments, animated: animated) } From ea10229b4807a9338ac41f1020b838573bb43545 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 5 Mar 2024 08:59:00 +0400 Subject: [PATCH 36/50] - bugfixes --- Telegram-Mac/BusinessHoursController.swift | 46 ++++++++++++---------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index 4d043c930..e0937addf 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -125,14 +125,14 @@ private struct State : Equatable { } - enum Day : Int32 { - case monday - case tuesday - case wednesday - case thrusday - case friday - case saturday - case sunday + enum Day : Int { + case monday = 0 + case tuesday = 1 + case wednesday = 2 + case thursday = 3 + case friday = 4 + case saturday = 5 + case sunday = 6 var title: String { switch self { @@ -142,7 +142,7 @@ private struct State : Equatable { return strings().weekdayTuesday case .wednesday: return strings().weekdayWednesday - case .thrusday: + case .thursday: return strings().weekdayThursday case .friday: return strings().weekdayFriday @@ -154,7 +154,7 @@ private struct State : Equatable { } static var all: [Day] { - return [.monday, .tuesday, .wednesday, .thrusday, .friday, .saturday, .sunday] + return [.monday, .tuesday, .wednesday, .thursday, .friday, .saturday, .sunday] } } struct Hours: Equatable { @@ -208,7 +208,9 @@ private struct State : Equatable { var filledMinutes = IndexSet() for i in 0 ..< 7 { - let today: State.Day = .init(rawValue: Int32(i))! + guard let today: State.Day = .init(rawValue: i) else { + return nil + } let dayStartMinute = i * 24 * 60 guard var effectiveRanges = self.data[today]?.list else { @@ -244,8 +246,9 @@ private struct State : Equatable { if mergedIntervals.isEmpty { mergedIntervals.append(interval) } else { - if mergedIntervals[mergedIntervals.count - 1].endMinute >= interval.startMinute { - mergedIntervals[mergedIntervals.count - 1] = TelegramBusinessHours.WorkingTimeInterval(startMinute: mergedIntervals[mergedIntervals.count - 1].startMinute, endMinute: interval.endMinute) + let index = mergedIntervals.count - 1 + if mergedIntervals[index].endMinute >= interval.startMinute { + mergedIntervals[index] = TelegramBusinessHours.WorkingTimeInterval(startMinute: mergedIntervals[index].startMinute, endMinute: interval.endMinute) } else { mergedIntervals.append(interval) } @@ -522,14 +525,15 @@ func BusinessHoursController(context: AccountContext) -> InputDataController { if let hours = hours { let weekDays = hours.splitIntoWeekDays() for (i, day) in weekDays.enumerated() { - let today = State.Day(rawValue: Int32(i))! - switch day { - case let .intervals(intervals): - current.data[today] = .init(list: intervals.map { .init(from: $0.startMinute, to: $0.endMinute, uniqueId: arc4random64()) }) - case .closed: - current.data.removeValue(forKey: today) - case .open: - current.data[today] = .init() + if let today = State.Day(rawValue: i) { + switch day { + case let .intervals(intervals): + current.data[today] = .init(list: intervals.map { .init(from: $0.startMinute, to: $0.endMinute, uniqueId: arc4random64()) }) + case .closed: + current.data.removeValue(forKey: today) + case .open: + current.data[today] = .init() + } } } } else { From b7c41134f53b2f1284183c2b23617db153fd6a26 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 5 Mar 2024 10:32:17 +0400 Subject: [PATCH 37/50] - bugfixes --- Telegram-Mac/BusinessHoursController.swift | 23 +++++--- Telegram-Mac/CallScreenController.swift | 54 ++++++++++++++++++- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- .../Sources/PeerCallArguments.swift | 6 +-- .../Sources/PeerCallScreen.swift | 20 ++++--- 6 files changed, 86 insertions(+), 21 deletions(-) diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index e0937addf..aeaad2cf9 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -13,6 +13,21 @@ import Cocoa import TGUIKit import SwiftSignalKit +private func wrappedMinuteRange(range: Range, dayIndexOffset: Int = 0) -> IndexSet { + let mappedRange = (range.lowerBound + dayIndexOffset * 24 * 60) ..< (range.upperBound + dayIndexOffset * 24 * 60) + + var result = IndexSet() + if mappedRange.upperBound > 7 * 24 * 60 { + if mappedRange.lowerBound < 7 * 24 * 60 { + result.insert(integersIn: mappedRange.lowerBound ..< 7 * 24 * 60) + } + result.insert(integersIn: 0 ..< (mappedRange.upperBound - 7 * 24 * 60)) + } else { + result.insert(integersIn: mappedRange) + } + return result +} + private extension TimeZoneList.Item { var text: String { @@ -221,13 +236,7 @@ private struct State : Equatable { for range in effectiveRanges { let minuteRange: Range = (dayStartMinute + range.from) ..< (dayStartMinute + range.to) - var wrappedMinutes = IndexSet() - if minuteRange.upperBound > 7 * 24 * 60 { - wrappedMinutes.insert(integersIn: minuteRange.lowerBound ..< 7 * 24 * 60) - wrappedMinutes.insert(integersIn: 0 ..< (7 * 24 * 60 - minuteRange.upperBound)) - } else { - wrappedMinutes.insert(integersIn: minuteRange) - } + let wrappedMinutes = wrappedMinuteRange(range: minuteRange) if !filledMinutes.intersection(wrappedMinutes).isEmpty { throw ValidationError.intersectingRanges diff --git a/Telegram-Mac/CallScreenController.swift b/Telegram-Mac/CallScreenController.swift index e7a855146..80e6f50ed 100644 --- a/Telegram-Mac/CallScreenController.swift +++ b/Telegram-Mac/CallScreenController.swift @@ -139,6 +139,8 @@ func callScreen(_ context: AccountContext, _ result:PCallResult) { switch result { case let .samePeer(session), let .success(session): + var getScreen:(()->Window?)? = nil + let arguments: PeerCallArguments = PeerCallArguments(engine: context.engine, peerId: session.peerId, makeAvatar: { view, peer in let control = view as? AvatarControl ?? AvatarControl(font: .avatar(17)) control.setFrameSize(NSMakeSize(120, 120)) @@ -147,10 +149,54 @@ func callScreen(_ context: AccountContext, _ result:PCallResult) { return control }, toggleMute: { [weak session] in session?.toggleMute() - }, toggleCamera: { + }, toggleCamera: { [weak session] callState in + + guard let screen = getScreen?() else { + return + } + + switch callState.videoState { + case let .active(available), let .paused(available), let .inactive(available): + if available { + if callState.isOutgoingVideoPaused || callState.isScreenCapture || callState.videoState == .inactive(available) { + session?.requestVideo() + } else { + session?.disableVideo() + } + } else { + verifyAlert_button(for: screen, information: strings().callCameraError, ok: strings().modalOK, cancel: "", option: strings().requestAccesErrorConirmSettings, successHandler: { result in + switch result { + case .thrid: + openSystemSettings(.camera) + default: + break + } + }, presentation: darkAppearance) + } + default: + break + } - }, toggleScreencast: { + }, toggleScreencast: { [weak session] callState in + guard let screen = getScreen?() else { + return + } + let result = session?.toggleScreenCapture() + + if let result = result { + switch result { + case .permission: + verifyAlert_button(for: screen, information: strings().callScreenError, ok: strings().modalOK, cancel: "", option: strings().requestAccesErrorConirmSettings, successHandler: { result in + switch result { + case .thrid: + openSystemSettings(.sharing) + default: + break + } + }, presentation: darkAppearance) + } + } }, endcall: { [weak session] state in switch state.state { @@ -201,6 +247,10 @@ func callScreen(_ context: AccountContext, _ result:PCallResult) { } context.sharedContext.peerCall = screen + getScreen = { [weak screen] in + return screen?.window + } + default: break } diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index e810bb169..da2673782 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260434 + 260440 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 35c536592..483732c2a 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260434 + 260440 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/PrivateCallScreen/Sources/PeerCallArguments.swift b/packages/PrivateCallScreen/Sources/PeerCallArguments.swift index cbb014622..cfb56f7cd 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallArguments.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallArguments.swift @@ -28,13 +28,13 @@ public final class PeerCallArguments { let engine: TelegramEngine let makeAvatar:(NSView?, Peer?)->NSView let toggleMute:()->Void - let toggleCamera:()->Void - let toggleScreencast:()->Void + let toggleCamera:(ExternalPeerCallState)->Void + let toggleScreencast:(ExternalPeerCallState)->Void let endcall:(ExternalPeerCallState)->Void let recall:()->Void let acceptcall:()->Void let video:(Bool)->Signal? - public init(engine: TelegramEngine, peerId: PeerId, makeAvatar: @escaping (NSView?, Peer?) -> NSView, toggleMute:@escaping()->Void, toggleCamera:@escaping()->Void, toggleScreencast:@escaping()->Void, endcall:@escaping(ExternalPeerCallState)->Void, recall:@escaping()->Void, acceptcall:@escaping()->Void, video:@escaping(Bool)->Signal?) { + public init(engine: TelegramEngine, peerId: PeerId, makeAvatar: @escaping (NSView?, Peer?) -> NSView, toggleMute:@escaping()->Void, toggleCamera:@escaping(ExternalPeerCallState)->Void, toggleScreencast:@escaping(ExternalPeerCallState)->Void, endcall:@escaping(ExternalPeerCallState)->Void, recall:@escaping()->Void, acceptcall:@escaping()->Void, video:@escaping(Bool)->Signal?) { self.engine = engine self.peerId = peerId self.makeAvatar = makeAvatar diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift index 2e7f7ecac..e0882f570 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift @@ -117,11 +117,11 @@ public final class PeerCallScreen : ViewController { break default: if !redial { - actions.append(makeAction(type: .video, text: "Video", resource: .icVideo, active: external.videoState == .active(true), enabled: videoEnabled, action: { [weak self] in - self?.external.toggleCamera() + actions.append(makeAction(type: .video, text: "Video", resource: .icVideo, active: external.videoState == .active(true) && !external.isScreenCapture, enabled: videoEnabled, action: { [weak self] in + self?.external.toggleCamera(external) })) actions.append(makeAction(type: .video, text: "Screen", resource: .icScreen, active: external.videoState == .active(true) && external.isScreenCapture, enabled: videoEnabled, action: { - self?.external.toggleScreencast() + self?.external.toggleScreencast(external) })) } } @@ -323,9 +323,12 @@ public final class PeerCallScreen : ViewController { if videoViewState.incomingView == nil { if let video = external.video(true) { let view = MetalVideoMakeView(videoStreamSignal: video) - view.background = NSColor.black.withAlphaComponent(0.7) + view.background = NSColor.black.withAlphaComponent(0.9) view.videoMetricsDidUpdate = { [weak self] _ in - self?.applyState(state, arguments: arguments, animated: animated) + guard let self else { + return + } + self.applyState(self.stateValue.with { $0 }, arguments: arguments, animated: animated) } videoViewState.incomingView = view } else { @@ -341,9 +344,12 @@ public final class PeerCallScreen : ViewController { if videoViewState.outgoingView == nil { if let video = external.video(false) { let view = MetalVideoMakeView(videoStreamSignal: video) - view.background = NSColor.black.withAlphaComponent(0.7) + view.background = NSColor.black.withAlphaComponent(0.9) view.videoMetricsDidUpdate = { [weak self] _ in - self?.applyState(state, arguments: arguments, animated: animated) + guard let self else { + return + } + self.applyState(self.stateValue.with { $0 }, arguments: arguments, animated: animated) } videoViewState.outgoingView = view } else { From bce4a6c5e048b525cfe138bcd4dbaf9b413f4570 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 5 Mar 2024 19:00:57 +0400 Subject: [PATCH 38/50] - bugfixes --- Telegram-Mac/AddReactionManager.swift | 2 +- Telegram-Mac/Auth_Signup.swift | 2 +- ...aticBusinessMessageSetupChatContents.swift | 6 + Telegram-Mac/AvatarContentView.swift | 2 +- Telegram-Mac/BusinessHoursController.swift | 2 +- Telegram-Mac/BusinessLocationController.swift | 18 +- Telegram-Mac/BusinessMessageController.swift | 17 +- .../BusinessQuickReplyController.swift | 10 +- Telegram-Mac/ChannelCommentsControls.swift | 6 +- Telegram-Mac/ChatController.swift | 3 +- Telegram-Mac/ChatFileContentView.swift | 2 +- Telegram-Mac/ChatGroupedItem.swift | 4 +- Telegram-Mac/ChatHeaderController.swift | 2 +- Telegram-Mac/ChatInputAttachView.swift | 2 +- Telegram-Mac/ChatInputView.swift | 2 +- Telegram-Mac/ChatInteractiveContentView.swift | 2 +- .../ChatInterfaceStateContextQueries.swift | 7 +- Telegram-Mac/ChatListController.swift | 2 +- .../ChatListSystemDeprecatedItem.swift | 2 +- Telegram-Mac/ChatMediaItem.swift | 4 +- Telegram-Mac/ChatMessageView.swift | 4 +- Telegram-Mac/ChatRowView.swift | 28 +- Telegram-Mac/ChatSelectText.swift | 4 +- .../ClosureInviteLinkController.swift | 131 ++++--- Telegram-Mac/DatePickerRowItem.swift | 2 +- Telegram-Mac/DesktopCapturePreviewItem.swift | 4 +- Telegram-Mac/EmojiesController.swift | 2 +- Telegram-Mac/EmojiesPackItem.swift | 4 +- .../EntertainmentViewController.swift | 24 +- Telegram-Mac/GalleryViewer.swift | 18 +- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/InlineAudioPlayerView.swift | 2 +- Telegram-Mac/InputSwapSuggestionsPanel.swift | 4 +- Telegram-Mac/LocationModalController.swift | 1 + Telegram-Mac/MediaGroupPreviewRowItem.swift | 4 +- Telegram-Mac/MediaPreviewRowItem.swift | 4 +- Telegram-Mac/PCallSession.swift | 6 +- Telegram-Mac/PIPVideoWindow.swift | 34 +- Telegram-Mac/PeerInfoBusinessItems.swift | 7 +- Telegram-Mac/PeerPhotosMonthItem.swift | 14 +- Telegram-Mac/PreviewSenderController.swift | 4 +- Telegram-Mac/RecentPeerRowItem.swift | 10 +- Telegram-Mac/ShortPeerRowView.swift | 8 +- Telegram-Mac/SmartThemePreviewRowItem.swift | 4 +- Telegram-Mac/StatisticRowItem.swift | 4 +- Telegram-Mac/StickerPackTrendingItem.swift | 4 +- Telegram-Mac/StickerSetTableRowItem.swift | 2 +- Telegram-Mac/StorageUsageMediaCells.swift | 14 +- Telegram-Mac/StoryInputView.swift | 2 +- Telegram-Mac/StoryMonthRowItem.swift | 14 +- Telegram-Mac/UIInputView.swift | 2 +- Telegram-Mac/VideoStickerContentView.swift | 2 +- Telegram-Mac/WPArticleContentView.swift | 4 +- Telegram-Mac/WPMediaContentView.swift | 2 +- Telegram-Mac/en.lproj/Localizable.strings | Bin 856932 -> 857102 bytes TelegramShare/Info.plist | 2 +- .../Sources/MetalCallVideoView.swift | 6 +- .../Sources/Localization/Localizable.swift | 2 + .../Sources/PeerCallScreen.swift | 2 - .../Sources/PeerCallScreenView.swift | 327 ++++++++++++++++-- .../TGUIKit/Sources/AppMenuController.swift | 4 +- packages/TGUIKit/Sources/AppMenuRowItem.swift | 4 +- packages/TGUIKit/Sources/Control.swift | 4 +- .../TGUIKit/Sources/NavigationModalView.swift | 8 +- packages/TGUIKit/Sources/Popover.swift | 2 +- .../TGUIKit/Sources/SPopoverRowItem.swift | 2 +- packages/TGUIKit/Sources/SearchView.swift | 24 +- packages/TGUIKit/Sources/ShadowView.swift | 1 + packages/TGUIKit/Sources/TableRowView.swift | 8 +- packages/TGUIKit/Sources/TableView.swift | 27 +- packages/TGUIKit/Sources/TextView.swift | 4 +- packages/TGUIKit/Sources/View.swift | 2 +- 72 files changed, 613 insertions(+), 288 deletions(-) diff --git a/Telegram-Mac/AddReactionManager.swift b/Telegram-Mac/AddReactionManager.swift index 2de89bd6e..9ff8e35cf 100644 --- a/Telegram-Mac/AddReactionManager.swift +++ b/Telegram-Mac/AddReactionManager.swift @@ -1219,7 +1219,7 @@ final class AddReactionManager : NSObject, Notifable { if isRemoving { self.nextResponder?.scrollWheel(with: event) } - if let window = kitWindow, window.inLiveSwiping { + if let window = _window, window.inLiveSwiping { return } if let superview = superview as? ChatControllerView { diff --git a/Telegram-Mac/Auth_Signup.swift b/Telegram-Mac/Auth_Signup.swift index d945e403c..4b112b22f 100644 --- a/Telegram-Mac/Auth_Signup.swift +++ b/Telegram-Mac/Auth_Signup.swift @@ -47,7 +47,7 @@ final class Auth_SignupHeader : View { let menu = ContextMenu() menu.addItem(ContextMenuItem(strings().loginNewRegisterSelect, handler: { [weak self] in - guard let window = self?.kitWindow else { + guard let window = self?._window else { return } filePanel(with: photoExts, allowMultiple: false, canChooseDirectories: false, for: window, completion: { paths in diff --git a/Telegram-Mac/AutomaticBusinessMessageSetupChatContents.swift b/Telegram-Mac/AutomaticBusinessMessageSetupChatContents.swift index 1bc0a99a8..3348061da 100644 --- a/Telegram-Mac/AutomaticBusinessMessageSetupChatContents.swift +++ b/Telegram-Mac/AutomaticBusinessMessageSetupChatContents.swift @@ -72,6 +72,12 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco self.nextUpdateIsHoleFill = false self.sourceHistoryView = view + + if !view.entries.contains(where: { $0.message.id.namespace == Namespaces.Message.QuickReplyCloud }) { + self.shortcutId = nil + } + + self.updateHistoryView(updateType: nextUpdateIsHoleFill ? .FillHole : .Generic) }) } diff --git a/Telegram-Mac/AvatarContentView.swift b/Telegram-Mac/AvatarContentView.swift index 245729361..d9556cab4 100644 --- a/Telegram-Mac/AvatarContentView.swift +++ b/Telegram-Mac/AvatarContentView.swift @@ -184,7 +184,7 @@ final class AvatarContentLayer: SimpleLayer { let frameSize = self.frame.size - let signal: Signal<(CGImage, CGImage)?, NoError> = peerAvatarImage(account: context.account, photo: .peer(peer, peer.smallProfileImage, peer.nameColor, peer.displayLetters, nil), displayDimensions: size, scale: System.backingScale, font: .avatar(size.height / 3 + 3), genCap: true, synchronousLoad: synchronousLoad) |> deliverOn(prepareQueue) |> map { image in + let signal: Signal<(CGImage, CGImage)?, NoError> = peerAvatarImage(account: context.account, photo: .peer(peer, peer.smallProfileImage, peer.nameColor, peer.displayLetters, nil), displayDimensions: size, scale: System.backingScale, font: .avatar(size.height / 3 + 3), genCap: true, synchronousLoad: synchronousLoad) |> map { image in if let image = image.0 { let clipImage = generateImage(frameSize, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index aeaad2cf9..e72d4dcbd 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -228,7 +228,7 @@ private struct State : Equatable { } let dayStartMinute = i * 24 * 60 - guard var effectiveRanges = self.data[today]?.list else { + guard let effectiveRanges = self.data[today]?.list else { continue } diff --git a/Telegram-Mac/BusinessLocationController.swift b/Telegram-Mac/BusinessLocationController.swift index 5142ecc64..106eff241 100644 --- a/Telegram-Mac/BusinessLocationController.swift +++ b/Telegram-Mac/BusinessLocationController.swift @@ -123,8 +123,8 @@ private final class MapRowItemView : GeneralContainableRowView, MKMapViewDelegat override func layout() { super.layout() - mapView.frame = bounds - overlay.frame = bounds + mapView.frame = containerView.bounds + overlay.frame = containerView.bounds } @@ -142,7 +142,7 @@ private final class MapRowItemView : GeneralContainableRowView, MKMapViewDelegat location.longitude = userLocation.longitude region.span = span region.center = location - mapView.setRegion(region, animated: true) + mapView.setRegion(region, animated: false) } func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { @@ -182,6 +182,7 @@ private final class MapRowItemView : GeneralContainableRowView, MKMapViewDelegat let previousItem = self.item super.set(item: item, animated: animated) + layout() guard let item = item as? MapRowItem else { return @@ -189,14 +190,12 @@ private final class MapRowItemView : GeneralContainableRowView, MKMapViewDelegat mapView.appearance = theme.appearance - focusVenue() - - if let previousItem = previousItem as? MapRowItem { - mapView.removeAnnotation(previousItem.location) - } + mapView.addAnnotation(item.location) + focusVenue() + } } @@ -265,7 +264,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { sectionId += 1 - entries.append(.input(sectionId: sectionId, index: 0, value: .string(state.address), error: nil, identifier: _id_input, mode: .plain, data: .init(viewType: .singleItem), placeholder: nil, inputPlaceholder: strings().businessLocationEnterAddress, filter: { $0 }, limit: 256)) + entries.append(.input(sectionId: sectionId, index: 0, value: .string(state.address), error: nil, identifier: _id_input, mode: .plain, data: .init(viewType: .singleItem), placeholder: nil, inputPlaceholder: strings().businessLocationEnterAddress, filter: { $0 }, limit: 96)) entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 @@ -358,6 +357,7 @@ func BusinessLocationController(context: AccountContext) -> InputDataController updateState { current in var current = current current.location = nil + current.address = nil return current } }) diff --git a/Telegram-Mac/BusinessMessageController.swift b/Telegram-Mac/BusinessMessageController.swift index a11f90de9..3ff444612 100644 --- a/Telegram-Mac/BusinessMessageController.swift +++ b/Telegram-Mac/BusinessMessageController.swift @@ -499,11 +499,11 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { let fromString = stringForMediumDate(timestamp: Int32(from.timeIntervalSince1970)) let toString = stringForMediumDate(timestamp: Int32(to.timeIntervalSince1970)) - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_start_time, data: .init(name: strings().businessMessageScheduleCustomStartTime, color: theme.colors.text, type: .nextContext(fromString), viewType: .firstItem, action: { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_start_time, data: .init(name: strings().businessMessageScheduleCustomStartTime, color: theme.colors.text, type: .nextContext(fromString), viewType: .firstItem, justUpdate: arc4random64(), action: { arguments.selectScheduleStart(from, to) }))) - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_end_time, data: .init(name: strings().businessMessageScheduleCustomEndTime, color: theme.colors.text, type: .nextContext(toString), viewType: .lastItem, action: { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_end_time, data: .init(name: strings().businessMessageScheduleCustomEndTime, color: theme.colors.text, type: .nextContext(toString), viewType: .lastItem, justUpdate: arc4random64(), action: { arguments.selectScheduleEnd(from, to) }))) default: @@ -893,6 +893,7 @@ func BusinessMessageController(context: AccountContext, type: BusinessMessageTyp updateState { current in var current = current current.shortcut = nil + current.enabled = false return current } } @@ -912,7 +913,11 @@ func BusinessMessageController(context: AccountContext, type: BusinessMessageTyp showModal(with: DateSelectorModalController(context: context, defaultDate: from, mode: .date(title: strings().businessScheduleStart, doneTitle: strings().modalDone), selectedAt: { updated in updateState { current in var current = current - current.schedule = .custom(from: updated, to: to) + if updated.timeIntervalSince1970 > to.timeIntervalSince1970 { + current.schedule = .custom(from: updated, to: Date(timeIntervalSince1970: updated.timeIntervalSince1970 + 1 * 24 * 60 * 60)) + } else { + current.schedule = .custom(from: updated, to: to) + } return current } }), for: context.window) @@ -920,7 +925,11 @@ func BusinessMessageController(context: AccountContext, type: BusinessMessageTyp showModal(with: DateSelectorModalController(context: context, defaultDate: to, mode: .date(title: strings().businessScheduleEnd, doneTitle: strings().modalDone), selectedAt: { updated in updateState { current in var current = current - current.schedule = .custom(from: from, to: updated) + if from.timeIntervalSince1970 > updated.timeIntervalSince1970 { + current.schedule = .custom(from: Date(timeIntervalSince1970: max(Date().timeIntervalSince1970, updated.timeIntervalSince1970 - 1 * 24 * 60 * 60)), to: updated) + } else { + current.schedule = .custom(from: from, to: updated) + } return current } }), for: context.window) diff --git a/Telegram-Mac/BusinessQuickReplyController.swift b/Telegram-Mac/BusinessQuickReplyController.swift index 7046b71f8..2b7902e94 100644 --- a/Telegram-Mac/BusinessQuickReplyController.swift +++ b/Telegram-Mac/BusinessQuickReplyController.swift @@ -77,8 +77,10 @@ final class QuickReplyRowItem : GeneralRowItem { selected_badge = nil } + + let prevRange = attr.range attr.append(texts) - attr.addAttribute(.foregroundColor, value: theme.colors.grayText, range: attr.range) + // attr.addAttribute(.foregroundColor, value: theme.colors.grayText, range: NSMakeRange(prevRange.location, texts.length)) let selectedAttr = attr.mutableCopy() as! NSMutableAttributedString selectedAttr.addAttribute(.foregroundColor, value: theme.colors.underSelectedColor, range: selectedAttr.range) @@ -242,7 +244,7 @@ private final class QuickReplyRowItemView: GeneralContainableRowView { return } - containerView.userInteractionEnabled = item.viewType != .legacy + containerView.userInteractionEnabled = item.viewType != .legacy && !item.editing imageView.setPeer(account: item.context.account, peer: item.context.myPeer) @@ -544,7 +546,9 @@ func BusinessQuickReplyController(context: AccountContext) -> InputDataControlle }, edit: { reply in }, remove: { reply in - context.engine.accountData.deleteMessageShortcuts(ids: [reply.id]) + verifyAlert(for: context.window, information: strings().businessQuickReplyConfirmDelete, ok: strings().modalDelete, successHandler: { _ in + context.engine.accountData.deleteMessageShortcuts(ids: [reply.id]) + }) }, editName: { reply in showModal(with: BusinessAddQuickReply(context: context, actionsDisposable: actionsDisposable, stateSignal: statePromise.get(), stateValue: stateValue, updateState: updateState, reply: reply), for: context.window) }, open: { reply in diff --git a/Telegram-Mac/ChannelCommentsControls.swift b/Telegram-Mac/ChannelCommentsControls.swift index 5e27f6b3c..125ec9322 100644 --- a/Telegram-Mac/ChannelCommentsControls.swift +++ b/Telegram-Mac/ChannelCommentsControls.swift @@ -435,8 +435,8 @@ class ChannelCommentsBubbleControl: CommentsBasicControl { let previousLastTextPosition = lastTextPosition super.update(data: data, size: size, animated: animated) - - + + CATransaction.begin() let (removed, inserted, updated) = mergeListsStableWithUpdates(leftList: self.peers, rightList: data.peers) @@ -494,6 +494,8 @@ class ChannelCommentsBubbleControl: CommentsBasicControl { self.peers = data.peers + + CATransaction.commit() enum NumericAnimation { case forward diff --git a/Telegram-Mac/ChatController.swift b/Telegram-Mac/ChatController.swift index b26856e6a..51e599b29 100644 --- a/Telegram-Mac/ChatController.swift +++ b/Telegram-Mac/ChatController.swift @@ -6756,7 +6756,8 @@ class ChatController: EditableViewController, Notifable, Tab } } chatInteraction.update(animated: !wasEmpty, { current in - var current = current.updatedHistoryCount(genericView.tableView.count - 1).updatedKeyboardButtonsMessage(initialData.buttonKeyboardMessage) + let messagesCount = processedView.originalView?.entries.count ?? 0 + var current = current.updatedHistoryCount(messagesCount).updatedKeyboardButtonsMessage(initialData.buttonKeyboardMessage) if let message = initialData.buttonKeyboardMessage { if message.requestsSetupReply { diff --git a/Telegram-Mac/ChatFileContentView.swift b/Telegram-Mac/ChatFileContentView.swift index 691fc2fcf..179e98f60 100644 --- a/Telegram-Mac/ChatFileContentView.swift +++ b/Telegram-Mac/ChatFileContentView.swift @@ -35,7 +35,7 @@ class ChatFileContentView: ChatMediaContentView { } override func previewMediaIfPossible() -> Bool { - guard let context = self.context, let window = self.kitWindow, let table = self.table, media?.isGraphicFile == true, fetchStatus == .Local else {return false} + guard let context = self.context, let window = self._window, let table = self.table, media?.isGraphicFile == true, fetchStatus == .Local else {return false} startModalPreviewHandle(table, window: window, context: context) return true } diff --git a/Telegram-Mac/ChatGroupedItem.swift b/Telegram-Mac/ChatGroupedItem.swift index 5d4fbc44c..4d3150e6f 100644 --- a/Telegram-Mac/ChatGroupedItem.swift +++ b/Telegram-Mac/ChatGroupedItem.swift @@ -593,8 +593,8 @@ class ChatGroupedView : ChatRowView , ModalPreviewRowViewProtocol { } - override func updateMouse() { - super.updateMouse() + override func updateMouse(animated: Bool) { + super.updateMouse(animated: animated) for content in contents { content.updateMouse() } diff --git a/Telegram-Mac/ChatHeaderController.swift b/Telegram-Mac/ChatHeaderController.swift index ca51c219c..be0c5e60b 100644 --- a/Telegram-Mac/ChatHeaderController.swift +++ b/Telegram-Mac/ChatHeaderController.swift @@ -1676,7 +1676,7 @@ private final class ChatRequestChat : Control, ChatHeaderProtocol { _ = self.dismiss.sizeToFit() self.set(handler: { [weak self] control in - if let window = control.kitWindow, let state = self?._state { + if let window = control._window, let state = self?._state { switch state.main { case let .requestChat(_, text): alert(for: window, info: text) diff --git a/Telegram-Mac/ChatInputAttachView.swift b/Telegram-Mac/ChatInputAttachView.swift index 604d090fb..40f743722 100644 --- a/Telegram-Mac/ChatInputAttachView.swift +++ b/Telegram-Mac/ChatInputAttachView.swift @@ -110,7 +110,7 @@ class ChatInputAttachView: ImageButton, Notifable { }, itemImage: MenuAnimation.menu_shared_media.value)) } - if let shortcuts = chatInteraction.presentation.shortcuts, let peer = chatInteraction.peer { + if let shortcuts = chatInteraction.presentation.shortcuts, let peer = chatInteraction.peer, chatInteraction.presentation.chatMode == .history { if peer.isUser && !peer.isBot { if !shortcuts.items.isEmpty, context.isPremium { items.append(ContextMenuItem(strings().chatInputAttachQuickReply, handler: { [weak self] in diff --git a/Telegram-Mac/ChatInputView.swift b/Telegram-Mac/ChatInputView.swift index eb1bdb5ae..e1e2d7e66 100644 --- a/Telegram-Mac/ChatInputView.swift +++ b/Telegram-Mac/ChatInputView.swift @@ -940,7 +940,7 @@ class ChatInputView: View, Notifable { } } - if let window = kitWindow, self.chatState == .normal || self.chatState == .editing { + if let window = _window, self.chatState == .normal || self.chatState == .editing { if let string = pasteboard.string(forType: .string) { interaction.update { current in diff --git a/Telegram-Mac/ChatInteractiveContentView.swift b/Telegram-Mac/ChatInteractiveContentView.swift index 7e3d347c6..494b7ad27 100644 --- a/Telegram-Mac/ChatInteractiveContentView.swift +++ b/Telegram-Mac/ChatInteractiveContentView.swift @@ -238,7 +238,7 @@ class ChatInteractiveContentView: ChatMediaContentView { } override func previewMediaIfPossible() -> Bool { - guard let context = self.context, let window = self.kitWindow, let table = self.table, parent == nil || parent?.containsSecretMedia == false, fetchStatus == .Local else {return false} + guard let context = self.context, let window = self._window, let table = self.table, parent == nil || parent?.containsSecretMedia == false, fetchStatus == .Local else {return false} startModalPreviewHandle(table, window: window, context: context) return true } diff --git a/Telegram-Mac/ChatInterfaceStateContextQueries.swift b/Telegram-Mac/ChatInterfaceStateContextQueries.swift index 112b72033..bae84a067 100644 --- a/Telegram-Mac/ChatInterfaceStateContextQueries.swift +++ b/Telegram-Mac/ChatInterfaceStateContextQueries.swift @@ -251,7 +251,12 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres } case let .command(query): let normalizedQuery = query.lowercased() - + switch chatPresentationInterfaceState.chatMode { + case .history: + break + default: + return (nil, .single({ _ in return nil })) + } if let peer = chatPresentationInterfaceState.peer { if peer.isUser, !peer.isBot { var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() diff --git a/Telegram-Mac/ChatListController.swift b/Telegram-Mac/ChatListController.swift index 2bc011a97..5689c8788 100644 --- a/Telegram-Mac/ChatListController.swift +++ b/Telegram-Mac/ChatListController.swift @@ -322,7 +322,7 @@ fileprivate func prepareEntries(from:[AppearanceWrapperEntry]?, let nState = scrollState ?? (animated ? .none(nil) : .saveVisible(.lower)) - let transition = TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated: animated, state: nState, grouping: !animated || scrollState != nil, animateVisibleOnly: false, groupInOne: !animated || scrollState != nil) + let transition = TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated: animated, state: nState, grouping: !animated || scrollState != nil, animateVisibleOnly: false, groupInOne: false) subscriber.putNext(transition) subscriber.putCompletion() return ActionDisposable { diff --git a/Telegram-Mac/ChatListSystemDeprecatedItem.swift b/Telegram-Mac/ChatListSystemDeprecatedItem.swift index 4fbebb6f4..490c374a7 100644 --- a/Telegram-Mac/ChatListSystemDeprecatedItem.swift +++ b/Telegram-Mac/ChatListSystemDeprecatedItem.swift @@ -67,7 +67,7 @@ private final class ChatListSystemDeprecatedItemView : TableRowView { control.border = [.Bottom] control.set(handler: { [weak self] control in - if let window = control.kitWindow { + if let window = control._window { verifyAlert_button(for: window, header: strings().deprecatedAlertTitle, information: strings().deprecatedAlertText, cancel: "", option: strings().deprecatedAlertThird, successHandler: { result in if result == .thrid { if let item = self?.item as? ChatListSystemDeprecatedItem { diff --git a/Telegram-Mac/ChatMediaItem.swift b/Telegram-Mac/ChatMediaItem.swift index b10be03e2..f2bb4a74c 100644 --- a/Telegram-Mac/ChatMediaItem.swift +++ b/Telegram-Mac/ChatMediaItem.swift @@ -753,8 +753,8 @@ class ChatMediaView: ChatRowView, ModalPreviewRowViewProtocol { } - override func updateMouse() { - super.updateMouse() + override func updateMouse(animated: Bool) { + super.updateMouse(animated: animated) self.contentNode?.updateMouse() } diff --git a/Telegram-Mac/ChatMessageView.swift b/Telegram-Mac/ChatMessageView.swift index 263506222..246500251 100644 --- a/Telegram-Mac/ChatMessageView.swift +++ b/Telegram-Mac/ChatMessageView.swift @@ -174,8 +174,8 @@ class ChatMessageView: ChatRowView, ModalPreviewRowViewProtocol { return views } - override func updateMouse() { - super.updateMouse() + override func updateMouse(animated: Bool) { + super.updateMouse(animated: animated) webpageContent?.updateMouse() } diff --git a/Telegram-Mac/ChatRowView.swift b/Telegram-Mac/ChatRowView.swift index bce5fb28a..2c1109692 100644 --- a/Telegram-Mac/ChatRowView.swift +++ b/Telegram-Mac/ChatRowView.swift @@ -197,7 +197,6 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDeleg let selectingMode = selectingMode && item?.chatInteraction.mode.threadId != item?.message?.id if let item = item { - updateMouse() if selectingMode { if selectingView == nil { selectingView = SelectingControl(unselectedImage: item.presentation.chat_toggle_unselected, selectedImage: item.presentation.chat_toggle_selected, selected: item.isSelectedMessage) @@ -383,7 +382,7 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDeleg renderLayoutType(item, animated: true) updateColors() - updateMouse() + updateMouse(animated: false) item.chatInteraction.focusInputField() super.onCloseContextMenu() } @@ -392,17 +391,25 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDeleg guard let item = item as? ChatRowItem else {return} renderLayoutType(item, animated: true) updateColors() - updateMouse() + updateMouse(animated: false) super.onCloseContextMenu() } + func mouseInsideRow() -> Bool { + guard let window else { + return false + } + let rect = rowView.convert(rowView.bounds, to: nil) + return NSPointInRect(window.mouseLocationOutsideOfEventStream, rect) + } - override func updateMouse() { + override func updateMouse(animated: Bool) { if let shareView = self.shareView, let item = item as? ChatRowItem { - shareView.change(opacity: item.chatInteraction.presentation.state != .selecting && mouseInside() && contextMenu == nil ? 1.0 : 0.0, animated: true) + let active = item.chatInteraction.presentation.state != .selecting && mouseInsideRow() && contextMenu == nil ? 1.0 : 0.0 + shareView.change(opacity: active, animated: false) } if let commentsView = self.channelCommentsBubbleSmallControl, let item = item as? ChatRowItem { - commentsView.change(opacity: item.chatInteraction.presentation.state != .selecting && mouseInside() && contextMenu == nil ? 1.0 : 0.0, animated: true) + commentsView.change(opacity: item.chatInteraction.presentation.state != .selecting && mouseInsideRow() && contextMenu == nil ? 1.0 : 0.0, animated: false) } } @@ -1367,10 +1374,11 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDeleg shareView = ImageButton(frame: CGRect(origin: shareViewPoint(item), size: NSMakeSize(26, 26))) shareView?.disableActions() shareView?.scaleOnClick = true - shareView?.change(opacity: 0, animated: false) rowView.addSubview(shareView!) } + updateMouse(animated: false) + guard let control = shareView else {return} control.autohighlight = false @@ -1739,12 +1747,6 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDeleg return } - //if previousItem == nil { - // bubbleView.frame = bubbleFrame(item) - // rowView.frame = CGRect(origin: rowPoint(item), size: frame.size) - // } - - if self.animatedView != nil && self.animatedView?.stableId != item.stableId { self.animatedView?.removeFromSuperview() self.animatedView = nil diff --git a/Telegram-Mac/ChatSelectText.swift b/Telegram-Mac/ChatSelectText.swift index bc055c61c..1b693f1a4 100644 --- a/Telegram-Mac/ChatSelectText.swift +++ b/Telegram-Mac/ChatSelectText.swift @@ -264,14 +264,14 @@ class ChatSelectText : NSObject { table.addScroll(listener: TableScrollListener (dispatchWhenVisibleRangeUpdated: false, { [weak table] _ in table?.enumerateVisibleViews(with: { view in - view.updateMouse() + view.updateMouse(animated: true) }) })) window.set(mouseHandler: { [weak table] event -> KeyHandlerResult in table?.enumerateVisibleViews(with: { view in - view.updateMouse() + view.updateMouse(animated: true) }) return .rejected diff --git a/Telegram-Mac/ClosureInviteLinkController.swift b/Telegram-Mac/ClosureInviteLinkController.swift index 19ea76832..95dd03de4 100644 --- a/Telegram-Mac/ClosureInviteLinkController.swift +++ b/Telegram-Mac/ClosureInviteLinkController.swift @@ -38,6 +38,7 @@ struct ClosureInviteLinkState: Equatable { fileprivate var tempDate: Int32? fileprivate(set) var requestApproval: Bool fileprivate(set) var title: String? + fileprivate(set) var isPublic: Bool = false } // @@ -67,76 +68,77 @@ private func inviteLinkEntries(state: ClosureInviteLinkState, arguments: InviteL entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().editInvitationTitleDesc), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) index += 1 + if !state.isPublic { + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_request_approval, data: .init(name: strings().editInvitationRequestApproval, color: theme.colors.text, type: .switchable(state.requestApproval), viewType: .singleItem, action: { + arguments.toggleRequestApproval(state.requestApproval) + }))) + index += 1 + + let requestApprovalText: String + if state.requestApproval { + requestApprovalText = strings().editInvitationRequestApprovalChannelOn + } else { + requestApprovalText = strings().editInvitationRequestApprovalChannelOff + } + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(requestApprovalText), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + } entries.append(.sectionId(sectionId, type: .customModern(20))) sectionId += 1 - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_request_approval, data: .init(name: strings().editInvitationRequestApproval, color: theme.colors.text, type: .switchable(state.requestApproval), viewType: .singleItem, action: { - arguments.toggleRequestApproval(state.requestApproval) - }))) - index += 1 - - let requestApprovalText: String - if state.requestApproval { - requestApprovalText = strings().editInvitationRequestApprovalChannelOn - } else { - requestApprovalText = strings().editInvitationRequestApprovalChannelOff - } - - entries.append(.desc(sectionId: sectionId, index: index, text: .plain(requestApprovalText), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().editInvitationLimitedByPeriod), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) index += 1 - entries.append(.sectionId(sectionId, type: .customModern(20))) - sectionId += 1 - - entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().editInvitationLimitedByPeriod), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) - index += 1 + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_period, equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in + let hour: Int32 = 60 * 60 + let day: Int32 = hour * 24 * 1 + var sizes:[Int32] = [hour, day, day * 7, Int32.max] - entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_period, equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in - let hour: Int32 = 60 * 60 - let day: Int32 = hour * 24 * 1 - var sizes:[Int32] = [hour, day, day * 7, Int32.max] - - if let temp = state.tempDate { - var bestIndex: Int = 0 - for (i, size) in sizes.enumerated() { - if size < temp { - bestIndex = i - } - } - sizes[bestIndex] = temp - } - - let current = state.date - if sizes.firstIndex(where: { $0 == current }) == nil { - var bestIndex: Int = 0 - for (i, size) in sizes.enumerated() { - if size < current { - bestIndex = i - } + if let temp = state.tempDate { + var bestIndex: Int = 0 + for (i, size) in sizes.enumerated() { + if size < temp { + bestIndex = i } - sizes[bestIndex] = current } - let titles: [String] = sizes.map { value in - if value == Int32.max { - return "∞" - } else { - return autoremoveLocalized(Int(value)) + sizes[bestIndex] = temp + } + + let current = state.date + if sizes.firstIndex(where: { $0 == current }) == nil { + var bestIndex: Int = 0 + for (i, size) in sizes.enumerated() { + if size < current { + bestIndex = i } } - let viewType: GeneralViewType - if state.requestApproval { - viewType = .singleItem + sizes[bestIndex] = current + } + let titles: [String] = sizes.map { value in + if value == Int32.max { + return "∞" } else { - viewType = .firstItem + return autoremoveLocalized(Int(value)) } + } + let viewType: GeneralViewType + if state.requestApproval { + viewType = .singleItem + } else { + viewType = .firstItem + } - return SelectSizeRowItem(initialSize, stableId: stableId, current: current, sizes: sizes, hasMarkers: false, titles: titles, viewType: viewType, selectAction: { index in - arguments.limitDate(sizes[index]) - }) - })) - index += 1 + return SelectSizeRowItem(initialSize, stableId: stableId, current: current, sizes: sizes, hasMarkers: false, titles: titles, viewType: viewType, selectAction: { index in + arguments.limitDate(sizes[index]) + }) + })) + index += 1 if !state.requestApproval { let dateFormatter = makeNewDateFormatter() @@ -286,10 +288,25 @@ func ClosureInviteLinkController(context: AccountContext, peerId: PeerId, mode: let state: ValuePromise = ValuePromise(initialState) let stateValue: Atomic = Atomic(value: initialState) + let actionsDisposable = DisposableSet() + + + let updateState:((ClosureInviteLinkState)->ClosureInviteLinkState) -> Void = { f in state.set(stateValue.modify(f)) } + actionsDisposable.add(getPeerView(peerId: peerId, postbox: context.account.postbox).start(next: { peer in + updateState { current in + var current = current + current.isPublic = peer?.addressName != nil && !peer!.addressName!.isEmpty + if current.isPublic { + current.requestApproval = false + } + return current + } + })) + let arguments = InviteLinkArguments(context: context, usageLimit: { value in updateState { current in var current = current @@ -337,6 +354,10 @@ func ClosureInviteLinkController(context: AccountContext, peerId: PeerId, mode: getModalController?()?.close() }) + controller.onDeinit = { + actionsDisposable.dispose() + } + controller.updateDatas = { data in updateState { current in var current = current diff --git a/Telegram-Mac/DatePickerRowItem.swift b/Telegram-Mac/DatePickerRowItem.swift index 750979c21..013d4da10 100644 --- a/Telegram-Mac/DatePickerRowItem.swift +++ b/Telegram-Mac/DatePickerRowItem.swift @@ -33,7 +33,7 @@ private final class DateSelectorView : View { atView.update(atLayout) self.dayPicker.set(handler: { [weak self] control in - if let control = control as? DatePicker, let window = self?.kitWindow, !hasPopover(window) { + if let control = control as? DatePicker, let window = self?._window, !hasPopover(window) { let calendar = CalendarController(NSMakeRect(0, 0, 300, 300), window, current: control.selected.value, onlyFuture: true, limitedBy: Date(timeIntervalSinceNow: 7 * 24 * 60 * 60), selectHandler: { [weak self] date in self?.applyDay(date) if let date = self?.select() { diff --git a/Telegram-Mac/DesktopCapturePreviewItem.swift b/Telegram-Mac/DesktopCapturePreviewItem.swift index 98808ab15..81da1bb8d 100644 --- a/Telegram-Mac/DesktopCapturePreviewItem.swift +++ b/Telegram-Mac/DesktopCapturePreviewItem.swift @@ -300,8 +300,8 @@ final class DesktopCapturePreviewView : HorizontalRowView { self.backgroundColor = backdorColor } - override func updateMouse() { - super.updateMouse() + override func updateMouse(animated: Bool) { + super.updateMouse(animated: animated) contentView.updateState() } diff --git a/Telegram-Mac/EmojiesController.swift b/Telegram-Mac/EmojiesController.swift index 3af93a0c7..c2c9f0f91 100644 --- a/Telegram-Mac/EmojiesController.swift +++ b/Telegram-Mac/EmojiesController.swift @@ -1465,7 +1465,7 @@ final class AnimatedEmojiesView : Control { func updateSearchState(_ searchState: SearchState, animated: Bool) { - if let window = kitWindow, self.mode == .reactions || self.mode == .defaultTags { + if let window = _window, self.mode == .reactions || self.mode == .defaultTags { switch searchState.state { case .Focus: window._canBecomeKey = true diff --git a/Telegram-Mac/EmojiesPackItem.swift b/Telegram-Mac/EmojiesPackItem.swift index 382624f25..3404fd028 100644 --- a/Telegram-Mac/EmojiesPackItem.swift +++ b/Telegram-Mac/EmojiesPackItem.swift @@ -102,8 +102,8 @@ private final class EmojiesPackView : HorizontalRowView { } - override func updateMouse() { - super.updateMouse() + override func updateMouse(animated: Bool) { + super.updateMouse(animated: animated) self.contentNode?.updateMouse() } diff --git a/Telegram-Mac/EntertainmentViewController.swift b/Telegram-Mac/EntertainmentViewController.swift index cc9cb76e4..87e5ccd3e 100644 --- a/Telegram-Mac/EntertainmentViewController.swift +++ b/Telegram-Mac/EntertainmentViewController.swift @@ -275,8 +275,8 @@ open class EntertainmentSearchView: OverlayControl, NSTextViewDelegate { change(state: .None, true) } - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) + self._window?.removeAllHandlers(for: self) + self._window?.removeObserver(for: self) } open func didBecomeResponder() { @@ -286,7 +286,7 @@ open class EntertainmentSearchView: OverlayControl, NSTextViewDelegate { change(state: .Focus, true) - self.kitWindow?.set(escape: { [weak self] _ -> KeyHandlerResult in + self._window?.set(escape: { [weak self] _ -> KeyHandlerResult in if let strongSelf = self { return strongSelf.changeResponder() ? .invoked : .rejected } @@ -294,21 +294,21 @@ open class EntertainmentSearchView: OverlayControl, NSTextViewDelegate { }, with: self, priority: .modal) - self.kitWindow?.set(handler: { [weak self] _ -> KeyHandlerResult in + self._window?.set(handler: { [weak self] _ -> KeyHandlerResult in if self?.state == .Focus { return .invokeNext } return .rejected }, with: self, for: .RightArrow, priority: .modal) - self.kitWindow?.set(handler: { [weak self] _ -> KeyHandlerResult in + self._window?.set(handler: { [weak self] _ -> KeyHandlerResult in if self?.state == .Focus { return .invokeNext } return .rejected }, with: self, for: .LeftArrow, priority: .modal) - self.kitWindow?.set(responder: {[weak self] () -> NSResponder? in + self._window?.set(responder: {[weak self] () -> NSResponder? in return self?.input }, with: self, priority: .modal) } @@ -356,8 +356,8 @@ open class EntertainmentSearchView: OverlayControl, NSTextViewDelegate { if state == .None { - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) + self._window?.removeAllHandlers(for: self) + self._window?.removeObserver(for: self) self.input.isHidden = true self.input.string = "" @@ -388,8 +388,8 @@ open class EntertainmentSearchView: OverlayControl, NSTextViewDelegate { if isEmpty { change(state: .None, false) } - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) + self._window?.removeAllHandlers(for: self) + self._window?.removeObserver(for: self) } } @@ -446,8 +446,8 @@ open class EntertainmentSearchView: OverlayControl, NSTextViewDelegate { } deinit { - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) + self._window?.removeAllHandlers(for: self) + self._window?.removeObserver(for: self) } public var query:String { diff --git a/Telegram-Mac/GalleryViewer.swift b/Telegram-Mac/GalleryViewer.swift index b6030b098..390524384 100644 --- a/Telegram-Mac/GalleryViewer.swift +++ b/Telegram-Mac/GalleryViewer.swift @@ -345,7 +345,7 @@ class GalleryViewer: NSResponder { } interactions.canShare = { if let chatMode = chatMode { - return !chatMode.isSavedMode + return !chatMode.isSavedMode && chatMode.customChatContents == nil } else { return true } @@ -837,14 +837,14 @@ class GalleryViewer: NSResponder { if savedGifs.contains(where: { $0.media.fileId == file.fileId }) { items.append(ContextMenuItem(strings().galleryRemoveGif, handler: { [weak control] in _ = removeSavedGif(postbox: context.account.postbox, mediaId: file.fileId).start() - if let window = control?.kitWindow { + if let window = control?._window { showModalText(for: window, text: strings().chatContextGifRemoved) } }, itemImage: MenuAnimation.menu_remove_gif.value)) } else { items.append(ContextMenuItem(strings().gallerySaveGif, handler: { [weak control, weak self] in - guard let window = control?.kitWindow else { + guard let window = control?._window else { return } @@ -857,7 +857,7 @@ class GalleryViewer: NSResponder { return } let _ = addSavedGif(postbox: context.account.postbox, fileReference: reference).start() - if let window = control?.kitWindow { + if let window = control?._window { showModalText(for: window, text: strings().chatContextGifAdded) } }, itemImage: MenuAnimation.menu_add_gif.value)) @@ -871,8 +871,16 @@ class GalleryViewer: NSResponder { }, itemImage: MenuAnimation.menu_copy.value)) } + let acceptInteractions: Bool + switch chatMode { + case .customChatContents(let contents): + acceptInteractions = false + default: + acceptInteractions = true + } + - if let contentInteractions = self.contentInteractions { + if let contentInteractions = self.contentInteractions, acceptInteractions { if let message = pager.selectedItem?.entry.message, let pageItem = pager.selectedItem { if self.type == .history { items.append(ContextMenuItem(strings().galleryContextShowMessage, handler: { [weak self] in diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index da2673782..a75c0e5c3 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260440 + 260552 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/InlineAudioPlayerView.swift b/Telegram-Mac/InlineAudioPlayerView.swift index 94a28bb40..c4a9ef1a6 100644 --- a/Telegram-Mac/InlineAudioPlayerView.swift +++ b/Telegram-Mac/InlineAudioPlayerView.swift @@ -305,7 +305,7 @@ class InlineAudioPlayerView: NavigationHeaderView, APDelegate { } private func showAudioPlayerList() { - guard let window = kitWindow, let context = self.context else {return} + guard let window = _window, let context = self.context else {return} let point = containerView.convert(window.mouseLocationOutsideOfEventStream, from: nil) if NSPointInRect(point, textViewContainer.frame) { if let controller = controller as? APChatMusicController, let song = controller.currentSong { diff --git a/Telegram-Mac/InputSwapSuggestionsPanel.swift b/Telegram-Mac/InputSwapSuggestionsPanel.swift index 6520b7c42..c5ec0d435 100644 --- a/Telegram-Mac/InputSwapSuggestionsPanel.swift +++ b/Telegram-Mac/InputSwapSuggestionsPanel.swift @@ -19,7 +19,7 @@ final class InputSwapSuggestionsPanel : View, TableViewDelegate { private let inputView: NSTextView private let textContent: NSClipView - private let _window: Window + private let __window: Window private let context: AccountContext private let tableView = HorizontalTableView(frame: .zero) private let containerView = View() @@ -34,7 +34,7 @@ final class InputSwapSuggestionsPanel : View, TableViewDelegate { self.inputView = inputView self.context = context self.relativeView = relativeView - self._window = window + self.__window = window self.chatInteraction = chatInteraction self.presentation = presentation super.init(frame: .zero) diff --git a/Telegram-Mac/LocationModalController.swift b/Telegram-Mac/LocationModalController.swift index c3fd14567..4148c2e48 100644 --- a/Telegram-Mac/LocationModalController.swift +++ b/Telegram-Mac/LocationModalController.swift @@ -722,6 +722,7 @@ class LocationModalController: ModalViewController { }, error: { [weak self] error in self?.delegate.cancelRequestLocation() })) + } deinit { diff --git a/Telegram-Mac/MediaGroupPreviewRowItem.swift b/Telegram-Mac/MediaGroupPreviewRowItem.swift index 8e1017a3e..80a848bc5 100644 --- a/Telegram-Mac/MediaGroupPreviewRowItem.swift +++ b/Telegram-Mac/MediaGroupPreviewRowItem.swift @@ -166,7 +166,7 @@ class MediaGroupPreviewRowView : TableRowView, ModalPreviewRowViewProtocol { needsLayout = true - updateMouse() + updateMouse(animated: animated) } override func forceClick(in location: NSPoint) { @@ -180,7 +180,7 @@ class MediaGroupPreviewRowView : TableRowView, ModalPreviewRowViewProtocol { } } - override func updateMouse() { + override func updateMouse(animated: Bool) { guard let window = window, let table = item?.table else { for node in self.contents { if let control = node.subviews.last as? MediaPreviewEditControl { diff --git a/Telegram-Mac/MediaPreviewRowItem.swift b/Telegram-Mac/MediaPreviewRowItem.swift index 5a699643f..6b8b4981f 100644 --- a/Telegram-Mac/MediaPreviewRowItem.swift +++ b/Telegram-Mac/MediaPreviewRowItem.swift @@ -159,7 +159,7 @@ fileprivate class MediaPreviewRowView : TableRowView, ModalPreviewRowViewProtoco } - override func updateMouse() { + override func updateMouse(animated: Bool) { guard let window = window, let table = item?.table else { editControl.isHidden = true return @@ -191,7 +191,7 @@ fileprivate class MediaPreviewRowView : TableRowView, ModalPreviewRowViewProtoco self.contentNode = node.init(frame:NSZeroRect) self.addSubview(self.contentNode!) addSubview(editControl) - updateMouse() + updateMouse(animated: animated) } diff --git a/Telegram-Mac/PCallSession.swift b/Telegram-Mac/PCallSession.swift index 3d9f74fb7..b4cbc7db7 100644 --- a/Telegram-Mac/PCallSession.swift +++ b/Telegram-Mac/PCallSession.swift @@ -606,7 +606,11 @@ class PCallSession { case .notAvailable: mappedVideoState = .notAvailable case .active: - mappedVideoState = .active(self.isVideoAvailable) + if videoIsForceDisabled { + mappedVideoState = .inactive(self.isVideoAvailable) + } else { + mappedVideoState = .active(self.isVideoAvailable) + } case .inactive: mappedVideoState = .inactive(self.isVideoAvailable) case .paused: diff --git a/Telegram-Mac/PIPVideoWindow.swift b/Telegram-Mac/PIPVideoWindow.swift index ea8856f89..bfc9dbb98 100644 --- a/Telegram-Mac/PIPVideoWindow.swift +++ b/Telegram-Mac/PIPVideoWindow.swift @@ -33,9 +33,9 @@ protocol PictureInPictureControl { private class PictureInpictureView : Control { - private let _window: Window + private let __window: Window init(frame: NSRect, window: Window) { - _window = window + __window = window super.init(frame: frame) autoresizesSubviews = true } @@ -61,14 +61,14 @@ private class PictureInpictureView : Control { } get { - return _window + return __window } } } fileprivate class ModernPictureInPictureVideoWindow: NSPanel { private let saver: WindowSaver - fileprivate let _window: Window + fileprivate let __window: Window fileprivate let control: PictureInPictureControl private let rect:NSRect private let restoreRect: NSRect @@ -94,17 +94,17 @@ fileprivate class ModernPictureInPictureVideoWindow: NSPanel { self.rect = newRect self.restoreRect = NSMakeRect(origin.x, origin.y, control.view.frame.width, control.view.frame.height) self.item = item - _window = Window(contentRect: newRect, styleMask: [.resizable], backing: .buffered, defer: true) - _window.name = "pip" - self.saver = .find(for: _window) - _window.setFrame(NSMakeRect(3000, 3000, saver.rect.width, saver.rect.height), display: true) + __window = Window(contentRect: newRect, styleMask: [.resizable], backing: .buffered, defer: true) + __window.name = "pip" + self.saver = .find(for: __window) + __window.setFrame(NSMakeRect(3000, 3000, saver.rect.width, saver.rect.height), display: true) super.init(contentRect: newRect, styleMask: [.resizable, .nonactivatingPanel], backing: .buffered, defer: true) self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]; - let view = PictureInpictureView(frame: bounds, window: _window) + let view = PictureInpictureView(frame: bounds, window: __window) self.contentView = view view.forceMouseDownCanMoveWindow = true @@ -119,22 +119,22 @@ fileprivate class ModernPictureInPictureVideoWindow: NSPanel { contentView?.addSubview(control.view) - _window.set(mouseHandler: { event -> KeyHandlerResult in + __window.set(mouseHandler: { event -> KeyHandlerResult in NSCursor.arrow.set() return .invoked }, with: self, for: .mouseMoved, priority: .low) - _window.set(mouseHandler: { event -> KeyHandlerResult in + __window.set(mouseHandler: { event -> KeyHandlerResult in NSCursor.arrow.set() return .invoked }, with: self, for: .mouseEntered, priority: .low) - _window.set(mouseHandler: { event -> KeyHandlerResult in + __window.set(mouseHandler: { event -> KeyHandlerResult in return .invoked }, with: self, for: .mouseExited, priority: .low) - _window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + __window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in if event.clickCount == 2, let strongSelf = self { let inner = strongSelf.control.view.convert(event.locationInWindow, from: nil) if NSWindow.windowNumber(at: NSEvent.mouseLocation, belowWindowWithWindowNumber: 0) == strongSelf.windowNumber, strongSelf.control.view.hitTest(inner) is MediaPlayerView { @@ -145,7 +145,7 @@ fileprivate class ModernPictureInPictureVideoWindow: NSPanel { }, with: self, for: .leftMouseDown, priority: .low) - _window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + __window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in self?.findAndMoveToCorner() return .rejected }, with: self, for: .leftMouseUp, priority: .low) @@ -160,13 +160,13 @@ fileprivate class ModernPictureInPictureVideoWindow: NSPanel { eventLocalMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .mouseEntered, .mouseExited, .leftMouseDown, .leftMouseUp], handler: { [weak self] event in guard let `self` = self else {return event} - self._window.sendEvent(event) + self.__window.sendEvent(event) return event }) eventGlobalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .mouseEntered, .mouseExited, .leftMouseDown, .leftMouseUp], handler: { [weak self] event in guard let `self` = self else {return} - self._window.sendEvent(event) + self.__window.sendEvent(event) }) @@ -194,7 +194,7 @@ fileprivate class ModernPictureInPictureVideoWindow: NSPanel { orderOut(nil) window = nil - _window.removeAllHandlers(for: self) + __window.removeAllHandlers(for: self) if let monitor = eventLocalMonitor { NSEvent.removeMonitor(monitor) } diff --git a/Telegram-Mac/PeerInfoBusinessItems.swift b/Telegram-Mac/PeerInfoBusinessItems.swift index 954e2d175..3ff3c935a 100644 --- a/Telegram-Mac/PeerInfoBusinessItems.swift +++ b/Telegram-Mac/PeerInfoBusinessItems.swift @@ -540,11 +540,12 @@ private final class PeerInfoHoursRowView: GeneralContainableRowView { } - transition.updateFrame(view: title, frame: NSMakeRect(item.viewType.innerInset.left, item.viewType.innerInset.top, title.frame.width, title.frame.height)) - transition.updateFrame(view: status, frame: NSMakeRect(item.viewType.innerInset.left, title.frame.maxY + 6, status.frame.width, status.frame.height)) + ContainedViewLayoutTransition.immediate.updateFrame(view: title, frame: NSMakeRect(item.viewType.innerInset.left, item.viewType.innerInset.top, title.frame.width, title.frame.height)) + + ContainedViewLayoutTransition.immediate.updateFrame(view: status, frame: NSMakeRect(item.viewType.innerInset.left, title.frame.maxY + 6, status.frame.width, status.frame.height)) - transition.updateFrame(view: today, frame: NSMakeRect(containerView.frame.width - today.frame.width - item.viewType.innerInset.right, status.frame.minY, today.frame.width, today.frame.height)) + ContainedViewLayoutTransition.immediate.updateFrame(view: today, frame: NSMakeRect(containerView.frame.width - today.frame.width - item.viewType.innerInset.right, status.frame.minY, today.frame.width, today.frame.height)) transition.updateFrame(view: daysContainer, frame: NSMakeRect(item.viewType.innerInset.left, item.basicHeight, containerView.frame.width - item.viewType.innerInset.left * 2, 0)) diff --git a/Telegram-Mac/PeerPhotosMonthItem.swift b/Telegram-Mac/PeerPhotosMonthItem.swift index b6e874065..9fa6b60b2 100644 --- a/Telegram-Mac/PeerPhotosMonthItem.swift +++ b/Telegram-Mac/PeerPhotosMonthItem.swift @@ -774,8 +774,8 @@ private final class PeerPhotosMonthView : TableRowView, Notifable { private weak var currentMouseCell: MediaCell? - @objc override func updateMouse() { - super.updateMouse() + @objc func _updateMouse() { + super.updateMouse(animated: true) guard let window = self.window else { return } @@ -795,15 +795,15 @@ private final class PeerPhotosMonthView : TableRowView, Notifable { override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) - updateMouse() + _updateMouse() } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) - updateMouse() + _updateMouse() } override func mouseMoved(with event: NSEvent) { super.mouseMoved(with: event) - updateMouse() + _updateMouse() } private func action(event: ControlEvent) { @@ -939,8 +939,8 @@ private final class PeerPhotosMonthView : TableRowView, Notifable { NotificationCenter.default.removeObserver(self) } else { NotificationCenter.default.addObserver(self, selector: #selector(updateVisibleItems), name: NSView.boundsDidChangeNotification, object: self.enclosingScrollView?.contentView) - NotificationCenter.default.addObserver(self, selector: #selector(updateMouse), name: NSWindow.didBecomeKeyNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(updateMouse), name: NSWindow.didResignKeyNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(_updateMouse), name: NSWindow.didBecomeKeyNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(_updateMouse), name: NSWindow.didResignKeyNotification, object: nil) } updateVisibleItems() } diff --git a/Telegram-Mac/PreviewSenderController.swift b/Telegram-Mac/PreviewSenderController.swift index 3d559546f..8ef1cd908 100644 --- a/Telegram-Mac/PreviewSenderController.swift +++ b/Telegram-Mac/PreviewSenderController.swift @@ -120,7 +120,7 @@ fileprivate class PreviewSenderView : Control { DispatchQueue.main.async { - if newValue.state != previous.state || newValue.isCollage != previous.isCollage, let window = self.kitWindow { + if newValue.state != previous.state || newValue.isCollage != previous.isCollage, let window = self._window { removeAllTooltips(window) } self.fileButton.controlState = .Normal @@ -1589,7 +1589,7 @@ class PreviewSenderController: ModalViewController, Notifable { guard let `self` = self else {return .rejected} self.genericView.tableView.enumerateViews(with: { view -> Bool in - view.updateMouse() + view.updateMouse(animated: true) return true }) diff --git a/Telegram-Mac/RecentPeerRowItem.swift b/Telegram-Mac/RecentPeerRowItem.swift index f84b364b7..de3a0c9b7 100644 --- a/Telegram-Mac/RecentPeerRowItem.swift +++ b/Telegram-Mac/RecentPeerRowItem.swift @@ -95,20 +95,20 @@ class RecentPeerRowView : ShortPeerRowView { override func mouseMoved(with event: NSEvent) { super.mouseMoved(with: event) - updateMouse() + updateMouse(animated: true) } override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) - updateMouse() + updateMouse(animated: true) } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) - updateMouse() + updateMouse(animated: true) } - override func updateMouse() { + override func updateMouse(animated: Bool) { if mouseInside(), control.superview != nil { control.isHidden = false badgeView?.isHidden = true @@ -150,7 +150,7 @@ class RecentPeerRowView : ShortPeerRowView { badgeView = nil } } - updateMouse() + updateMouse(animated: true) needsLayout = true } diff --git a/Telegram-Mac/ShortPeerRowView.swift b/Telegram-Mac/ShortPeerRowView.swift index dc0b9f136..838b1211b 100644 --- a/Telegram-Mac/ShortPeerRowView.swift +++ b/Telegram-Mac/ShortPeerRowView.swift @@ -52,11 +52,11 @@ class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { containerView.addSubview(separator) container.set(handler: { [weak self] _ in - self?.updateMouse() + self?.updateMouse(animated: true) }, for: .Hover) container.set(handler: { [weak self] _ in - self?.updateMouse() + self?.updateMouse(animated: true) }, for: .Normal) container.userInteractionEnabled = false @@ -251,8 +251,8 @@ class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { self.container.needsDisplay = true } - override func updateMouse() { - super.updateMouse() + override func updateMouse(animated: Bool) { + super.updateMouse(animated: animated) updateColors() container.needsDisplay = true guard let item = item as? ShortPeerRowItem else { diff --git a/Telegram-Mac/SmartThemePreviewRowItem.swift b/Telegram-Mac/SmartThemePreviewRowItem.swift index bcba1cf22..d111d6d04 100644 --- a/Telegram-Mac/SmartThemePreviewRowItem.swift +++ b/Telegram-Mac/SmartThemePreviewRowItem.swift @@ -99,8 +99,8 @@ private final class ChatThemeRowView: HorizontalRowView { }, for: .Normal) } - override func updateMouse() { - super.updateMouse() + override func updateMouse(animated: Bool) { + super.updateMouse(animated: animated) overlay.updateState() } diff --git a/Telegram-Mac/StatisticRowItem.swift b/Telegram-Mac/StatisticRowItem.swift index cc0659e26..eb67a9885 100644 --- a/Telegram-Mac/StatisticRowItem.swift +++ b/Telegram-Mac/StatisticRowItem.swift @@ -138,8 +138,8 @@ class StatisticRowView: TableRowView { } - override func updateMouse() { - super.updateMouse() + override func updateMouse(animated: Bool) { + super.updateMouse(animated: animated) chartView.updateMouse() } diff --git a/Telegram-Mac/StickerPackTrendingItem.swift b/Telegram-Mac/StickerPackTrendingItem.swift index 7a0c285bd..e52dfc298 100644 --- a/Telegram-Mac/StickerPackTrendingItem.swift +++ b/Telegram-Mac/StickerPackTrendingItem.swift @@ -93,8 +93,8 @@ private final class FeaturedAnimatedHorizontalView : HorizontalRowView { } - override func updateMouse() { - super.updateMouse() + override func updateMouse(animated: Bool) { + super.updateMouse(animated: animated) self.contentNode?.updateMouse() } diff --git a/Telegram-Mac/StickerSetTableRowItem.swift b/Telegram-Mac/StickerSetTableRowItem.swift index b68ad8693..060e6e1b1 100644 --- a/Telegram-Mac/StickerSetTableRowItem.swift +++ b/Telegram-Mac/StickerSetTableRowItem.swift @@ -211,7 +211,7 @@ class StickerSetTableRowView : TableRowView, ViewDisplayDelegate { override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) - self.updateMouse() + self.updateMouse(animated: animated) self.contentView.userInteractionEnabled = false if let item = item as? StickerSetTableRowItem { diff --git a/Telegram-Mac/StorageUsageMediaCells.swift b/Telegram-Mac/StorageUsageMediaCells.swift index cc5e327e2..81978a90a 100644 --- a/Telegram-Mac/StorageUsageMediaCells.swift +++ b/Telegram-Mac/StorageUsageMediaCells.swift @@ -346,8 +346,8 @@ private final class StorageUsageMediaCellsView : GeneralContainableRowView { private weak var currentMouseCell: MediaCell? - @objc override func updateMouse() { - super.updateMouse() + @objc func _updateMouse() { + super.updateMouse(animated: true) guard let window = self.window else { return } @@ -367,15 +367,15 @@ private final class StorageUsageMediaCellsView : GeneralContainableRowView { override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) - updateMouse() + _updateMouse() } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) - updateMouse() + _updateMouse() } override func mouseMoved(with event: NSEvent) { super.mouseMoved(with: event) - updateMouse() + _updateMouse() } @@ -483,8 +483,8 @@ private final class StorageUsageMediaCellsView : GeneralContainableRowView { NotificationCenter.default.removeObserver(self) } else { NotificationCenter.default.addObserver(self, selector: #selector(updateVisibleItems), name: NSView.boundsDidChangeNotification, object: self.enclosingScrollView?.contentView) - NotificationCenter.default.addObserver(self, selector: #selector(updateMouse), name: NSWindow.didBecomeKeyNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(updateMouse), name: NSWindow.didResignKeyNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(_updateMouse), name: NSWindow.didBecomeKeyNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(_updateMouse), name: NSWindow.didResignKeyNotification, object: nil) } updateVisibleItems() } diff --git a/Telegram-Mac/StoryInputView.swift b/Telegram-Mac/StoryInputView.swift index 1d0d7fc9a..875a7410a 100644 --- a/Telegram-Mac/StoryInputView.swift +++ b/Telegram-Mac/StoryInputView.swift @@ -507,7 +507,7 @@ final class StoryInputView : Control, StoryInput { func processPaste(_ pasteboard: NSPasteboard) -> Bool { - if let window = kitWindow, let arguments = self.arguments { + if let window = _window, let arguments = self.arguments { let context = arguments.context let chatInteraction = arguments.chatInteraction diff --git a/Telegram-Mac/StoryMonthRowItem.swift b/Telegram-Mac/StoryMonthRowItem.swift index 99e9ba392..65d48000e 100644 --- a/Telegram-Mac/StoryMonthRowItem.swift +++ b/Telegram-Mac/StoryMonthRowItem.swift @@ -286,8 +286,8 @@ private final class StoryMonthRowView : GeneralContainableRowView, Notifable { private weak var currentMouseCell: MediaCell? - @objc override func updateMouse() { - super.updateMouse() + @objc func _updateMouse() { + self.updateMouse(animated: true) guard let window = self.window else { return } @@ -307,15 +307,15 @@ private final class StoryMonthRowView : GeneralContainableRowView, Notifable { override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) - updateMouse() + _updateMouse() } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) - updateMouse() + _updateMouse() } override func mouseMoved(with event: NSEvent) { super.mouseMoved(with: event) - updateMouse() + _updateMouse() } private func action(event: ControlEvent) { @@ -432,8 +432,8 @@ private final class StoryMonthRowView : GeneralContainableRowView, Notifable { NotificationCenter.default.removeObserver(self) } else { NotificationCenter.default.addObserver(self, selector: #selector(updateVisibleItems), name: NSView.boundsDidChangeNotification, object: self.enclosingScrollView?.contentView) - NotificationCenter.default.addObserver(self, selector: #selector(updateMouse), name: NSWindow.didBecomeKeyNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(updateMouse), name: NSWindow.didResignKeyNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(_updateMouse), name: NSWindow.didBecomeKeyNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(_updateMouse), name: NSWindow.didResignKeyNotification, object: nil) } updateVisibleItems() } diff --git a/Telegram-Mac/UIInputView.swift b/Telegram-Mac/UIInputView.swift index 843eb5ac0..adffa6a3a 100644 --- a/Telegram-Mac/UIInputView.swift +++ b/Telegram-Mac/UIInputView.swift @@ -255,7 +255,7 @@ final class UITextView : View, Notifable, ChatInputTextViewDelegate { case .url: let range = self.interactions.presentation.selectionRange let textRange = NSMakeRange(range.lowerBound, range.upperBound - range.lowerBound) - guard textRange.min != textRange.max, let window = kitWindow else { + guard textRange.min != textRange.max, let window = _window else { return } diff --git a/Telegram-Mac/VideoStickerContentView.swift b/Telegram-Mac/VideoStickerContentView.swift index c2601b7c7..b2c53681b 100644 --- a/Telegram-Mac/VideoStickerContentView.swift +++ b/Telegram-Mac/VideoStickerContentView.swift @@ -31,7 +31,7 @@ class VideoStickerContentView: ChatMediaContentView { } override func previewMediaIfPossible() -> Bool { - guard let context = self.context, let window = self.kitWindow, let table = self.table else {return false} + guard let context = self.context, let window = self._window, let table = self.table else {return false} startModalPreviewHandle(table, window: window, context: context) return true } diff --git a/Telegram-Mac/WPArticleContentView.swift b/Telegram-Mac/WPArticleContentView.swift index 8c7b05dfb..41b635a6a 100644 --- a/Telegram-Mac/WPArticleContentView.swift +++ b/Telegram-Mac/WPArticleContentView.swift @@ -38,7 +38,7 @@ class WPArticleContentView: WPContentView { } override func previewMediaIfPossible() -> Bool { - guard let window = self.kitWindow, let content = content as? WPArticleLayout, content.isFullImageSize, let table = content.table, let imageView = imageView, imageView._mouseInside(), playIcon == nil, !content.hasInstantPage else {return false} + guard let window = self._window, let content = content as? WPArticleLayout, content.isFullImageSize, let table = content.table, let imageView = imageView, imageView._mouseInside(), playIcon == nil, !content.hasInstantPage else {return false} startModalPreviewHandle(table, window: window, context: content.context) return true } @@ -81,7 +81,7 @@ class WPArticleContentView: WPContentView { } func open() { - if let content = content?.content, let layout = self.content, let window = kitWindow { + if let content = content?.content, let layout = self.content, let window = _window { if layout.hasInstantPage { showInstantPage(InstantPageViewController(layout.context, webPage: layout.parent.media[0] as! TelegramMediaWebpage, message: layout.parent.text)) diff --git a/Telegram-Mac/WPMediaContentView.swift b/Telegram-Mac/WPMediaContentView.swift index a33a5aa00..e090c42fa 100644 --- a/Telegram-Mac/WPMediaContentView.swift +++ b/Telegram-Mac/WPMediaContentView.swift @@ -49,7 +49,7 @@ class WPMediaContentView: WPContentView { } override func previewMediaIfPossible() -> Bool { - guard let window = self.kitWindow, let content = content as? WPArticleLayout, content.isFullImageSize, let table = content.table, let contentNode = contentNode, contentNode.mouseInside() else {return false} + guard let window = self._window, let content = content as? WPArticleLayout, content.isFullImageSize, let table = content.table, let contentNode = contentNode, contentNode.mouseInside() else {return false} startModalPreviewHandle(table, window: window, context: content.context) return true } diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index e5c08ae4e4327b7eb56ddec451452ce16a3b7e01..58745b958b4d0858bada95bcc6d24cd003b984e2 100644 GIT binary patch delta 137 zcmaE|%%pFHNka=`3sVbo3rh=Y3tJ0&3r7oQ3s(zw3(pkZp6OC7ydu*NJmwLb-Zqty zZ}KK1vB@z%9K;J5N*OX4k{PlY6c~yaQW**uau_NZ?596`%OgL%=NgaDbh-UpBGVgA naS2RMIL*Z|o$D1)3@ovVhnHu1z)4oI=>?hGEZg-MdCi0XJ$x)} delta 45 zcmeBMVe({|Nka=`3sVbo3rh=Y3tJ0&3r7oQ3s(zw3(pkZp6Nooyi(f(7CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260440 + 260552 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/CallVideoLayer/Sources/MetalCallVideoView.swift b/packages/CallVideoLayer/Sources/MetalCallVideoView.swift index e45ff50fa..612ba567b 100644 --- a/packages/CallVideoLayer/Sources/MetalCallVideoView.swift +++ b/packages/CallVideoLayer/Sources/MetalCallVideoView.swift @@ -16,7 +16,7 @@ private func resolveVideoRotationAngle(angle: Float, followsDeviceOrientation: B } -public class MetalCallVideoView : LayerBackedView { +public class MetalCallVideoView : Control { public struct VideoMetrics: Equatable { public var resolution: CGSize @@ -44,6 +44,10 @@ public class MetalCallVideoView : LayerBackedView { videoLayer.isDoubleSided = false videoLayer.contentsGravity = .resizeAspect videoLayer.blurredLayer.contentsGravity = .resizeAspectFill + self.userInteractionEnabled = false + if #available(macOS 10.15, *) { + layer?.cornerCurve = .continuous + } } diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index df0c6a0ac..9162cd6ea 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -997,6 +997,8 @@ public final class L10n { public static var businessMessageSelectPeersChatTypes: String { return L10n.tr("Localizable", "Business.Message.SelectPeers.ChatTypes") } /// Add Quick Reply public static var businessQuickReplyAdd: String { return L10n.tr("Localizable", "Business.QuickReply.Add") } + /// Are you sure you want to delete quick reply? + public static var businessQuickReplyConfirmDelete: String { return L10n.tr("Localizable", "Business.QuickReply.ConfirmDelete") } /// Edit Name public static var businessQuickReplyEditName: String { return L10n.tr("Localizable", "Business.QuickReply.EditName") } /// Set up shortcuts with rich text and media to respond to messages faster. diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift index e0882f570..9bb270cd4 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift @@ -323,7 +323,6 @@ public final class PeerCallScreen : ViewController { if videoViewState.incomingView == nil { if let video = external.video(true) { let view = MetalVideoMakeView(videoStreamSignal: video) - view.background = NSColor.black.withAlphaComponent(0.9) view.videoMetricsDidUpdate = { [weak self] _ in guard let self else { return @@ -344,7 +343,6 @@ public final class PeerCallScreen : ViewController { if videoViewState.outgoingView == nil { if let video = external.video(false) { let view = MetalVideoMakeView(videoStreamSignal: video) - view.background = NSColor.black.withAlphaComponent(0.9) view.videoMetricsDidUpdate = { [weak self] _ in guard let self else { return diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift index 7df693383..2e1f37950 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift @@ -14,6 +14,63 @@ import TGUIKit import MetalEngine import AppKit + + +private class ShadowView: View { + + + public override init() { + super.init(frame: .zero) + setup() + } + public required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setup() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var gradient: CAGradientLayer { + return self.layer as! CAGradientLayer + } + + private func setup() { + self.layer = CAGradientLayer() + update() + } + + + private func update() { + let baseGradientAlpha: CGFloat = 1.0 + let numSteps = 8 + let firstStep = 1 + let firstLocation: CGFloat = 0 + let color = NSColor.black.withAlphaComponent(0.6) + self.gradient.colors = (0 ..< numSteps).map { i in + if i < firstStep { + return color.cgColor + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + let value: CGFloat = 1.0 - bezierPoint(0.42, 0.0, 0.58, 1.0, step) + return color.withAlphaComponent(baseGradientAlpha * value).cgColor + } + } + + self.gradient.locations = (0 ..< numSteps).map { i -> NSNumber in + if i < firstStep { + return 0.0 as NSNumber + } else { + let step: CGFloat = CGFloat(i - firstStep) / CGFloat(numSteps - firstStep - 1) + return (firstLocation + (1.0 - firstLocation) * step) as NSNumber + } + } + } +} + + + private final class InfoHelpView : NSVisualEffectView { private var shimmer: ShimmerLayer? @@ -89,16 +146,23 @@ struct PeerCallVideoViewState { var smallVideoSize: NSSize { if let outgoingView, let metrics = outgoingView.videoMetrics { - let targetSize = NSMakeSize(180, 180) + let targetSize = NSMakeSize(190, 190) return metrics.resolution.aspectFitted(targetSize) } - return NSMakeSize(180, 100) + return NSMakeSize(190, 100) } } +private enum VideoMagnify { + case topLeft + case topRight + case bottomLeft + case bottomRight +} + final class PeerCallScreenView : Control { private let backgroundLayer: CallBackgroundLayer = .init() - private let photoView = PeerCallPhotoView(frame: NSMakeRect(0, 0, 120, 120)) + private var photoView: PeerCallPhotoView? private let statusView = PeerCallStatusView(frame: NSMakeRect(0, 0, 300, 58)) @@ -123,8 +187,14 @@ final class PeerCallScreenView : Control { private var actionsViews: [PeerCallActionView] = [] private var actionsList: [PeerCallAction] = [] - private weak var videoLink_incoming:NSView? - private weak var videoLink_outgoing:NSView? + private weak var videoLink_incoming:MetalCallVideoView? + private weak var videoLink_outgoing:MetalCallVideoView? + + private var videoShadowView: ShadowView? + private var videoBackgroundView: NSVisualEffectView? + private var videoBackgroundView_color: View? + + private var videoMagnify: VideoMagnify = .bottomRight required init(frame frameRect: NSRect) { @@ -132,10 +202,11 @@ final class PeerCallScreenView : Control { self.layer?.addSublayer(backgroundLayer) self.layer?.addSublayer(backgroundLayer.blurredLayer) - addSubview(photoView) - addSubview(statusView) addSubview(actions) + + addSubview(statusView) + actions.layer?.masksToBounds = false @@ -145,7 +216,72 @@ final class PeerCallScreenView : Control { override func viewDidMoveToWindow() { super.viewDidMoveToWindow() self.backgroundLayer.isInHierarchy = window != nil - self.photoView.blobView.maskLayer = backgroundLayer.blurredLayer + } + + override func viewWillMove(toWindow newWindow: NSWindow?) { + if let window = newWindow as? Window { + var start: NSPoint? = nil + + window.set(mouseHandler: { [weak self] event in + guard let self, let window = self.window, let control = self.videoLink_outgoing else { + return .rejected + } + let startPoint = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) + + if NSPointInRect(startPoint, control.frame) { + start = startPoint + } else { + start = nil + } + return .rejected + }, with: self, for: .leftMouseDown) + + window.set(mouseHandler: { [weak self] event in + + guard let self, let startPoint = start, let control = self.videoLink_outgoing else { + return .rejected + } + control.isMoving = true + + let current = self.convert(event.locationInWindow, from: nil) + let difference = current - startPoint + + control.setFrameOrigin(control.frame.origin + difference) + start = current + + return .rejected + + }, with: self, for: .leftMouseDragged) + + window.set(mouseHandler: { [ weak self] event in + guard let self, let control = self.videoLink_outgoing, let _ = start else { + return .rejected + } + let current = self.convert(event.locationInWindow, from: nil) + start = nil + control.isMoving = false + + if current.x < frame.width / 2 { + if current.y > frame.height / 2 { + self.videoMagnify = .bottomLeft + } else { + self.videoMagnify = .topLeft + } + } else { + if current.y > frame.height / 2 { + self.videoMagnify = .bottomRight + } else { + self.videoMagnify = .topRight + } + } + self.updateLayout(size: self.frame.size, transition: .animated(duration: 0.35, curve: .spring)) + + return .rejected + }, with: self, for: .leftMouseUp) + + } else { + _window?.removeAllHandlers(for: self) + } } override var mouseDownCanMoveWindow: Bool { @@ -172,32 +308,47 @@ final class PeerCallScreenView : Control { self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(size.width), height: Int(size.height)), edgeInset: 0) + if let photoView { + transition.updateFrame(view: photoView, frame: photoView.centerFrameX(y: floorToScreenPixels(size.height / 3) - 50)) + photoView.updateLayout(size: photoView.frame.size, transition: transition) + } - transition.updateFrame(view: photoView, frame: photoView.centerFrameX(y: floorToScreenPixels(size.height / 3) - 50)) - photoView.updateLayout(size: photoView.frame.size, transition: transition) + if videoLink_incoming != nil { + transition.updateFrame(view: statusView, frame: statusView.centerFrameX(y: 10)) + statusView.updateLayout(size: statusView.frame.size, transition: transition) + } else if let photoView { + transition.updateFrame(view: statusView, frame: statusView.centerFrameX(y: photoView.frame.maxY + 32)) + statusView.updateLayout(size: statusView.frame.size, transition: transition) + } - transition.updateFrame(view: statusView, frame: statusView.centerFrameX(y: photoView.frame.maxY + 32)) - statusView.updateLayout(size: statusView.frame.size, transition: transition) if let videoViewState { if let incomingVideoView = videoLink_incoming { - transition.updateFrame(view: incomingVideoView, frame: size.bounds) + transition.updateFrame(view: incomingVideoView, frame: incomingVideoFrame(view: incomingVideoView, size: size, state: videoViewState)) } - if let outgointVideoView = videoLink_outgoing { - let videoSize = videoViewState.smallVideoSize - transition.updateFrame(view: outgointVideoView, frame: CGRect(origin: NSMakePoint(size.width - videoSize.width - 10, size.height - videoSize.height - 10), size: videoSize)) + if let outgointVideoView = videoLink_outgoing, !outgointVideoView.isMoving { + transition.updateFrame(view: outgointVideoView, frame: outgoingVideoFrame(view: outgointVideoView, size: size, videoMagnify: self.videoMagnify, state: videoViewState)) } } + if let videoShadowView { + transition.updateFrame(view: videoShadowView, frame: CGRect(origin: videoShadowView.frame.origin, size: NSMakeSize(size.width, videoShadowView.frame.height))) + } + if let videoBackgroundView { + transition.updateFrame(view: videoBackgroundView, frame: size.bounds) + } + if let videoBackgroundView_color { + transition.updateFrame(view: videoBackgroundView_color, frame: size.bounds) + } if let statusTooltip { transition.updateFrame(view: statusTooltip, frame: statusTooltipFrame(view: statusTooltip, state: state)) } if let secretView { - transition.updateFrame(view: secretView, frame: secretKeyFrame(view: secretView, state: state)) + transition.updateFrame(view: secretView, frame: secretKeyFrame(view: secretView, state: state, incomingVideo: videoLink_incoming != nil)) secretView.updateLayout(size: secretView.frame.size, transition: transition) } @@ -236,17 +387,26 @@ final class PeerCallScreenView : Control { self.arguments = arguments self.videoViewState = videoViewState - self.photoView.updateState(state, arguments: arguments, transition: transition) self.statusView.updateState(state, arguments: arguments, transition: transition) self.backgroundLayer.update(stateIndex: state.stateIndex, isEnergySavingEnabled: false, transition: transition) - var videos: [NSView] = [] + + + if let incomingView = videoViewState.incomingView { if videoViewState.incomingInited { - videos.append(incomingView) + addSubview(incomingView, positioned: .below, relativeTo: self.videoLink_outgoing ?? actions) + + if transition.isAnimated, videoLink_incoming == nil { + ContainedViewLayoutTransition.immediate.updateFrame(view: incomingView, frame: incomingVideoFrame(view: incomingView, size: frame.size, state: videoViewState)) + incomingView.layer?.animateAlpha(from: 0, to: 1, duration: transition.duration, timingFunction: transition.timingFunction) + } + videoLink_incoming = incomingView + + } else { + videoLink_incoming = nil } - videoLink_incoming = incomingView } else if let view = self.videoLink_incoming { performSubviewRemoval(view, animated: transition.isAnimated) self.videoLink_incoming = nil @@ -254,24 +414,98 @@ final class PeerCallScreenView : Control { if let outgoingView = videoViewState.outgoingView { if videoViewState.outgoingInited { - videos.append(outgoingView) + addSubview(outgoingView, positioned: .below, relativeTo: actions) + outgoingView.layer?.cornerRadius = 10 + outgoingView.userInteractionEnabled = true + + + if transition.isAnimated, videoLink_outgoing == nil { + ContainedViewLayoutTransition.immediate.updateFrame(view: outgoingView, frame: outgoingVideoFrame(view: outgoingView, size: frame.size, videoMagnify: self.videoMagnify, state: videoViewState)) + outgoingView.layer?.animateAlpha(from: 0, to: 1, duration: transition.duration, timingFunction: transition.timingFunction) + } + + videoLink_outgoing = outgoingView + + } else { + videoLink_outgoing = nil } - videoLink_outgoing = outgoingView } else if let view = self.videoLink_outgoing { performSubviewRemoval(view, animated: transition.isAnimated) self.videoLink_outgoing = nil } - CATransaction.begin() - for video in videos { - video.removeFromSuperview() + if videoLink_incoming != nil { + let current: ShadowView + if let view = self.videoShadowView { + current = view + } else { + current = ShadowView(frame: NSMakeRect(0, -50, frame.width, 160)) + addSubview(current, positioned: .below, relativeTo: self.statusView) + self.videoShadowView = current } - if let index = self.subviews.firstIndex(of: self.actions) { - self.subviews.insert(contentsOf: videos, at: index) + } else if let videoShadowView { + performSubviewRemoval(videoShadowView, animated: transition.isAnimated) + self.videoShadowView = nil + } + + if let videoLink_incoming { + let current: View + if let view = self.videoBackgroundView_color { + current = view + } else { + current = View(frame: self.bounds) + addSubview(current, positioned: .below, relativeTo: videoLink_incoming) + self.videoBackgroundView_color = current } - CATransaction.commit() + current.backgroundColor = NSColor.black.withAlphaComponent(0.7) + } else if let videoBackgroundView_color { + performSubviewRemoval(videoBackgroundView_color, animated: transition.isAnimated) + self.videoBackgroundView_color = nil + } + + if let videoLink_incoming { + let current: NSVisualEffectView + if let view = self.videoBackgroundView { + current = view + } else { + current = NSVisualEffectView(frame: self.bounds) + current.material = .dark + current.state = .active + current.blendingMode = .withinWindow + current.wantsLayer = true + addSubview(current, positioned: .below, relativeTo: videoLink_incoming) + self.videoBackgroundView = current + } + } else if let videoBackgroundView { + performSubviewRemoval(videoBackgroundView, animated: transition.isAnimated) + self.videoBackgroundView = nil + } + + if videoLink_incoming == nil { + let current: PeerCallPhotoView + if let view = self.photoView { + current = view + } else { + current = PeerCallPhotoView(frame: NSMakeRect(0, 0, 120, 120)) + addSubview(current, positioned: .below, relativeTo: self.statusView) + self.photoView = current + + ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: current.centerFrameX(y: floorToScreenPixels(frame.height / 3) - 50)) + + if transition.isAnimated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + current.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) + } + } + current.blobView.maskLayer = backgroundLayer.blurredLayer + current.updateState(state, arguments: arguments, transition: transition) + } else if let photoView { + performSubviewRemoval(photoView, animated: transition.isAnimated, scale: true) + self.photoView = nil + } + if let tooltip = state.statusTooltip { @@ -284,7 +518,7 @@ final class PeerCallScreenView : Control { let current: InfoHelpView let isNew = true current = InfoHelpView(frame: .zero) - self.addSubview(current, positioned: .below, relativeTo: subviews.first) + self.addSubview(current) self.statusTooltip = current current.set(string: tooltip, hasShimm: true) @@ -369,7 +603,7 @@ final class PeerCallScreenView : Control { current.updateState(state, arguments: arguments, transition: isNew ? .immediate : transition) if isNew { - ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: secretKeyFrame(view: current, state: state)) + ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: secretKeyFrame(view: current, state: state, incomingVideo: videoLink_incoming != nil)) if transition.isAnimated { current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) current.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) @@ -409,7 +643,7 @@ final class PeerCallScreenView : Control { for view in self.tooltipsViews { view.removeFromSuperview() } - self.subviews.insert(contentsOf: self.tooltipsViews, at: 0) + self.subviews.append(contentsOf: self.tooltipsViews) CATransaction.commit() self.tooltips = tooltips @@ -469,18 +703,41 @@ final class PeerCallScreenView : Control { //RECT -extension PeerCallScreenView { +private extension PeerCallScreenView { func statusTooltipFrame(view: NSView, state: PeerCallState) -> NSRect { return view.centerFrameX(y: statusView.frame.maxY + 12) } - func secretKeyFrame(view: NSView, state: PeerCallState) -> NSRect { + func outgoingVideoFrame(view: MetalCallVideoView, size: NSSize, videoMagnify: VideoMagnify, state: PeerCallVideoViewState) -> NSRect { + let videoSize = state.smallVideoSize + let point: NSPoint + switch videoMagnify { + case .topLeft: + point = NSMakePoint(10, 10) + case .topRight: + point = NSMakePoint(size.width - videoSize.width - 10, 10) + case .bottomLeft: + point = NSMakePoint(10, size.height - videoSize.height - 10) + case .bottomRight: + point = NSMakePoint(size.width - videoSize.width - 10, size.height - videoSize.height - 10) + } + return CGRect(origin: point, size: videoSize) + } + func incomingVideoFrame(view: MetalCallVideoView, size: NSSize, state: PeerCallVideoViewState) -> NSRect { + return size.bounds + } + func secretKeyFrame(view: NSView, state: PeerCallState, incomingVideo: Bool) -> NSRect { if state.secretKeyViewState == .revealed { var rect = focus(NSMakeSize(200, 50)) rect.origin.y -= 30 return rect } else { var rect = focus(NSMakeSize(100, 25)) - rect.origin.y = 16 + if incomingVideo { + rect.origin.y = 16 + rect.origin.x = frame.width - rect.width - 25 + } else { + rect.origin.y = 16 + } return rect } } diff --git a/packages/TGUIKit/Sources/AppMenuController.swift b/packages/TGUIKit/Sources/AppMenuController.swift index 95e9d6adc..30a784591 100644 --- a/packages/TGUIKit/Sources/AppMenuController.swift +++ b/packages/TGUIKit/Sources/AppMenuController.swift @@ -689,7 +689,7 @@ final class AppMenuController : NSObject { for (_, window) in windows { window.view.tableView.enumerateViews(with: { view in - view.updateMouse() + view.updateMouse(animated: true) return true }) } @@ -768,7 +768,7 @@ final class AppMenuController : NSObject { } if let active = self.activeMenu, active.submenuId != nil { - if active.parentView == parentView, let window = active.kitWindow { + if active.parentView == parentView, let window = active._window { self.cancelSubmenuNow(window) parentView.view.childView = nil } diff --git a/packages/TGUIKit/Sources/AppMenuRowItem.swift b/packages/TGUIKit/Sources/AppMenuRowItem.swift index f62eae78f..5aaf649fa 100644 --- a/packages/TGUIKit/Sources/AppMenuRowItem.swift +++ b/packages/TGUIKit/Sources/AppMenuRowItem.swift @@ -439,8 +439,8 @@ open class AppMenuRowView: AppMenuBasicItemView { NSCursor.arrow.set() } - open override func updateMouse() { - super.updateMouse() + open override func updateMouse(animated: Bool) { + super.updateMouse(animated: animated) updateColors() containerView.updateState() } diff --git a/packages/TGUIKit/Sources/Control.swift b/packages/TGUIKit/Sources/Control.swift index a646d38fd..52a2a63e7 100644 --- a/packages/TGUIKit/Sources/Control.swift +++ b/packages/TGUIKit/Sources/Control.swift @@ -120,7 +120,7 @@ open class Control: View { private var mouseMovedInside: Bool = true private var longInvoked: Bool = false public var handleLongEvent: Bool = true - + public var isMoving: Bool = false public var scaleOnClick: Bool = false open override var backgroundColor: NSColor { @@ -651,7 +651,7 @@ open class Control: View { } open override func becomeFirstResponder() -> Bool { - if let window = kitWindow { + if let window = _window { return window.makeFirstResponder(self) } return false diff --git a/packages/TGUIKit/Sources/NavigationModalView.swift b/packages/TGUIKit/Sources/NavigationModalView.swift index 33908229c..c9649e0ee 100644 --- a/packages/TGUIKit/Sources/NavigationModalView.swift +++ b/packages/TGUIKit/Sources/NavigationModalView.swift @@ -75,7 +75,7 @@ class NavigationModalView: Control { func close() { - kitWindow?.removeAllHandlers(for: self) + _window?.removeAllHandlers(for: self) self.lock = true self.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion:false, completion:{[weak self] (completed) in @@ -84,18 +84,18 @@ class NavigationModalView: Control { } override func removeFromSuperview() { - kitWindow?.removeAllHandlers(for: self) + _window?.removeAllHandlers(for: self) super.removeFromSuperview() } override func viewDidMoveToWindow() { if window != nil { self.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) - self.kitWindow?.set(escape: { [weak self] _ -> KeyHandlerResult in + self._window?.set(escape: { [weak self] _ -> KeyHandlerResult in self?.close() return .invoked }, with: self, priority: .high) - self.kitWindow?.set(responder: { () -> NSResponder? in + self._window?.set(responder: { () -> NSResponder? in return nil }, with: self, priority: .modal) } else { diff --git a/packages/TGUIKit/Sources/Popover.swift b/packages/TGUIKit/Sources/Popover.swift index 9ee59f7a8..d0aebdb85 100644 --- a/packages/TGUIKit/Sources/Popover.swift +++ b/packages/TGUIKit/Sources/Popover.swift @@ -153,7 +153,7 @@ open class Popover: NSObject { controller.loadViewIfNeeded() controller.viewWillAppear(animates) - self.window = control.kitWindow + self.window = control._window NotificationCenter.default.addObserver(self, selector: #selector(windowDidResized(_:)), name: NSWindow.didResizeNotification, object: window) diff --git a/packages/TGUIKit/Sources/SPopoverRowItem.swift b/packages/TGUIKit/Sources/SPopoverRowItem.swift index a9003544f..0a64de7ca 100644 --- a/packages/TGUIKit/Sources/SPopoverRowItem.swift +++ b/packages/TGUIKit/Sources/SPopoverRowItem.swift @@ -116,7 +116,7 @@ private class SPopoverRowView: TableRowView { } - override func updateMouse() { + override func updateMouse(animated: Bool) { overlay.updateState() } diff --git a/packages/TGUIKit/Sources/SearchView.swift b/packages/TGUIKit/Sources/SearchView.swift index 0adcf6393..35c370ce0 100644 --- a/packages/TGUIKit/Sources/SearchView.swift +++ b/packages/TGUIKit/Sources/SearchView.swift @@ -475,8 +475,8 @@ open class SearchView: OverlayControl, NSTextViewDelegate { change(state: .None, true) } - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) + self._window?.removeAllHandlers(for: self) + self._window?.removeObserver(for: self) } open func didBecomeResponder() { @@ -486,7 +486,7 @@ open class SearchView: OverlayControl, NSTextViewDelegate { change(state: .Focus, true) - self.kitWindow?.set(escape: { [weak self] _ -> KeyHandlerResult in + self._window?.set(escape: { [weak self] _ -> KeyHandlerResult in if let strongSelf = self { strongSelf.setString("") return strongSelf.changeResponder() ? .invoked : .rejected @@ -495,21 +495,21 @@ open class SearchView: OverlayControl, NSTextViewDelegate { }, with: self, priority: .modal) - self.kitWindow?.set(handler: { [weak self] _ -> KeyHandlerResult in + self._window?.set(handler: { [weak self] _ -> KeyHandlerResult in if self?.state == .Focus { return .invokeNext } return .rejected }, with: self, for: .RightArrow, priority: .modal) - self.kitWindow?.set(handler: { [weak self] _ -> KeyHandlerResult in + self._window?.set(handler: { [weak self] _ -> KeyHandlerResult in if self?.state == .Focus { return .invokeNext } return .rejected }, with: self, for: .LeftArrow, priority: .modal) - self.kitWindow?.set(responder: {[weak self] () -> NSResponder? in + self._window?.set(responder: {[weak self] () -> NSResponder? in return self?.input }, with: self, priority: .modal) } @@ -555,8 +555,8 @@ open class SearchView: OverlayControl, NSTextViewDelegate { if state == .None { updateTags([], presentation.search.searchImage) - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) + self._window?.removeAllHandlers(for: self) + self._window?.removeObserver(for: self) self.input.isHidden = true inputContainer.isHidden = true @@ -584,8 +584,8 @@ open class SearchView: OverlayControl, NSTextViewDelegate { if isEmpty { change(state: .None, false) } - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) + self._window?.removeAllHandlers(for: self) + self._window?.removeObserver(for: self) } } @@ -726,8 +726,8 @@ open class SearchView: OverlayControl, NSTextViewDelegate { } deinit { - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) + self._window?.removeAllHandlers(for: self) + self._window?.removeObserver(for: self) } public var query:String { diff --git a/packages/TGUIKit/Sources/ShadowView.swift b/packages/TGUIKit/Sources/ShadowView.swift index d98c16515..a7512d285 100644 --- a/packages/TGUIKit/Sources/ShadowView.swift +++ b/packages/TGUIKit/Sources/ShadowView.swift @@ -72,3 +72,4 @@ open class ShadowView: View { } } } + diff --git a/packages/TGUIKit/Sources/TableRowView.swift b/packages/TGUIKit/Sources/TableRowView.swift index c509a36cc..de6a55585 100644 --- a/packages/TGUIKit/Sources/TableRowView.swift +++ b/packages/TGUIKit/Sources/TableRowView.swift @@ -183,15 +183,15 @@ open class TableRowView: NSTableRowView, CALayerDelegate { open override func mouseMoved(with event: NSEvent) { super.mouseMoved(with: event) - updateMouse() + updateMouse(animated: true) } open override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) - updateMouse() + updateMouse(animated: true) } open override func mouseExited(with event: NSEvent) { super.mouseMoved(with: event) - updateMouse() + updateMouse(animated: true) } open override func mouseDown(with event: NSEvent) { @@ -322,7 +322,7 @@ open class TableRowView: NSTableRowView, CALayerDelegate { fatalError("init(coder:) has not been implemented") } - open func updateMouse() { + open func updateMouse(animated: Bool) { } diff --git a/packages/TGUIKit/Sources/TableView.swift b/packages/TGUIKit/Sources/TableView.swift index 1f88d0913..51a02f852 100644 --- a/packages/TGUIKit/Sources/TableView.swift +++ b/packages/TGUIKit/Sources/TableView.swift @@ -1151,7 +1151,7 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele let range = self.visibleRows() for i in range.location ..< range.location + range.length { if let view = self.viewNecessary(at: i) { - view.updateMouse() + view.updateMouse(animated: false) } } } @@ -1997,17 +1997,19 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele self.tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row)) return + } else { + if let item = self.optionalItem(at: row) { + let animated = visibleRows().contains(row) && item.view != nil && animated + NSAnimationContext.current.duration = animated ? duration : 0.0 + NSAnimationContext.current.timingFunction = CAMediaTimingFunction(name: .easeOut) + self.tableView.beginUpdates() + self.tableView.removeRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none]) + self.tableView.insertRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none]) + self.tableView.endUpdates() + } } } - if let item = self.optionalItem(at: row) { - let animated = visibleRows().contains(row) && item.view != nil && animated - NSAnimationContext.current.duration = animated ? duration : 0.0 - NSAnimationContext.current.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.tableView.beginUpdates() - self.tableView.removeRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none]) - self.tableView.insertRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none]) - self.tableView.endUpdates() - } + } @@ -2817,9 +2819,7 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele continue } - self.tableView.beginUpdates() - self.tableView.reloadData() - self.tableView.endUpdates() + nrect = rectOf(item: item) @@ -3190,7 +3190,6 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele } super.change(size: size, animated: animated, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - self.tile() self.updateStickAfterScroll(animated) } diff --git a/packages/TGUIKit/Sources/TextView.swift b/packages/TGUIKit/Sources/TextView.swift index cff9bb085..1b32dc1e3 100644 --- a/packages/TGUIKit/Sources/TextView.swift +++ b/packages/TGUIKit/Sources/TextView.swift @@ -2701,7 +2701,7 @@ public class TextView: Control, NSViewToolTipOwner, ViewDisplayDelegate { menu.addItem(copy) - if resolved == nil, let window = self?.kitWindow { + if resolved == nil, let window = self?._window { if let text = self?.effectiveText, let translate = layout.interactions.translate?(text, window) { menu.addItem(translate) } @@ -3000,7 +3000,7 @@ public class TextView: Control, NSViewToolTipOwner, ViewDisplayDelegate { self.locationInWindow = nil - if let layout = textLayout, userInteractionEnabled, let window = kitWindow { + if let layout = textLayout, userInteractionEnabled, let window = _window { let point = self.convert(event.locationInWindow, from: nil) if let _ = layout.spoiler(at: point) { layout.revealSpoiler() diff --git a/packages/TGUIKit/Sources/View.swift b/packages/TGUIKit/Sources/View.swift index c59e86753..5678cdfc1 100644 --- a/packages/TGUIKit/Sources/View.swift +++ b/packages/TGUIKit/Sources/View.swift @@ -573,7 +573,7 @@ open class View : NSView, CALayerDelegate, AppearanceViewProtocol { } - open var kitWindow: Window? { + open var _window: Window? { return super.window as? Window } From 2717420dd1bd0ce040295df14885bbe09f8387fe Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 5 Mar 2024 19:02:46 +0400 Subject: [PATCH 39/50] - bugfixes --- Telegram-Mac/ChatRowView.swift | 6 +++--- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Telegram-Mac/ChatRowView.swift b/Telegram-Mac/ChatRowView.swift index 2c1109692..b90047a2b 100644 --- a/Telegram-Mac/ChatRowView.swift +++ b/Telegram-Mac/ChatRowView.swift @@ -406,10 +406,10 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDeleg override func updateMouse(animated: Bool) { if let shareView = self.shareView, let item = item as? ChatRowItem { let active = item.chatInteraction.presentation.state != .selecting && mouseInsideRow() && contextMenu == nil ? 1.0 : 0.0 - shareView.change(opacity: active, animated: false) + shareView.change(opacity: active, animated: animated) } if let commentsView = self.channelCommentsBubbleSmallControl, let item = item as? ChatRowItem { - commentsView.change(opacity: item.chatInteraction.presentation.state != .selecting && mouseInsideRow() && contextMenu == nil ? 1.0 : 0.0, animated: false) + commentsView.change(opacity: item.chatInteraction.presentation.state != .selecting && mouseInsideRow() && contextMenu == nil ? 1.0 : 0.0, animated: animated) } } @@ -1377,7 +1377,7 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDeleg rowView.addSubview(shareView!) } - updateMouse(animated: false) + updateMouse(animated: animated) guard let control = shareView else {return} control.autohighlight = false diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index a75c0e5c3..3c934725a 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260552 + 260553 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 166bf77a9..7e96c7591 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260552 + 260553 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion From e73df22688e9c8afc7d08bf1ac17b32b092ea2ca Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 5 Mar 2024 19:11:47 +0400 Subject: [PATCH 40/50] - bugfixes --- submodules/telegram-ios | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/telegram-ios b/submodules/telegram-ios index ae998eb91..ebd7459fe 160000 --- a/submodules/telegram-ios +++ b/submodules/telegram-ios @@ -1 +1 @@ -Subproject commit ae998eb91ef05dfec5c5fa2dfa3c78371992a482 +Subproject commit ebd7459fe21cabe477522c57851886a186751086 From 4c1c5682749b85abcd695a0039908e7eb638a84e Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 5 Mar 2024 19:30:14 +0400 Subject: [PATCH 41/50] - bugfixes --- Telegram-Mac/BusinessMessageController.swift | 14 ++++++------ .../BusinessQuickReplyController.swift | 22 ++++++++++--------- Telegram-Mac/ChatController.swift | 6 +++-- .../ChatInterfaceStateContextQueries.swift | 2 +- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/InputContextHelper.swift | 2 +- TelegramShare/Info.plist | 2 +- .../Sources/PeerCallScreenView.swift | 4 ++++ 8 files changed, 31 insertions(+), 23 deletions(-) diff --git a/Telegram-Mac/BusinessMessageController.swift b/Telegram-Mac/BusinessMessageController.swift index 3ff444612..d05dc3fbb 100644 --- a/Telegram-Mac/BusinessMessageController.swift +++ b/Telegram-Mac/BusinessMessageController.swift @@ -335,7 +335,7 @@ private struct State : Equatable { } var mappedGreeting: TelegramBusinessGreetingMessage? { - if enabled, let shortcut = shortcut { + if enabled, let shortcut = shortcut, let shortcutId = shortcut.id { let peerIds: Set switch recepient { case .all: @@ -349,14 +349,14 @@ private struct State : Equatable { } let recepients: TelegramBusinessRecipients = .init(categories: mappedCategories, additionalPeers: peerIds, exclude: recepient == .all) - return .init(shortcutId: shortcut.id, recipients: recepients, inactivityDays: awayPeriod) + return .init(shortcutId: shortcutId, recipients: recepients, inactivityDays: awayPeriod) } else { return nil } } var mappedAway: TelegramBusinessAwayMessage? { - if enabled, let shortcut = shortcut { + if enabled, let shortcut = shortcut, let shortcutId = shortcut.id { let peerIds: Set switch recepient { case .all: @@ -369,7 +369,7 @@ private struct State : Equatable { }) } let recepients: TelegramBusinessRecipients = .init(categories: mappedCategories, additionalPeers: peerIds, exclude: recepient == .all) - return .init(shortcutId: shortcut.id, recipients: recepients, schedule: scheduleAway, sendWhenOffline: onlyOffline) + return .init(shortcutId: shortcutId, recipients: recepients, schedule: scheduleAway, sendWhenOffline: onlyOffline) } else { return nil } @@ -746,7 +746,7 @@ func BusinessMessageController(context: AccountContext, type: BusinessMessageTyp let awayMessage = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BusinessAwayMessage(id: context.peerId)) let greetingMessage = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BusinessGreetingMessage(id: context.peerId)) - let shortcuts = context.engine.accountData.shortcutMessageList() + let shortcuts = context.engine.accountData.shortcutMessageList(onlyRemote: false) actionsDisposable.add(combineLatest(awayMessage, greetingMessage, shortcuts).start(next: { awayMessage, greetingMessage, shortcuts in @@ -888,8 +888,8 @@ func BusinessMessageController(context: AccountContext, type: BusinessMessageTyp let messages = AutomaticBusinessMessageSetupChatContents(context: context, kind: type == .away ? .awayMessageInput : .greetingMessageInput, shortcutId: stateValue.with { $0.shortcut?.id }) context.bindings.rootNavigation().push(ChatAdditionController(context: context, chatLocation: .peer(context.peerId),mode: .customChatContents(contents: messages))) }, remove: { - if let shortcut = stateValue.with({ $0.shortcut }) { - context.engine.accountData.deleteMessageShortcuts(ids: [shortcut.id]) + if let shortcut = stateValue.with({ $0.shortcut }), let shortcutId = shortcut.id { + context.engine.accountData.deleteMessageShortcuts(ids: [shortcutId]) updateState { current in var current = current current.shortcut = nil diff --git a/Telegram-Mac/BusinessQuickReplyController.swift b/Telegram-Mac/BusinessQuickReplyController.swift index 2b7902e94..36bf586b4 100644 --- a/Telegram-Mac/BusinessQuickReplyController.swift +++ b/Telegram-Mac/BusinessQuickReplyController.swift @@ -430,8 +430,8 @@ private struct State : Equatable { private let _id_header = InputDataIdentifier("_id_header") private let _id_add = InputDataIdentifier("_id_add") -private func _id_reply(_ id: Int32) -> InputDataIdentifier { - return .init("_id_reply_\(id)") +private func _id_reply(_ id: ShortcutMessageList.Item) -> InputDataIdentifier { + return .init("_id_reply__\(id.shortcut)") } @@ -500,7 +500,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { } for tuple in tuples { - entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_reply(tuple.reply.id), equatable: .init(tuple), comparable: nil, item: { initialSize, stableId in + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_reply(tuple.reply), equatable: .init(tuple), comparable: nil, item: { initialSize, stableId in return QuickReplyRowItem(initialSize, stableId: stableId, reply: tuple.reply, context: arguments.context, editing: tuple.editing, viewType: tuple.viewType, open: arguments.open, editName: arguments.editName, remove: arguments.remove) })) } @@ -529,7 +529,7 @@ func BusinessQuickReplyController(context: AccountContext) -> InputDataControlle let nextTransactionNonAnimated = Atomic(value: false) - let shortcutList = context.engine.accountData.shortcutMessageList() + let shortcutList = context.engine.accountData.shortcutMessageList(onlyRemote: false) actionsDisposable.add(shortcutList.start(next: { value in updateState { current in @@ -546,9 +546,11 @@ func BusinessQuickReplyController(context: AccountContext) -> InputDataControlle }, edit: { reply in }, remove: { reply in - verifyAlert(for: context.window, information: strings().businessQuickReplyConfirmDelete, ok: strings().modalDelete, successHandler: { _ in - context.engine.accountData.deleteMessageShortcuts(ids: [reply.id]) - }) + if let shortcutId = reply.id { + verifyAlert(for: context.window, information: strings().businessQuickReplyConfirmDelete, ok: strings().modalDelete, successHandler: { _ in + context.engine.accountData.deleteMessageShortcuts(ids: [shortcutId]) + }) + } }, editName: { reply in showModal(with: BusinessAddQuickReply(context: context, actionsDisposable: actionsDisposable, stateSignal: statePromise.get(), stateValue: stateValue, updateState: updateState, reply: reply), for: context.window) }, open: { reply in @@ -610,7 +612,7 @@ func BusinessQuickReplyController(context: AccountContext) -> InputDataControlle replies.move(at: fromValue, to: toValue) _ = nextTransactionNonAnimated.swap(true) - context.engine.accountData.reorderMessageShortcuts(ids: replies.map { $0.id }, completion: { + context.engine.accountData.reorderMessageShortcuts(ids: replies.compactMap { $0.id }, completion: { }) @@ -721,10 +723,10 @@ private func BusinessAddQuickReply(context: AccountContext, actionsDisposable: D if reply == nil { let messages = AutomaticBusinessMessageSetupChatContents(context: context, kind: .quickReplyMessageInput(shortcut: value), shortcutId: nil) context.bindings.rootNavigation().push(ChatAdditionController(context: context, chatLocation: .peer(context.peerId),mode: .customChatContents(contents: messages))) - } else if let reply { + } else if let reply, let shortcutId = reply.id { let name = stateValue.with { $0.creatingName ?? "" } if !name.isEmpty { - context.engine.accountData.editMessageShortcut(id: reply.id, shortcut: name) + context.engine.accountData.editMessageShortcut(id: shortcutId, shortcut: name) } else { return .fail(.fields([_id_input : .shake])) } diff --git a/Telegram-Mac/ChatController.swift b/Telegram-Mac/ChatController.swift index 51e599b29..5e227f27c 100644 --- a/Telegram-Mac/ChatController.swift +++ b/Telegram-Mac/ChatController.swift @@ -3761,7 +3761,9 @@ class ChatController: EditableViewController, Notifable, Tab } chatInteraction.sendMessageShortcut = { item in - context.engine.accountData.sendMessageShortcut(peerId: peerId, id: item.id) + if let shortcutId = item.id { + context.engine.accountData.sendMessageShortcut(peerId: peerId, id: shortcutId) + } } chatInteraction.openProxySettings = { [weak self] in @@ -5569,7 +5571,7 @@ class ChatController: EditableViewController, Notifable, Tab let shortcuts: Signal if peerId.namespace == Namespaces.Peer.CloudUser { - shortcuts = context.engine.accountData.shortcutMessageList() |> map(Optional.init) + shortcuts = context.engine.accountData.shortcutMessageList(onlyRemote: true) |> map(Optional.init) } else { shortcuts = .single(nil) } diff --git a/Telegram-Mac/ChatInterfaceStateContextQueries.swift b/Telegram-Mac/ChatInterfaceStateContextQueries.swift index bae84a067..fb23f1e0b 100644 --- a/Telegram-Mac/ChatInterfaceStateContextQueries.swift +++ b/Telegram-Mac/ChatInterfaceStateContextQueries.swift @@ -262,7 +262,7 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() if chatPresentationInterfaceState.accountPeer?.isPremium == true { - signal = context.engine.accountData.shortcutMessageList() |> map { list in + signal = context.engine.accountData.shortcutMessageList(onlyRemote: true) |> map { list in let found = list.items.filter({ item in let normalized = item.shortcut.lowercased() return normalized.hasPrefix(normalizedQuery) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 3c934725a..64d12f211 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260553 + 260554 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/InputContextHelper.swift b/Telegram-Mac/InputContextHelper.swift index a49316b06..e3c0f1fe9 100644 --- a/Telegram-Mac/InputContextHelper.swift +++ b/Telegram-Mac/InputContextHelper.swift @@ -58,7 +58,7 @@ enum InputContextEntry : Comparable, Identifiable { case let .hashtag(hashtag, _): return Int64(hashtag.hashValue) case let .shortcut(shortcut, _, _): - return Int64(shortcut.id) + return Int64(shortcut.id ?? Int32(arc4random64())) case let .emoji(clue, _, _, _, _): return Int64(clue.joined().hashValue) case .inlineRestricted: diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 7e96c7591..e4fde2abf 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260553 + 260554 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift index 2e1f37950..29012af63 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift @@ -544,6 +544,10 @@ final class PeerCallScreenView : Control { current = view isNew = false } else { + if let index = self.subviews.firstIndex(of: secretView) { + self.subviews.remove(at: index) + self.subviews.append(secretView) + } current = PeerCallRevealedSecretKeyView(frame: NSMakeRect(0, 0, 300, 300)) self.addSubview(current, positioned: .below, relativeTo: secretView) self.revealedKey = current From 5af6fdc0e380b98af07ecf9f9643b0f19a005271 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Tue, 5 Mar 2024 19:56:14 +0400 Subject: [PATCH 42/50] - bugfixes --- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- .../Sources/PeerCallScreen.swift | 5 +- .../Sources/PeerCallScreenView.swift | 161 +++++++++--------- 4 files changed, 87 insertions(+), 83 deletions(-) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 64d12f211..5123af136 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260554 + 260557 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index e4fde2abf..a73eb959d 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260554 + 260557 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift index 9bb270cd4..dae3ed46f 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift @@ -57,6 +57,7 @@ public final class PeerCallScreen : ViewController { self.screen.backgroundColor = .black self.screen.titlebarAppearsTransparent = true self.screen.isMovableByWindowBackground = true + self.screen.isReleasedWhenClosed = false } else { fatalError("screen not found") } @@ -384,7 +385,6 @@ public final class PeerCallScreen : ViewController { public func show() { - screen.contentView = view if !screen.isOnActiveSpace { self.screen.alphaValue = 0 @@ -399,6 +399,9 @@ public final class PeerCallScreen : ViewController { self.screen.orderFrontRegardless() } + screen.contentView = view + + self.genericView.updateLayout(size: screen.frame.size, transition: .immediate) } diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift index 29012af63..1cd183f22 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift @@ -535,6 +535,87 @@ final class PeerCallScreenView : Control { self.statusTooltip = nil } + + do { + var tooltips:[PeerCallTooltipStatusView.TooltipType] = [] + + if state.externalState.isMuted, state.isActive { + tooltips.append(.yourMicroOff) + } + if state.externalState.remoteAudioState == .muted, state.isActive { + tooltips.append(.microOff(state.compactTitle)) + } + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.tooltips, rightList: tooltips) + + for deleteIndex in deleteIndices.reversed() { + let view = self.tooltipsViews.remove(at: deleteIndex) + performSubviewRemoval(view, animated: transition.isAnimated, scale: true) + } + for indicesAndItem in indicesAndItems { + let view = PeerCallTooltipStatusView(frame: .zero) + view.set(type: indicesAndItem.1) + tooltipsViews.insert(view, at: indicesAndItem.0) + } + for updateIndex in updateIndices { + let view = self.tooltipsViews[updateIndex.0] + view.set(type: updateIndex.1) + } + CATransaction.begin() + for view in self.tooltipsViews { + view.removeFromSuperview() + } + self.subviews.append(contentsOf: self.tooltipsViews) + CATransaction.commit() + + self.tooltips = tooltips + } + + do { + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.actionsList, rightList: state.actions) + + var deletedViews: [Int: PeerCallActionView] = [:] + + for deleteIndex in deleteIndices.reversed() { + let view = self.actionsViews.remove(at: deleteIndex) + deletedViews[deleteIndex] = view + } + for indicesAndItem in indicesAndItems { + let previous: PeerCallActionView? + if let previousIndex = indicesAndItem.2 { + previous = deletedViews[previousIndex] + deletedViews.removeValue(forKey: previousIndex) + } else { + previous = nil + } + let view = previous ?? PeerCallActionView() + view.setFrameOrigin(actions.focus(view.frame.size).origin) + view.update(indicesAndItem.1, animated: false) + actionsViews.insert(view, at: indicesAndItem.0) + if transition.isAnimated, indicesAndItem.2 == nil { + view.layer?.animateAlpha(from: 0, to: indicesAndItem.1.enabled ? 1.0 : 0.7, duration: 0.2) + view.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) + } + } + for updateIndex in updateIndices { + let view = self.actionsViews[updateIndex.0] + view.update(updateIndex.1, animated: transition.isAnimated) + } + + for (_, view) in deletedViews { + performSubviewRemoval(view, animated: transition.isAnimated, scale: true) + } + + CATransaction.begin() + for view in self.actionsViews { + view.removeFromSuperview() + } + self.actions.subviews.insert(contentsOf: self.actionsViews, at: 0) + CATransaction.commit() + + self.actionsList = state.actions + } if state.secretKeyViewState == .revealed, let secretView { @@ -618,86 +699,6 @@ final class PeerCallScreenView : Control { self.secretView = nil } - do { - var tooltips:[PeerCallTooltipStatusView.TooltipType] = [] - - if state.externalState.isMuted, state.isActive { - tooltips.append(.yourMicroOff) - } - if state.externalState.remoteAudioState == .muted, state.isActive { - tooltips.append(.microOff(state.compactTitle)) - } - - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.tooltips, rightList: tooltips) - - for deleteIndex in deleteIndices.reversed() { - let view = self.tooltipsViews.remove(at: deleteIndex) - performSubviewRemoval(view, animated: transition.isAnimated, scale: true) - } - for indicesAndItem in indicesAndItems { - let view = PeerCallTooltipStatusView(frame: .zero) - view.set(type: indicesAndItem.1) - tooltipsViews.insert(view, at: indicesAndItem.0) - } - for updateIndex in updateIndices { - let view = self.tooltipsViews[updateIndex.0] - view.set(type: updateIndex.1) - } - CATransaction.begin() - for view in self.tooltipsViews { - view.removeFromSuperview() - } - self.subviews.append(contentsOf: self.tooltipsViews) - CATransaction.commit() - - self.tooltips = tooltips - } - - do { - - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.actionsList, rightList: state.actions) - - var deletedViews: [Int: PeerCallActionView] = [:] - - for deleteIndex in deleteIndices.reversed() { - let view = self.actionsViews.remove(at: deleteIndex) - deletedViews[deleteIndex] = view - } - for indicesAndItem in indicesAndItems { - let previous: PeerCallActionView? - if let previousIndex = indicesAndItem.2 { - previous = deletedViews[previousIndex] - deletedViews.removeValue(forKey: previousIndex) - } else { - previous = nil - } - let view = previous ?? PeerCallActionView() - view.setFrameOrigin(actions.focus(view.frame.size).origin) - view.update(indicesAndItem.1, animated: false) - actionsViews.insert(view, at: indicesAndItem.0) - if transition.isAnimated, indicesAndItem.2 == nil { - view.layer?.animateAlpha(from: 0, to: indicesAndItem.1.enabled ? 1.0 : 0.7, duration: 0.2) - view.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) - } - } - for updateIndex in updateIndices { - let view = self.actionsViews[updateIndex.0] - view.update(updateIndex.1, animated: transition.isAnimated) - } - - for (_, view) in deletedViews { - performSubviewRemoval(view, animated: transition.isAnimated, scale: true) - } - - CATransaction.begin() - for view in self.actionsViews { - view.removeFromSuperview() - } - self.actions.subviews.insert(contentsOf: self.actionsViews, at: 0) - CATransaction.commit() - - self.actionsList = state.actions - } } } From d07f523f609bc0e2c5dc0208815a01f3df1a16ad Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 6 Mar 2024 15:00:42 +0400 Subject: [PATCH 43/50] - bugfixes --- Telegram-Mac/ApplicationContext.swift | 2 +- Telegram-Mac/BusinessHoursController.swift | 178 ++++++++------ .../BusinessTimezonesController.swift | 140 +++++++++++ .../DateSelectorModalController.swift | 2 +- Telegram-Mac/Info.plist | 2 +- .../SelectCountryModalController.swift | 1 - .../TimeRangeSelectorController.swift | 229 ++++++++++++++++++ Telegram.xcodeproj/project.pbxproj | 8 + TelegramShare/Info.plist | 2 +- .../Sources/PeerCallScreenView.swift | 2 +- packages/TGUIKit/Sources/DatePicker.swift | 42 ++-- 11 files changed, 518 insertions(+), 90 deletions(-) create mode 100644 Telegram-Mac/BusinessTimezonesController.swift create mode 100644 Telegram-Mac/TimeRangeSelectorController.swift diff --git a/Telegram-Mac/ApplicationContext.swift b/Telegram-Mac/ApplicationContext.swift index ba27a51ae..2c29c9460 100644 --- a/Telegram-Mac/ApplicationContext.swift +++ b/Telegram-Mac/ApplicationContext.swift @@ -526,7 +526,7 @@ final class AuthorizedApplicationContext: NSObject, SplitViewDelegate { #if DEBUG // var peerCall: PeerCallScreen? self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in - +// showModal(with: TimeRangeSelectorController(context: context, from: .init(hours: 1, minutes: 30), to: .init(hours: 22, minutes: 30), title: "Monday", ok: "Save", fromString: "Opening Time", toString: "Closing Time"), for: context.window) // peerCall = PeerCallScreen(external: PeerCallArguments(engine: context.engine, peerId: context.peerId, makeAvatar: { view, peer in // let control = view as? AvatarControl ?? AvatarControl(font: .avatar(17)) // control.setFrameSize(NSMakeSize(120, 120)) diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index e72d4dcbd..32b75d2aa 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -29,7 +29,7 @@ private func wrappedMinuteRange(range: Range, dayIndexOffset: Int = 0) -> I } -private extension TimeZoneList.Item { +extension TimeZoneList.Item { var text: String { let hoursFromGMT = TimeInterval(self.utcOffset) / 60.0 / 60.0 let gmtText = "\(hoursFromGMT)" @@ -121,7 +121,9 @@ private final class Arguments { let editSpefic:(State.Day, State.Hours.MinutesInDay)->Void let removeSpefic:(State.Day, State.Hours.MinutesInDay)->Void let selectTimezone:(TimeZoneList.Item)->Void - init(context: AccountContext, toggleEnabled:@escaping()->Void, toggleDay:@escaping(State.Day)->Void, enableDay:@escaping(State.Day)->Void, addSpecific:@escaping(State.Day)->Void, removeSpefic:@escaping(State.Day, State.Hours.MinutesInDay)->Void, editSpefic:@escaping(State.Day, State.Hours.MinutesInDay)->Void, selectTimezone:@escaping(TimeZoneList.Item)->Void) { + let selectSpecific:(State.Day, State.Hours.MinutesInDay, Bool)->Void + let openTimezones:()->Void + init(context: AccountContext, toggleEnabled:@escaping()->Void, toggleDay:@escaping(State.Day)->Void, enableDay:@escaping(State.Day)->Void, addSpecific:@escaping(State.Day)->Void, removeSpefic:@escaping(State.Day, State.Hours.MinutesInDay)->Void, editSpefic:@escaping(State.Day, State.Hours.MinutesInDay)->Void, selectTimezone:@escaping(TimeZoneList.Item)->Void, selectSpecific:@escaping(State.Day, State.Hours.MinutesInDay, Bool)->Void, openTimezones:@escaping()->Void) { self.context = context self.toggleEnabled = toggleEnabled self.toggleDay = toggleDay @@ -130,6 +132,8 @@ private final class Arguments { self.removeSpefic = removeSpefic self.editSpefic = editSpefic self.selectTimezone = selectTimezone + self.selectSpecific = selectSpecific + self.openTimezones = openTimezones } } @@ -340,7 +344,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { }, state: nil) } - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_timezone, data: .init(name: strings().businessHoursTimezone, color: theme.colors.text, type: .contextSelector(timezone.text, zones), viewType: .singleItem))) + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_timezone, data: .init(name: strings().businessHoursTimezone, color: theme.colors.text, type: .nextContext(timezone.text), viewType: .singleItem, action: arguments.openTimezones))) } @@ -380,80 +384,85 @@ private func dayEntries(_ state: State, day: State.Day, arguments: Arguments) -> - let getHoursMenu:(State.Hours.MinutesInDay, Bool)->[ContextMenuItem] = { hour, from in - var items:[ContextMenuItem] = [] - - var start: Int = 0 - var end: Int = 24 * 60 - - if from { - start = 0 - end = hour.to - } else { - start = hour.from - end = 24 * 60 - } - - - - let s = Int(Float(start) / 60.0) - let e = Int(Float(end) / 60.0) - - let from_hours = Int(Float(hour.from) / 60) - let to_hours = Int(Float(hour.to) / 60) - - - - for i in s ... e { - let minuteRange = i * 60 - - var intersected = false - let hourRange = NSMakeRange(minuteRange, minuteRange + 60) - if let startMinute, let endMinute { - let range = NSMakeRange(startMinute, endMinute - startMinute) - intersected = range.intersection(hourRange) == hourRange - } - - if !intersected { - let state: NSControl.StateValue? = i == (from ? from_hours : to_hours) ? .on : nil - let item = ContextMenuItem(formatHourToLocaleTime(hour: minuteRange), handler: { - arguments.editSpefic(day, .init(from: from ? minuteRange : hour.from, to: !from ? minuteRange : hour.to, uniqueId: hour.uniqueId)) - }, state: state) - - let minutes = ContextMenu() - var minuteItems:[ContextMenuItem] = [] - for j in 0 ..< 60 { - let hourRange = NSMakeRange(minuteRange, minuteRange + i) - if let startMinute, let endMinute { - let range = NSMakeRange(startMinute, endMinute - startMinute) - intersected = range.intersection(hourRange) == hourRange - } - if !intersected { - let item = ContextMenuItem(formattedMinutes[j]!, handler: { - arguments.editSpefic(day, .init(from: from ? minuteRange + j : hour.from, to: !from ? minuteRange + j : hour.to, uniqueId: hour.uniqueId)) - }, state: state == .on && j == (from ? hour.from % 60 : hour.to % 60) ? .on : nil) - - minuteItems.append(item) - } - } - minutes.items = minuteItems - - item.submenu = minutes - items.append(item) - } - - } - return items - } +// let getHoursMenu:(State.Hours.MinutesInDay, Bool)->[ContextMenuItem] = { hour, from in +// var items:[ContextMenuItem] = [] +// +// var start: Int = 0 +// var end: Int = 24 * 60 +// +// if from { +// start = 0 +// end = hour.to +// } else { +// start = hour.from +// end = 24 * 60 +// } +// +// +// +// let s = Int(Float(start) / 60.0) +// let e = Int(Float(end) / 60.0) +// +// let from_hours = Int(Float(hour.from) / 60) +// let to_hours = Int(Float(hour.to) / 60) +// +// +// +// for i in s ... e { +// let minuteRange = i * 60 +// +// var intersected = false +// let hourRange = NSMakeRange(minuteRange, minuteRange + 60) +// if let startMinute, let endMinute { +// let range = NSMakeRange(startMinute, endMinute - startMinute) +// intersected = range.intersection(hourRange) == hourRange +// } +// +// if !intersected { +// let state: NSControl.StateValue? = i == (from ? from_hours : to_hours) ? .on : nil +// let item = ContextMenuItem(formatHourToLocaleTime(hour: minuteRange), handler: { +// arguments.editSpefic(day, .init(from: from ? minuteRange : hour.from, to: !from ? minuteRange : hour.to, uniqueId: hour.uniqueId)) +// }, state: state) +// +// let minutes = ContextMenu() +// var minuteItems:[ContextMenuItem] = [] +// for j in 0 ..< 60 { +// let hourRange = NSMakeRange(minuteRange, minuteRange + i) +// if let startMinute, let endMinute { +// let range = NSMakeRange(startMinute, endMinute - startMinute) +// intersected = range.intersection(hourRange) == hourRange +// } +// if !intersected { +// let item = ContextMenuItem(formattedMinutes[j]!, handler: { +// arguments.editSpefic(day, .init(from: from ? minuteRange + j : hour.from, to: !from ? minuteRange + j : hour.to, uniqueId: hour.uniqueId)) +// }, state: state == .on && j == (from ? hour.from % 60 : hour.to % 60) ? .on : nil) +// +// minuteItems.append(item) +// } +// } +// minutes.items = minuteItems +// +// item.submenu = minutes +// items.append(item) +// } +// +// } +// return items +// } for hour in hours.list { entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_opening_time(hour), data: .init(name: strings().businessHoursSetOpeningTime, color: theme.colors.text, type: .contextSelector(formatHourToLocaleTime(hour: hour.from), getHoursMenu(hour, true)), viewType: .firstItem))) + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_opening_time(hour), data: .init(name: strings().businessHoursSetOpeningTime, color: theme.colors.text, type: .nextContext(formatHourToLocaleTime(hour: hour.from)), viewType: .firstItem, action: { + arguments.selectSpecific(day, hour, true) + }))) + - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_closing_time(hour), data: .init(name: strings().businessHoursSetClosingTime, color: theme.colors.text, type: .contextSelector(formatHourToLocaleTime(hour: hour.to), getHoursMenu(hour, false)), viewType: .innerItem))) + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_closing_time(hour), data: .init(name: strings().businessHoursSetClosingTime, color: theme.colors.text, type: .nextContext(formatHourToLocaleTime(hour: hour.to)), viewType: .innerItem, action: { + arguments.selectSpecific(day, hour, false) + }))) entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_remove_opening(hour), data: .init(name: strings().businessHoursSetRemove, color: theme.colors.redUI, type: .none, viewType: .lastItem, action: { arguments.removeSpefic(day, hour) @@ -619,6 +628,35 @@ func BusinessHoursController(context: AccountContext) -> InputDataController { current.timezone = timezone return current } + }, selectSpecific: { day, hour, isFrom in + let from: TimePickerOption = TimePickerOption(hours: Int32(Float(hour.from) / 60), minutes: Int32(hour.from % 60)) + let to: TimePickerOption = TimePickerOption(hours: Int32(Float(hour.to) / 60), minutes: Int32(hour.to % 60)) + + showModal(with: TimeRangeSelectorController(context: context, from: from, to: to, title: day.title, ok: strings().modalSave, fromString: strings().businessHoursSetOpeningTime, toString: strings().businessHoursSetClosingTime, endIsResponder: !isFrom, updatedValue: { updatedFrom, updatedTo in + + let updated = State.Hours.MinutesInDay(from: Int(updatedFrom.hours * 60 + updatedFrom.minutes), to: Int(updatedTo.hours * 60 + updatedTo.minutes), uniqueId: hour.uniqueId) + updateState { current in + var current = current + var hours = current.data[day] ?? .init() + if let hourIndex = hours.list.firstIndex(where: { $0.uniqueId == updated.uniqueId }) { + hours.list[hourIndex] = updated + } + current.data[day] = hours + return current + } + + }), for: context.window) + }, openTimezones: { + let state = stateValue.with { $0 } + if let list = state.timeZones?.items, let timezone = state.timezone { + showModal(with: BusinessTimezonesController(context: context, timezones: list, selected: timezone, complete: { updated in + updateState { current in + var current = current + current.timezone = updated + return current + } + }), for: context.window) + } }) getArguments = { [weak arguments] in diff --git a/Telegram-Mac/BusinessTimezonesController.swift b/Telegram-Mac/BusinessTimezonesController.swift new file mode 100644 index 000000000..51204743b --- /dev/null +++ b/Telegram-Mac/BusinessTimezonesController.swift @@ -0,0 +1,140 @@ +// +// BusinessTimezonesController.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.03.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import Foundation +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + + +private final class Arguments { + let context: AccountContext + let toggle: (TimeZoneList.Item)->Void + let updateSearch:(SearchState)->Void + init(context: AccountContext, toggle: @escaping(TimeZoneList.Item)->Void, updateSearch:@escaping(SearchState)->Void) { + self.context = context + self.toggle = toggle + self.updateSearch = updateSearch + } +} + +private struct State : Equatable { + var list: [TimeZoneList.Item] = [] + var selected: TimeZoneList.Item + var searchState: SearchState = .init(state: .None, request: nil) +} +private func _id_timezone(_ timezone: TimeZoneList.Item) -> InputDataIdentifier { + return .init("_id_timezone\(timezone.id)") +} + +private let _id_search = InputDataIdentifier("_id_search") + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + + + entries.append(.search(sectionId: sectionId, index: index, value: .none, identifier: _id_search, update: arguments.updateSearch)) + + let list: [TimeZoneList.Item] + if !state.searchState.request.isEmpty { + list = state.list.filter({ + $0.text.lowercased().contains(state.searchState.request.lowercased()) + }) + } else { + list = state.list + } + + if list.isEmpty { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("empty"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return SearchEmptyRowItem(initialSize, stableId: stableId) + })) + } else { + for (i, timezone) in list.enumerated() { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_timezone(timezone), data: .init(name: timezone.text, color: theme.colors.text, type: timezone == state.selected ? .image(theme.icons.poll_selected) : .none, viewType: .legacy, action: { + arguments.toggle(timezone) + }))) + } + } + + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func BusinessTimezonesController(context: AccountContext, timezones: [TimeZoneList.Item] = [], selected: TimeZoneList.Item, complete: @escaping(TimeZoneList.Item)->Void) -> InputDataModalController { + + let actionsDisposable = DisposableSet() + + var close:(()->Void)? = nil + + let initialState = State(list: timezones, selected: selected) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context, toggle: { timezone in + complete(timezone) + close?() + }, updateSearch: { searchState in + updateState { current in + var current = current + current.searchState = searchState + return current + } + }) + + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: strings().businessHoursTimezone) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + controller.validateData = { _ in + complete(stateValue.with { $0.selected }) + close?() + return .none + } + + + let modalController = InputDataModalController(controller, size: NSMakeSize(370, 0)) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + controller.getBackgroundColor = { + theme.colors.background + } + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController + +} + + diff --git a/Telegram-Mac/DateSelectorModalController.swift b/Telegram-Mac/DateSelectorModalController.swift index 0452e9f38..828b99eb1 100644 --- a/Telegram-Mac/DateSelectorModalController.swift +++ b/Telegram-Mac/DateSelectorModalController.swift @@ -122,7 +122,7 @@ extension TimePickerOption { var interval: TimeInterval { let hours = Double(self.hours) * 60.0 * 60 let minutes = Double(self.minutes) * 60.0 - let seconds = Double(self.seconds) + let seconds = Double(self.seconds ?? 0) return hours + minutes + seconds } } diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 5123af136..28702df90 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260557 + 260597 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/SelectCountryModalController.swift b/Telegram-Mac/SelectCountryModalController.swift index 0c9ec2e2e..b80b35a98 100644 --- a/Telegram-Mac/SelectCountryModalController.swift +++ b/Telegram-Mac/SelectCountryModalController.swift @@ -12,7 +12,6 @@ import TGUIKit import SwiftSignalKit import TelegramCore - private final class Arguments { let context: AccountContext let toggle: (Country)->Void diff --git a/Telegram-Mac/TimeRangeSelectorController.swift b/Telegram-Mac/TimeRangeSelectorController.swift new file mode 100644 index 000000000..1b4b06277 --- /dev/null +++ b/Telegram-Mac/TimeRangeSelectorController.swift @@ -0,0 +1,229 @@ +// +// TimeRangeSelectorController.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.03.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + +private final class Arguments { + let context: AccountContext + let updateFrom:(TimePickerOption)->Void + let updateTo:(TimePickerOption)->Void + init(context: AccountContext, updateFrom:@escaping(TimePickerOption)->Void, updateTo:@escaping(TimePickerOption)->Void) { + self.context = context + self.updateFrom = updateFrom + self.updateTo = updateTo + } +} + +private struct State : Equatable { + var from: TimePickerOption + var to: TimePickerOption +} + +private final class TimeRangeSelectorItem : GeneralRowItem { + let from: TimePickerOption + let to: TimePickerOption + let fromString: String + let toString: String + let updateFrom:(TimePickerOption)->Void + let updateTo:(TimePickerOption)->Void + init(_ initialSize: NSSize, stableId: AnyHashable, viewType: GeneralViewType, from: TimePickerOption, to: TimePickerOption, fromString: String, toString: String, updateFrom:@escaping(TimePickerOption)->Void, updateTo:@escaping(TimePickerOption)->Void) { + self.from = from + self.to = to + self.fromString = fromString + self.toString = toString + self.updateFrom = updateFrom + self.updateTo = updateTo + super.init(initialSize, height: 55, stableId: stableId) + } + + override func viewClass() -> AnyClass { + return TimeRangeSelectorView.self + } +} + +private final class TimeRangeSelectorView : GeneralContainableRowView { + let fromView = TimePicker(selected: TimePickerOption(hours: 0, minutes: 0, seconds: nil)) + let toView = TimePicker(selected: TimePickerOption(hours: 0, minutes: 0, seconds: nil)) + let fromLabel = TextViewLabel() + let toLabel = TextViewLabel() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(fromView) + addSubview(toView) + + addSubview(fromLabel) + addSubview(toLabel) + } + + override func layout() { + super.layout() + self.fromView.setFrameSize(NSMakeSize(115, 30)) + self.toView.setFrameSize(NSMakeSize(115, 30)) + + + self.fromView.setFrameOrigin(NSMakePoint(0, containerView.frame.height - fromView.frame.height)) + self.toView.setFrameOrigin(NSMakePoint(self.fromView.frame.maxX + 40, containerView.frame.height - toView.frame.height)) + + fromLabel.setFrameOrigin(NSMakePoint(floorToScreenPixels((fromView.frame.width - fromLabel.frame.width) / 2), 0)) + toLabel.setFrameOrigin(NSMakePoint(self.toView.frame.minX + floorToScreenPixels((toView.frame.width - toLabel.frame.width) / 2), 0)) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var backdorColor: NSColor { + return .clear + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? TimeRangeSelectorItem else { + return + } + + self.fromView.update = { [weak item] value in + item?.updateFrom(value) + return true + } + self.toView.update = { [weak item] value in + item?.updateTo(value) + return true + } + + self.fromView.selected = item.from + self.toView.selected = item.to + + fromLabel.attributedString = .initialize(string: item.fromString, color: theme.colors.text, font: .medium(.text)) + toLabel.attributedString = .initialize(string: item.toString, color: theme.colors.text, font: .medium(.text)) + + fromLabel.sizeToFit() + toLabel.sizeToFit() + + } + + override var firstResponder: NSResponder? { + return fromView.firstResponder + } + + override func nextResponder() -> NSResponder? { + if window?.firstResponder == fromView.firstResponder { + return toView.firstResponder + } else { + return firstResponder + } + } + + override func hasFirstResponder() -> Bool { + return true + } +} + + +private func entries(_ state: State, arguments: Arguments, fromString: String, toString: String) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .customModern(10))) + sectionId += 1 + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("date"), equatable: .init(state), comparable: nil, item: { initialSize, stableId in + return TimeRangeSelectorItem(initialSize, stableId: stableId, viewType: .singleItem, from: state.from, to: state.to, fromString: fromString, toString: toString, updateFrom: arguments.updateFrom, updateTo: arguments.updateTo) + })) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func TimeRangeSelectorController(context: AccountContext, from: TimePickerOption, to: TimePickerOption, title: String, ok: String, fromString: String, toString: String, endIsResponder: Bool = false, updatedValue:@escaping(TimePickerOption, TimePickerOption)->Void) -> InputDataModalController { + + let actionsDisposable = DisposableSet() + + let initialState = State(from: from, to: to) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + var close:(()->Void)? = nil + + let arguments = Arguments(context: context, updateFrom: { value in + updateState { current in + var current = current + current.from = value + return current + } + }, updateTo: { value in + updateState { current in + var current = current + current.to = value + return current + } + }) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments, fromString: fromString, toString: toString)) + } + + let controller = InputDataController(dataSignal: signal, title: title) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + let modalInteractions = ModalInteractions(acceptTitle: ok, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, size: NSMakeSize(310, 300)) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + controller.didAppear = { controller in + if endIsResponder { + controller.jumpNext() + } + } + + controller.validateData = { _ in + let value = stateValue.with { $0 } + updatedValue(value.from, value.to) + + return .success(.custom({ + close?() + })) + + } + + close = { [weak modalController] in + modalController?.modal?.close() + } + + + return modalController +} + + +/* + + */ + + diff --git a/Telegram.xcodeproj/project.pbxproj b/Telegram.xcodeproj/project.pbxproj index fd19324c1..040fae6b3 100644 --- a/Telegram.xcodeproj/project.pbxproj +++ b/Telegram.xcodeproj/project.pbxproj @@ -95,6 +95,8 @@ 27303AC12AC1D13600433CED /* GiveawayModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27303AC02AC1D13600433CED /* GiveawayModalController.swift */; }; 27303ACB2AC2B20C00433CED /* GiftLinkModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27303ACA2AC2B20C00433CED /* GiftLinkModalController.swift */; }; 27303ACD2AC2BD5000433CED /* InputDataTableBasedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27303ACC2AC2BD5000433CED /* InputDataTableBasedItem.swift */; }; + 273060AA2B984AE700F3317D /* TimeRangeSelectorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 273060A92B984AE700F3317D /* TimeRangeSelectorController.swift */; }; + 273060AC2B98765D00F3317D /* BusinessTimezonesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 273060AB2B98765D00F3317D /* BusinessTimezonesController.swift */; }; 27307E982ADDA40B00D970EE /* menu_quote.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27307E972ADDA40A00D970EE /* menu_quote.tgs */; }; 27307E9A2ADE6E0C00D970EE /* SelectCountryModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27307E992ADE6E0C00D970EE /* SelectCountryModalController.swift */; }; 27307E9C2ADEB45800D970EE /* SelectColorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27307E9B2ADEB45800D970EE /* SelectColorController.swift */; }; @@ -1608,6 +1610,8 @@ 27303AC02AC1D13600433CED /* GiveawayModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiveawayModalController.swift; sourceTree = ""; }; 27303ACA2AC2B20C00433CED /* GiftLinkModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftLinkModalController.swift; sourceTree = ""; }; 27303ACC2AC2BD5000433CED /* InputDataTableBasedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDataTableBasedItem.swift; sourceTree = ""; }; + 273060A92B984AE700F3317D /* TimeRangeSelectorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeRangeSelectorController.swift; sourceTree = ""; }; + 273060AB2B98765D00F3317D /* BusinessTimezonesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BusinessTimezonesController.swift; sourceTree = ""; }; 27307E972ADDA40A00D970EE /* menu_quote.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = menu_quote.tgs; sourceTree = ""; }; 27307E992ADE6E0C00D970EE /* SelectCountryModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryModalController.swift; sourceTree = ""; }; 27307E9B2ADEB45800D970EE /* SelectColorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectColorController.swift; sourceTree = ""; }; @@ -3542,6 +3546,7 @@ 27E1F2432B7B007B00DCF241 /* BusinessLocationController.swift */, 27E1F2452B7B876800DCF241 /* BusinessChatbotController.swift */, 279338862B7E2B070096A292 /* BusinessQuickReplyController.swift */, + 273060AB2B98765D00F3317D /* BusinessTimezonesController.swift */, ); name = business; sourceTree = ""; @@ -4585,6 +4590,7 @@ 2751017C29D2BA02004CEFF3 /* TextInputController.swift */, 27303ACC2AC2BD5000433CED /* InputDataTableBasedItem.swift */, 27C50AE82AC7492A00FED2E4 /* InputDataCloseItem.swift */, + 273060A92B984AE700F3317D /* TimeRangeSelectorController.swift */, ); name = general; sourceTree = ""; @@ -7250,6 +7256,7 @@ C2B9BE891EFC5E7000D6B96F /* Appearance.swift in Sources */, C29F4C781F45FBFF00DBFC00 /* InstantPageSlideshowItem.swift in Sources */, C2B1A1111D9FD0A100ACB1DD /* ChatInterfaceInteraction.swift in Sources */, + 273060AC2B98765D00F3317D /* BusinessTimezonesController.swift in Sources */, C2B1A0EF1D9D94CE00ACB1DD /* SeparatorRowItem.swift in Sources */, C29340F11F506C310074991E /* EmptyGroupstickerSearchRowItem.swift in Sources */, D06C6950253887F600DD9005 /* SlotMachineValue.swift in Sources */, @@ -7620,6 +7627,7 @@ C2AC9C181E1E687E0085C7DE /* GlobalBadgeNode.swift in Sources */, 270202C6261B3D7C00CFCE6D /* UITextField.swift in Sources */, 27FFCEC72694BC95006CA024 /* WidgetController.swift in Sources */, + 273060AA2B984AE700F3317D /* TimeRangeSelectorController.swift in Sources */, C275E9F51F8FEDEB00D3D8C0 /* PhoneNumberConfirmController.swift in Sources */, 2748C56327BA53C800F05503 /* Auth_Loading.swift in Sources */, 277D281C2AD0322E00CC6420 /* ExperimentalTextView.swift in Sources */, diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index a73eb959d..9645eb522 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260557 + 260597 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift index 1cd183f22..b573fb52d 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift @@ -489,7 +489,7 @@ final class PeerCallScreenView : Control { current = view } else { current = PeerCallPhotoView(frame: NSMakeRect(0, 0, 120, 120)) - addSubview(current, positioned: .below, relativeTo: self.statusView) + addSubview(current, positioned: .below, relativeTo: self.subviews.first) self.photoView = current ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: current.centerFrameX(y: floorToScreenPixels(frame.height / 3) - 50)) diff --git a/packages/TGUIKit/Sources/DatePicker.swift b/packages/TGUIKit/Sources/DatePicker.swift index ae93dd272..5694611ca 100644 --- a/packages/TGUIKit/Sources/DatePicker.swift +++ b/packages/TGUIKit/Sources/DatePicker.swift @@ -128,8 +128,8 @@ public class DatePicker: Control where T: Equatable { public struct TimePickerOption : Equatable { public let hours: Int32 public let minutes: Int32 - public let seconds: Int32 - public init(hours: Int32, minutes: Int32, seconds: Int32) { + public let seconds: Int32? + public init(hours: Int32, minutes: Int32, seconds: Int32? = nil) { self.hours = hours self.minutes = minutes self.seconds = seconds @@ -257,7 +257,7 @@ public class TimePicker: Control { private let hoursView:TimeOptionView private let minutesView:TimeOptionView - private let secondsView:TimeOptionView + private var secondsView:TimeOptionView? private let separatorView1: TextView = TextView() private let separatorView2: TextView = TextView() private let borderView = View() @@ -267,7 +267,7 @@ public class TimePicker: Control { if oldValue != selected { self.hoursView.value = selected.hours self.minutesView.value = selected.minutes - self.secondsView.value = selected.seconds + self.secondsView?.value = selected.seconds ?? 0 needsLayout = true self.updateSelected(animated: true) } @@ -280,7 +280,12 @@ public class TimePicker: Control { self.selected = selected self.hoursView = TimeOptionView(value: selected.hours) self.minutesView = TimeOptionView(value: selected.minutes) - self.secondsView = TimeOptionView(value: selected.seconds) + + if let seconds = selected.seconds { + self.secondsView = TimeOptionView(value: seconds) + } else { + self.secondsView = nil + } super.init(frame: NSZeroRect) @@ -297,7 +302,12 @@ public class TimePicker: Control { self.addSubview(self.borderView) self.addSubview(self.hoursView) self.addSubview(self.minutesView) - self.addSubview(self.secondsView) + if let secondsView { + self.addSubview(secondsView) + } + + self.separatorView2.isHidden = secondsView == nil + self.updateLocalizationAndTheme(theme: presentation) hoursView.keyDown = { [weak self] value, isFirst in @@ -342,7 +352,7 @@ public class TimePicker: Control { self?.shake() } } - secondsView.keyDown = { [weak self] value, isFirst in + secondsView?.keyDown = { [weak self] value, isFirst in guard let selected = self?.selected else { return } @@ -350,8 +360,8 @@ public class TimePicker: Control { if isFirst { updatedValue = value } else { - if selected.seconds > 0, selected.seconds < 10 { - updatedValue = min(Int32("\(selected.seconds)\(updatedValue)")!, 59) + if let seconds = selected.seconds, seconds > 0, seconds < 10 { + updatedValue = min(Int32("\(seconds)\(updatedValue)")!, 59) self?.switchToRight() } } @@ -377,7 +387,7 @@ public class TimePicker: Control { self?.switchToRight() } } - secondsView.next = { [weak self] toRight in + secondsView?.next = { [weak self] toRight in if !toRight { self?.switchToLeft() } else { @@ -390,7 +400,7 @@ public class TimePicker: Control { if self.window?.firstResponder == self.hoursView { self.window?.makeFirstResponder(self.minutesView) } else if self.window?.firstResponder == self.minutesView { - self.window?.makeFirstResponder(self.secondsView) + self.window?.makeFirstResponder(self.secondsView ?? self.hoursView) } else { self.window?.makeFirstResponder(self.hoursView) } @@ -401,7 +411,7 @@ public class TimePicker: Control { } else if self.window?.firstResponder == self.minutesView { self.window?.makeFirstResponder(self.hoursView) } else { - self.window?.makeFirstResponder(self.secondsView) + self.window?.makeFirstResponder(self.secondsView ?? self.hoursView) } } @@ -418,11 +428,15 @@ public class TimePicker: Control { public override func layout() { super.layout() - minutesView.center() + if secondsView == nil { + minutesView.centerY(x: frame.width - minutesView.frame.width - 30) + } else { + minutesView.center() + } separatorView1.centerY(x: minutesView.frame.minX - separatorView1.frame.width - 2) hoursView.centerY(x: minutesView.frame.minX - hoursView.frame.width - separatorView1.frame.width - 5) separatorView2.centerY(x: minutesView.frame.maxX + 3) - secondsView.centerY(x: minutesView.frame.maxX + separatorView1.frame.width + 5) + secondsView?.centerY(x: minutesView.frame.maxX + separatorView1.frame.width + 5) self.borderView.frame = bounds } From 17c6bb3fd8d3652b417a90f78c80745205e2f55f Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 7 Mar 2024 10:22:44 +0400 Subject: [PATCH 44/50] fixes and new private call screen for arm devices --- Telegram-Mac/CallScreenController.swift | 4 + Telegram-Mac/CallSettingsController.swift | 14 +- .../CallSettingsModalController.swift | 20 +- Telegram-Mac/CallWindowController.swift | 4 +- .../ChatInterfaceStateContextQueries.swift | 15 +- Telegram-Mac/ChatListRowItem.swift | 19 ++ Telegram-Mac/ChatListRowView.swift | 2 +- Telegram-Mac/Extensions.swift | 6 +- Telegram-Mac/GeneralRowView.swift | 2 +- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/StoryStealthModeController.swift | 4 + Telegram-Mac/en.lproj/Localizable.strings | Bin 857102 -> 857252 bytes TelegramShare/Info.plist | 2 +- .../CallVideoLayer/Sources/CallBlobView.swift | 2 +- .../Sources/Localization/Localizable.swift | 4 + .../Sources/PeerCallArguments.swift | 11 +- .../Sources/PeerCallScreen.swift | 48 +++- .../Sources/PeerCallScreenView.swift | 216 +++++++++++++----- .../ic_settings.imageset/Contents.json | 22 ++ .../ic_settings.imageset/callsettings.png | Bin 0 -> 531 bytes .../ic_settings.imageset/callsettings@2x.png | Bin 0 -> 1183 bytes .../PrivateCallScreen/Sources/State.swift | 6 + submodules/tgcalls | 2 +- 23 files changed, 315 insertions(+), 90 deletions(-) create mode 100644 packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_settings.imageset/Contents.json create mode 100644 packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_settings.imageset/callsettings.png create mode 100644 packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_settings.imageset/callsettings@2x.png diff --git a/Telegram-Mac/CallScreenController.swift b/Telegram-Mac/CallScreenController.swift index 80e6f50ed..96fb25667 100644 --- a/Telegram-Mac/CallScreenController.swift +++ b/Telegram-Mac/CallScreenController.swift @@ -222,6 +222,10 @@ func callScreen(_ context: AccountContext, _ result:PCallResult) { session?.acceptCallSession() }, video: { [weak session] isIncoming in return session?.makeVideo(isIncoming: isIncoming) + }, audioLevel: { [weak session] in + return session?.audioLevel ?? .single(0) + }, openSettings: { window in + showModal(with: CallSettingsModalController(context.sharedContext, presentation: darkAppearance), for: window) }) let screen: PeerCallScreen diff --git a/Telegram-Mac/CallSettingsController.swift b/Telegram-Mac/CallSettingsController.swift index b01ea941e..c57efe726 100644 --- a/Telegram-Mac/CallSettingsController.swift +++ b/Telegram-Mac/CallSettingsController.swift @@ -35,7 +35,7 @@ private let _id_input_audio = InputDataIdentifier("_id_input_audio") private let _id_output_audio = InputDataIdentifier("_id_output_audio") private let _id_micro = InputDataIdentifier("_id_micro") -private func callSettingsEntries(settings: VoiceCallSettings, devices: IODevices, arguments: CallSettingsArguments) -> [InputDataEntry] { +private func callSettingsEntries(settings: VoiceCallSettings, devices: IODevices, arguments: CallSettingsArguments, theme: TelegramPresentationTheme) -> [InputDataEntry] { var entries:[InputDataEntry] = [] var sectionId: Int32 = 0 @@ -62,6 +62,8 @@ private func callSettingsEntries(settings: VoiceCallSettings, devices: IODevices activeCameraDevice = cameraDevice } + let customTheme = GeneralRowItem.Theme.initialize(theme) + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().callSettingsCameraTitle), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) index += 1 entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_input_camera, data: .init(name: strings().callSettingsInputText, color: theme.colors.text, type: .contextSelector(cameraDevice?.localizedName ?? strings().callSettingsDeviceDefault, [ContextMenuItem(strings().callSettingsDeviceDefault, handler: { @@ -70,7 +72,7 @@ private func callSettingsEntries(settings: VoiceCallSettings, devices: IODevices return ContextMenuItem(value.localizedName, handler: { arguments.toggleInputVideoDevice(value.uniqueID) }) - }), viewType: activeCameraDevice == nil ? .singleItem : .firstItem))) + }), viewType: activeCameraDevice == nil ? .singleItem : .firstItem, theme: customTheme))) index += 1 if let activeCameraDevice = activeCameraDevice { @@ -107,12 +109,12 @@ private func callSettingsEntries(settings: VoiceCallSettings, devices: IODevices return ContextMenuItem(value.localizedName, handler: { arguments.toggleInputAudioDevice(value.uniqueID) }) - }), viewType: activeMicroDevice == nil ? .singleItem : .firstItem))) + }), viewType: activeMicroDevice == nil ? .singleItem : .firstItem, theme: customTheme))) index += 1 if let activeMicroDevice = activeMicroDevice { entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_micro, equatable: InputDataEquatable(activeMicroDevice.uniqueID), comparable: nil, item: { initialSize, stableId -> TableRowItem in - return MicrophonePreviewRowItem(initialSize, stableId: stableId, context: arguments.sharedContext, viewType: .lastItem) + return MicrophonePreviewRowItem(initialSize, stableId: stableId, context: arguments.sharedContext, viewType: .lastItem, customTheme: customTheme) })) index += 1 } @@ -127,7 +129,7 @@ private func callSettingsEntries(settings: VoiceCallSettings, devices: IODevices -func CallSettingsController(sharedContext: SharedAccountContext) -> InputDataController { +func CallSettingsController(sharedContext: SharedAccountContext, presentation: TelegramPresentationTheme = theme) -> InputDataController { let devicesContext = sharedContext.devicesContext @@ -148,7 +150,7 @@ func CallSettingsController(sharedContext: SharedAccountContext) -> InputDataCon }) let signal = combineLatest(voiceCallSettings(sharedContext.accountManager), devicesContext.signal) |> map { settings, devices in - return InputDataSignalValue(entries: callSettingsEntries(settings: settings, devices: devices, arguments: arguments)) + return InputDataSignalValue(entries: callSettingsEntries(settings: settings, devices: devices, arguments: arguments, theme: presentation)) } let controller = InputDataController(dataSignal: signal, title: strings().callSettingsTitle, hasDone: false) diff --git a/Telegram-Mac/CallSettingsModalController.swift b/Telegram-Mac/CallSettingsModalController.swift index 4ad5777a6..8f45e5c3f 100644 --- a/Telegram-Mac/CallSettingsModalController.swift +++ b/Telegram-Mac/CallSettingsModalController.swift @@ -14,19 +14,33 @@ import Postbox import TGUIKit -func CallSettingsModalController(_ sharedContext: SharedAccountContext) -> InputDataModalController { +func CallSettingsModalController(_ sharedContext: SharedAccountContext, presentation: TelegramPresentationTheme = theme) -> InputDataModalController { var close: (()->Void)? = nil - let controller = CallSettingsController(sharedContext: sharedContext) + let controller = CallSettingsController(sharedContext: sharedContext, presentation: presentation) + + controller.getBackgroundColor = { + return presentation.colors.listBackground + } controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { close?() }) - let modalController = InputDataModalController(controller) + let modalController = InputDataModalController(controller, presentation: presentation) + modalController._hasBorder = false + + modalController.getModalTheme = { + .init(text: presentation.colors.text, grayText: presentation.colors.grayText, background: .clear, border: .clear, accent: presentation.colors.accent, grayForeground: .clear, activeBackground: presentation.colors.background, activeBorder: presentation.colors.border, listBackground: .clear) + } + modalController.getHeaderBorderColor = { + return presentation.colors.border + } + + close = { [weak modalController] in modalController?.close() } diff --git a/Telegram-Mac/CallWindowController.swift b/Telegram-Mac/CallWindowController.swift index df4b017f2..8881e243f 100644 --- a/Telegram-Mac/CallWindowController.swift +++ b/Telegram-Mac/CallWindowController.swift @@ -1468,8 +1468,8 @@ func closeCall(minimisize: Bool = false) { private var peerCall: PeerCallScreen? func applyUIPCallResult(_ context: AccountContext, _ result:PCallResult) { - #if DEBUG - + #if arch(arm64) + callScreen(context, result) return diff --git a/Telegram-Mac/ChatInterfaceStateContextQueries.swift b/Telegram-Mac/ChatInterfaceStateContextQueries.swift index fb23f1e0b..d6df5c446 100644 --- a/Telegram-Mac/ChatInterfaceStateContextQueries.swift +++ b/Telegram-Mac/ChatInterfaceStateContextQueries.swift @@ -251,14 +251,17 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres } case let .command(query): let normalizedQuery = query.lowercased() - switch chatPresentationInterfaceState.chatMode { - case .history: - break - default: - return (nil, .single({ _ in return nil })) - } + if let peer = chatPresentationInterfaceState.peer { if peer.isUser, !peer.isBot { + + switch chatPresentationInterfaceState.chatMode { + case .history: + break + default: + return (nil, .single({ _ in return nil })) + } + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() if chatPresentationInterfaceState.accountPeer?.isPremium == true { diff --git a/Telegram-Mac/ChatListRowItem.swift b/Telegram-Mac/ChatListRowItem.swift index 7efda52ee..3d4b50bb0 100644 --- a/Telegram-Mac/ChatListRowItem.swift +++ b/Telegram-Mac/ChatListRowItem.swift @@ -1239,6 +1239,25 @@ class ChatListRowItem: TableRowItem { return (max(200, size.width) - margin * 3) - w - (chatNameLayout != nil ? textLeftCutout : 0) } + var inputActivityWidth: CGFloat { + var w: CGFloat = 0 + if let badgeNode = badgeNode { + w += badgeNode.size.width + 5 + } + if let _ = mentionsCount { + w += 24 + } + if let _ = reactionsCount { + w += 24 + } + if isPinned && badgeNode == nil { + w += 20 + } + w += (leftInset - 20) + + return (max(200, size.width) - margin * 3) - w - (chatNameLayout != nil ? textLeftCutout : 0) + } + var leftInset:CGFloat { switch mode { case .chat, .savedMessages: diff --git a/Telegram-Mac/ChatListRowView.swift b/Telegram-Mac/ChatListRowView.swift index b70c106b5..8743b7125 100644 --- a/Telegram-Mac/ChatListRowView.swift +++ b/Telegram-Mac/ChatListRowView.swift @@ -1045,7 +1045,7 @@ class ChatListRowView: TableRowView, ViewDisplayDelegate, RevealTableView { activity = theme.activity(key: 15, foregroundColor: theme.colors.grayIcon, backgroundColor: theme.colors.background) } if oldValue != item.activities || activity != activitiesModel?.theme { - activitiesModel?.update(with: inputActivities, for: item.messageWidth, theme: activity, layout: { [weak self] show in + activitiesModel?.update(with: inputActivities, for: item.inputActivityWidth, theme: activity, layout: { [weak self] show in self?.hiddenMessage = show self?.needsLayout = true }) diff --git a/Telegram-Mac/Extensions.swift b/Telegram-Mac/Extensions.swift index 00c65db15..274f10f7f 100644 --- a/Telegram-Mac/Extensions.swift +++ b/Telegram-Mac/Extensions.swift @@ -1805,7 +1805,11 @@ extension String { let char = string.character(at: range.location + i) rep += "\(chars[Int(char) % chars.count])" } - return string.replacingCharacters(in: range, with: rep) + if let intersection = NSMakeRange(0, string.length).intersection(range) { + return string.replacingCharacters(in: range, with: rep) + } else { + return string as String + } // if string.length <= range.upperBound { // return string.replacingCharacters(in: range, with: rep) diff --git a/Telegram-Mac/GeneralRowView.swift b/Telegram-Mac/GeneralRowView.swift index 489e11368..b27a96a70 100644 --- a/Telegram-Mac/GeneralRowView.swift +++ b/Telegram-Mac/GeneralRowView.swift @@ -77,7 +77,7 @@ class GeneralContainableRowView : TableRowView { guard let item = item as? GeneralRowItem else { return } - let blockWidth = min(maxBlockWidth, size.width - item.inset.left - item.inset.right) + let blockWidth = min(item.viewType == .legacy ? size.width : maxBlockWidth, size.width - item.inset.left - item.inset.right) let rect = NSMakeRect(floorToScreenPixels(backingScaleFactor, (size.width - blockWidth) / 2), item.inset.top, blockWidth, size.height - item.inset.bottom - item.inset.top) transition.updateFrame(view: self.containerView, frame: rect) diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 28702df90..73ecfdf09 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260597 + 260644 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/StoryStealthModeController.swift b/Telegram-Mac/StoryStealthModeController.swift index 6fe697020..7ef185531 100644 --- a/Telegram-Mac/StoryStealthModeController.swift +++ b/Telegram-Mac/StoryStealthModeController.swift @@ -296,6 +296,10 @@ final class StoryStealthModeController: ModalViewController { } }, for: .Click) } + + override var hasBorder: Bool { + return false + } } diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index 58745b958b4d0858bada95bcc6d24cd003b984e2..e7433498e73b48e5922e874b104451d6eae856ea 100644 GIT binary patch delta 163 zcmeBMVX|bUNka=`3sVbo3rh>@7Php7)46n+c&5iJX5*SHcV23H+CsJ{mg(=7uu1ra zGo&&kGGsIO0qGKka)x|{A|OwRL4m;*h?N*%@(K)jC@K^fiWxE)(t)Ch3^_n`)(l(> lT$3A(#HahbV&rOnu#^pm*@2h?h&h26B*wk{!BQT*JOG$FFe(55 delta 62 zcmZ3o(xh*NNka=`3sVbo3rh>@7Php7+ovsLi(;An;}4@)d(JX8AZ7<*4j|?PVlE)& L-k!6J$082^6rdRh diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 9645eb522..1f3c1c0e8 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260597 + 260644 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/CallVideoLayer/Sources/CallBlobView.swift b/packages/CallVideoLayer/Sources/CallBlobView.swift index e6eba45fc..a4ac62e0c 100644 --- a/packages/CallVideoLayer/Sources/CallBlobView.swift +++ b/packages/CallVideoLayer/Sources/CallBlobView.swift @@ -12,7 +12,7 @@ import AppKit public class CallBlobView : LayerBackedView { - private let blob = CallBlobsLayer() + public let blob = CallBlobsLayer() private let backgroundLayer = CALayer() public var maskLayer = CALayer() { didSet { diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index 9162cd6ea..ebe406532 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -1165,6 +1165,8 @@ public final class L10n { public static var callStatusRinging: String { return L10n.tr("Localizable", "Call.StatusRinging") } /// Undefined error, please try later. public static var callUndefinedError: String { return L10n.tr("Localizable", "Call.UndefinedError") } + /// Video + public static var callVideo: String { return L10n.tr("Localizable", "Call.Video") } /// %@'s paused video public static func callVideoPaused(_ p1: String) -> String { return L10n.tr("Localizable", "Call.VideoPaused", p1) @@ -1221,6 +1223,8 @@ public final class L10n { public static func callToastMicroOff(_ p1: String) -> String { return L10n.tr("Localizable", "Call.Toast.MicroOff", p1) } + /// Weak network signal + public static var callToastWeakNetwork: String { return L10n.tr("Localizable", "Call.Toast.WeakNetwork") } /// Your microphone is off public static var callToastMicroOffYour: String { return L10n.tr("Localizable", "Call.Toast.MicroOff.Your") } /// Add an optional comment diff --git a/packages/PrivateCallScreen/Sources/PeerCallArguments.swift b/packages/PrivateCallScreen/Sources/PeerCallArguments.swift index cfb56f7cd..ff0071e95 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallArguments.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallArguments.swift @@ -12,13 +12,16 @@ import AppKit import SwiftSignalKit import TelegramMedia import TelegramVoip +import TGUIKit internal final class Arguments { let toggleSecretKey:()->Void let makeAvatar:(NSView?, Peer?)->NSView? - init(toggleSecretKey:@escaping()->Void, makeAvatar:@escaping(NSView?, Peer?)->NSView?) { + let openSettings:()->Void + init(toggleSecretKey:@escaping()->Void, makeAvatar:@escaping(NSView?, Peer?)->NSView?, openSettings:@escaping()->Void) { self.toggleSecretKey = toggleSecretKey self.makeAvatar = makeAvatar + self.openSettings = openSettings } } @@ -34,7 +37,9 @@ public final class PeerCallArguments { let recall:()->Void let acceptcall:()->Void let video:(Bool)->Signal? - public init(engine: TelegramEngine, peerId: PeerId, makeAvatar: @escaping (NSView?, Peer?) -> NSView, toggleMute:@escaping()->Void, toggleCamera:@escaping(ExternalPeerCallState)->Void, toggleScreencast:@escaping(ExternalPeerCallState)->Void, endcall:@escaping(ExternalPeerCallState)->Void, recall:@escaping()->Void, acceptcall:@escaping()->Void, video:@escaping(Bool)->Signal?) { + let audioLevel:()->Signal + let openSettings:(Window)->Void + public init(engine: TelegramEngine, peerId: PeerId, makeAvatar: @escaping (NSView?, Peer?) -> NSView, toggleMute:@escaping()->Void, toggleCamera:@escaping(ExternalPeerCallState)->Void, toggleScreencast:@escaping(ExternalPeerCallState)->Void, endcall:@escaping(ExternalPeerCallState)->Void, recall:@escaping()->Void, acceptcall:@escaping()->Void, video:@escaping(Bool)->Signal?, audioLevel:@escaping()->Signal, openSettings:@escaping(Window)->Void) { self.engine = engine self.peerId = peerId self.makeAvatar = makeAvatar @@ -45,5 +50,7 @@ public final class PeerCallArguments { self.recall = recall self.acceptcall = acceptcall self.video = video + self.audioLevel = audioLevel + self.openSettings = openSettings } } diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift index dae3ed46f..3cd4d8b74 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreen.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreen.swift @@ -16,6 +16,7 @@ import MetalEngine import AppKit import KeyboardKey import TelegramMedia +import Localization protocol CallViewUpdater { func updateState(_ state: PeerCallState, arguments: Arguments, transition: ContainedViewLayoutTransition) @@ -27,6 +28,7 @@ public final class PeerCallScreen : ViewController { private let screen: Window private let actionsDisposable = DisposableSet() + private let audioLevelDisposable = MetaDisposable() public var onCompletion: (()->Void)? = nil @@ -44,6 +46,7 @@ public final class PeerCallScreen : ViewController { public func update(arguments: PeerCallArguments) { self.external = arguments + self.updateAudioLevel() } @@ -61,9 +64,15 @@ public final class PeerCallScreen : ViewController { } else { fatalError("screen not found") } - super.init() - + actionsDisposable.add(audioLevelDisposable) + self.updateAudioLevel() + } + + private func updateAudioLevel() { + audioLevelDisposable.set((external.audioLevel() |> deliverOnMainQueue).start(next: { [weak self] level in + self?.genericView.updateAudioLevel(level) + })) } public func setState(_ signal: Signal) { @@ -118,22 +127,22 @@ public final class PeerCallScreen : ViewController { break default: if !redial { - actions.append(makeAction(type: .video, text: "Video", resource: .icVideo, active: external.videoState == .active(true) && !external.isScreenCapture, enabled: videoEnabled, action: { [weak self] in + actions.append(makeAction(type: .video, text: L10n.callVideo, resource: .icVideo, active: external.videoState == .active(true) && !external.isScreenCapture, enabled: videoEnabled, action: { [weak self] in self?.external.toggleCamera(external) })) - actions.append(makeAction(type: .video, text: "Screen", resource: .icScreen, active: external.videoState == .active(true) && external.isScreenCapture, enabled: videoEnabled, action: { + actions.append(makeAction(type: .video, text: L10n.callScreen, resource: .icScreen, active: external.videoState == .active(true) && external.isScreenCapture, enabled: videoEnabled, action: { self?.external.toggleScreencast(external) })) } } } - actions.append(makeAction(type: .mute, text: "Mute", resource: .icMute, active: external.isMuted && isActive, enabled: muteEnabled, action: { + actions.append(makeAction(type: .mute, text: L10n.callMute, resource: .icMute, active: external.isMuted && isActive, enabled: muteEnabled, action: { self?.external.toggleMute() })) switch external.state { case .ringing: - actions.append(makeAction(type: .accept, text: "Accept", resource: .icAccept, interactive: true, attract: true, action: { + actions.append(makeAction(type: .accept, text: L10n.callAccept, resource: .icAccept, interactive: true, attract: true, action: { self?.external.acceptcall() })) default: @@ -141,12 +150,12 @@ public final class PeerCallScreen : ViewController { } if redial { - actions.append(makeAction(type: .redial, text: "Redial", resource: .icRedial, interactive: true, attract: false, action: { + actions.append(makeAction(type: .redial, text: L10n.callRecall, resource: .icRedial, interactive: true, attract: false, action: { self?.external.recall() })) } - actions.append(makeAction(type: .mute, text: "End", resource: .icDecline, enabled: endEnabled, interactive: true, action: { + actions.append(makeAction(type: .mute, text: L10n.callEnd, resource: .icDecline, enabled: endEnabled, interactive: true, action: { self?.external.endcall(external) })) @@ -195,6 +204,11 @@ public final class PeerCallScreen : ViewController { } }, makeAvatar: { [weak self] view, peerId in return self?.external.makeAvatar(view, peerId) + }, openSettings: { [weak self] in + guard let self else { + return + } + self.external.openSettings(self.screen) }) @@ -216,10 +230,10 @@ public final class PeerCallScreen : ViewController { func take(_ index: Int, state: PeerCallState) -> String { if index == 0 { - return "Weak network signal" + return L10n.callToastWeakNetwork } if index == 1 { - return "\(state.compactTitle)'s battery is low" + return L10n.callToastLowBattery(state.compactTitle) } return "" } @@ -330,6 +344,13 @@ public final class PeerCallScreen : ViewController { } self.applyState(self.stateValue.with { $0 }, arguments: arguments, animated: animated) } + view.set(handler: { [weak self] control in + self?.updateState { current in + var current = current + current.smallVideo = .outgoing + return current + } + }, for: .Up) videoViewState.incomingView = view } else { videoViewState.incomingView = nil @@ -350,6 +371,13 @@ public final class PeerCallScreen : ViewController { } self.applyState(self.stateValue.with { $0 }, arguments: arguments, animated: animated) } + view.set(handler: { [weak self] _ in + self?.updateState { current in + var current = current + current.smallVideo = .incoming + return current + } + }, for: .Up) videoViewState.outgoingView = view } else { videoViewState.outgoingView = nil diff --git a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift index b573fb52d..025333657 100644 --- a/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift +++ b/packages/PrivateCallScreen/Sources/PeerCallScreenView.swift @@ -165,6 +165,7 @@ final class PeerCallScreenView : Control { private var photoView: PeerCallPhotoView? private let statusView = PeerCallStatusView(frame: NSMakeRect(0, 0, 300, 58)) + private let settingsView = ImageButton() private var arguments: Arguments? private var state: PeerCallState? @@ -187,8 +188,8 @@ final class PeerCallScreenView : Control { private var actionsViews: [PeerCallActionView] = [] private var actionsList: [PeerCallAction] = [] - private weak var videoLink_incoming:MetalCallVideoView? - private weak var videoLink_outgoing:MetalCallVideoView? + private weak var videoLink_large: MetalCallVideoView? + private weak var videoLink_small: MetalCallVideoView? private var videoShadowView: ShadowView? private var videoBackgroundView: NSVisualEffectView? @@ -196,6 +197,18 @@ final class PeerCallScreenView : Control { private var videoMagnify: VideoMagnify = .bottomRight + private var canAnimateAudioLevel = true + + + private var processedInitialAudioLevelBump: Bool = false + private var audioLevelBump: Float = 0.0 + + private var currentAvatarAudioScale: CGFloat = 1.0 + private var targetAudioLevel: Float = 0.0 + private var audioLevel: Float = 0.0 + private var audioLevelUpdateSubscription: SharedDisplayLinkDriver.Link? + + required init(frame frameRect: NSRect) { super.init(frame: frameRect) @@ -207,9 +220,19 @@ final class PeerCallScreenView : Control { addSubview(statusView) + settingsView.set(image: NSImage(resource: .icSettings).precomposed(.white), for: .Normal) + settingsView.autohighlight = false + settingsView.scaleOnClick = true + settingsView.sizeToFit() + + addSubview(settingsView) actions.layer?.masksToBounds = false + settingsView.set(handler: { [weak self] _ in + self?.arguments?.openSettings() + }, for: .SingleClick) + updateLayout(size: self.frame.size, transition: .immediate) } @@ -221,24 +244,26 @@ final class PeerCallScreenView : Control { override func viewWillMove(toWindow newWindow: NSWindow?) { if let window = newWindow as? Window { var start: NSPoint? = nil - + var initial: NSPoint? = nil window.set(mouseHandler: { [weak self] event in - guard let self, let window = self.window, let control = self.videoLink_outgoing else { + guard let self, let window = self.window, let control = self.videoLink_small else { return .rejected } let startPoint = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) if NSPointInRect(startPoint, control.frame) { start = startPoint + initial = startPoint } else { start = nil + initial = nil } return .rejected }, with: self, for: .leftMouseDown) window.set(mouseHandler: { [weak self] event in - guard let self, let startPoint = start, let control = self.videoLink_outgoing else { + guard let self, let startPoint = start, let control = self.videoLink_small else { return .rejected } control.isMoving = true @@ -254,13 +279,19 @@ final class PeerCallScreenView : Control { }, with: self, for: .leftMouseDragged) window.set(mouseHandler: { [ weak self] event in - guard let self, let control = self.videoLink_outgoing, let _ = start else { + guard let self, let control = self.videoLink_small, let _ = start else { return .rejected } let current = self.convert(event.locationInWindow, from: nil) + start = nil control.isMoving = false + + if initial == current { + return .rejected + } + if current.x < frame.width / 2 { if current.y > frame.height / 2 { self.videoMagnify = .bottomLeft @@ -276,11 +307,20 @@ final class PeerCallScreenView : Control { } self.updateLayout(size: self.frame.size, transition: .animated(duration: 0.35, curve: .spring)) - return .rejected + return .invoked }, with: self, for: .leftMouseUp) + self.audioLevelUpdateSubscription = SharedDisplayLinkDriver.shared.add { [weak self] _ in + guard let self else { + return + } + self.attenuateAudioLevelStep() + } + + } else { _window?.removeAllHandlers(for: self) + self.audioLevelUpdateSubscription = nil } } @@ -303,6 +343,8 @@ final class PeerCallScreenView : Control { return } + transition.updateFrame(view: settingsView, frame: CGRect.init(origin: CGPoint(x: size.width - settingsView.frame.width - 5, y: 5), size: settingsView.frame.size)) + backgroundLayer.frame = size.bounds backgroundLayer.blurredLayer.frame = size.bounds @@ -313,7 +355,7 @@ final class PeerCallScreenView : Control { photoView.updateLayout(size: photoView.frame.size, transition: transition) } - if videoLink_incoming != nil { + if videoLink_large != nil { transition.updateFrame(view: statusView, frame: statusView.centerFrameX(y: 10)) statusView.updateLayout(size: statusView.frame.size, transition: transition) } else if let photoView { @@ -323,12 +365,17 @@ final class PeerCallScreenView : Control { if let videoViewState { - if let incomingVideoView = videoLink_incoming { - transition.updateFrame(view: incomingVideoView, frame: incomingVideoFrame(view: incomingVideoView, size: size, state: videoViewState)) +// let transition: ContainedViewLayoutTransition = transition.isAnimated ? .animated(duration: 0.2, curve: .easeOut) : .immediate + if let videoLink_large = videoLink_large { + let frame = largeVideoFrame(view: videoLink_large, size: size, state: videoViewState) + transition.updateFrame(view: videoLink_large, frame: frame) + videoLink_large.updateLayout(size: frame.size, transition: transition) } - if let outgointVideoView = videoLink_outgoing, !outgointVideoView.isMoving { - transition.updateFrame(view: outgointVideoView, frame: outgoingVideoFrame(view: outgointVideoView, size: size, videoMagnify: self.videoMagnify, state: videoViewState)) + if let videoLink_small = videoLink_small, !videoLink_small.isMoving { + let frame = smallVideoFrame(view: videoLink_small, size: size, videoMagnify: self.videoMagnify, state: videoViewState) + transition.updateFrame(view: videoLink_small, frame: frame) + videoLink_small.updateLayout(size: frame.size, transition: transition) } } @@ -348,7 +395,7 @@ final class PeerCallScreenView : Control { } if let secretView { - transition.updateFrame(view: secretView, frame: secretKeyFrame(view: secretView, state: state, incomingVideo: videoLink_incoming != nil)) + transition.updateFrame(view: secretView, frame: secretKeyFrame(view: secretView, state: state, largeVideo: videoLink_large != nil)) secretView.updateLayout(size: secretView.frame.size, transition: transition) } @@ -382,6 +429,39 @@ final class PeerCallScreenView : Control { } } + func updateAudioLevel(_ value: Float) { + if self.canAnimateAudioLevel { + self.targetAudioLevel = value + } else { + self.targetAudioLevel = 0.0 + } + } + + + private func attenuateAudioLevelStep() { + self.audioLevel = self.audioLevel * 0.8 + (self.targetAudioLevel + self.audioLevelBump) * 0.2 + if self.audioLevel <= 0.01 { + self.audioLevel = 0.0 + } + self.updateAudioLevel() + } + + private func updateAudioLevel() { + if self.canAnimateAudioLevel, let photoView { + let additionalAvatarScale = CGFloat(max(0.0, min(self.audioLevel, 5.0)) * 0.05) + self.currentAvatarAudioScale = 1.0 + additionalAvatarScale +// photoView.layer?.anchorPoint = NSMakePoint(0.5, 0.5) +// photoView.layer?.transform = CATransform3DMakeScale(self.currentAvatarAudioScale, self.currentAvatarAudioScale, 1.0) +// + let blobAmplificationFactor: CGFloat = 2.0 + photoView.blobView.blob.transform = CATransform3DMakeScale(1.0 + additionalAvatarScale * blobAmplificationFactor, 1.0 + additionalAvatarScale * blobAmplificationFactor, 1.0) + + } + } + + + + func updateState(_ state: PeerCallState, videoViewState: PeerCallVideoViewState, arguments: Arguments, transition: ContainedViewLayoutTransition) { self.state = state self.arguments = arguments @@ -391,52 +471,80 @@ final class PeerCallScreenView : Control { self.backgroundLayer.update(stateIndex: state.stateIndex, isEnergySavingEnabled: false, transition: transition) + var smallVideo: MetalCallVideoView? + var largeVideo: MetalCallVideoView? + var largeInited: Bool + var smallInited: Bool + + switch state.smallVideo { + case .incoming: + smallVideo = videoViewState.incomingView + largeVideo = videoViewState.outgoingView + largeInited = videoViewState.outgoingInited + smallInited = videoViewState.incomingInited + case .outgoing: + smallVideo = videoViewState.outgoingView + largeVideo = videoViewState.incomingView + largeInited = videoViewState.incomingInited + smallInited = videoViewState.outgoingInited + } - - if let incomingView = videoViewState.incomingView { - if videoViewState.incomingInited { - addSubview(incomingView, positioned: .below, relativeTo: self.videoLink_outgoing ?? actions) + if let largeVideo = largeVideo { + if largeInited { - if transition.isAnimated, videoLink_incoming == nil { - ContainedViewLayoutTransition.immediate.updateFrame(view: incomingView, frame: incomingVideoFrame(view: incomingView, size: frame.size, state: videoViewState)) - incomingView.layer?.animateAlpha(from: 0, to: 1, duration: transition.duration, timingFunction: transition.timingFunction) + if transition.isAnimated, largeVideo.superview == nil { + let frame = largeVideoFrame(view: largeVideo, size: frame.size, state: videoViewState) + ContainedViewLayoutTransition.immediate.updateFrame(view: largeVideo, frame: frame) + largeVideo.updateLayout(size: frame.size, transition: .immediate) + largeVideo.layer?.animateAlpha(from: 0, to: 1, duration: transition.duration, timingFunction: transition.timingFunction) } - videoLink_incoming = incomingView + addSubview(largeVideo, positioned: .below, relativeTo: self.videoLink_small ?? actions) + + largeVideo.layer?.cornerRadius = 0 + largeVideo.userInteractionEnabled = false + + videoLink_large = largeVideo } else { - videoLink_incoming = nil + videoLink_large = nil + } + } else if let view = self.videoLink_large { + if view != smallVideo && view != largeVideo { + performSubviewRemoval(view, animated: transition.isAnimated) } - } else if let view = self.videoLink_incoming { - performSubviewRemoval(view, animated: transition.isAnimated) - self.videoLink_incoming = nil + self.videoLink_large = nil } - if let outgoingView = videoViewState.outgoingView { - if videoViewState.outgoingInited { - addSubview(outgoingView, positioned: .below, relativeTo: actions) - - outgoingView.layer?.cornerRadius = 10 - outgoingView.userInteractionEnabled = true - + if let smallVideo = smallVideo { + if smallInited { - if transition.isAnimated, videoLink_outgoing == nil { - ContainedViewLayoutTransition.immediate.updateFrame(view: outgoingView, frame: outgoingVideoFrame(view: outgoingView, size: frame.size, videoMagnify: self.videoMagnify, state: videoViewState)) - outgoingView.layer?.animateAlpha(from: 0, to: 1, duration: transition.duration, timingFunction: transition.timingFunction) + if transition.isAnimated, smallVideo.superview == nil { + let frame = smallVideoFrame(view: smallVideo, size: frame.size, videoMagnify: self.videoMagnify, state: videoViewState) + ContainedViewLayoutTransition.immediate.updateFrame(view: smallVideo, frame: frame) + smallVideo.updateLayout(size: frame.size, transition: .immediate) + smallVideo.layer?.animateAlpha(from: 0, to: 1, duration: transition.duration, timingFunction: transition.timingFunction) } + addSubview(smallVideo, positioned: .below, relativeTo: actions) + + smallVideo.layer?.cornerRadius = 10 + smallVideo.userInteractionEnabled = true + - videoLink_outgoing = outgoingView + videoLink_small = smallVideo } else { - videoLink_outgoing = nil + videoLink_small = nil + } + } else if let view = self.videoLink_small { + if view != smallVideo && view != largeVideo { + performSubviewRemoval(view, animated: transition.isAnimated) } - } else if let view = self.videoLink_outgoing { - performSubviewRemoval(view, animated: transition.isAnimated) - self.videoLink_outgoing = nil + self.videoLink_small = nil } - if videoLink_incoming != nil { + if videoLink_large != nil { let current: ShadowView if let view = self.videoShadowView { current = view @@ -450,13 +558,13 @@ final class PeerCallScreenView : Control { self.videoShadowView = nil } - if let videoLink_incoming { + if let videoLink_large { let current: View if let view = self.videoBackgroundView_color { current = view } else { current = View(frame: self.bounds) - addSubview(current, positioned: .below, relativeTo: videoLink_incoming) + addSubview(current, positioned: .below, relativeTo: videoLink_large) self.videoBackgroundView_color = current } current.backgroundColor = NSColor.black.withAlphaComponent(0.7) @@ -465,7 +573,7 @@ final class PeerCallScreenView : Control { self.videoBackgroundView_color = nil } - if let videoLink_incoming { + if let videoLink_large { let current: NSVisualEffectView if let view = self.videoBackgroundView { current = view @@ -475,7 +583,7 @@ final class PeerCallScreenView : Control { current.state = .active current.blendingMode = .withinWindow current.wantsLayer = true - addSubview(current, positioned: .below, relativeTo: videoLink_incoming) + addSubview(current, positioned: .below, relativeTo: videoLink_large) self.videoBackgroundView = current } } else if let videoBackgroundView { @@ -483,7 +591,7 @@ final class PeerCallScreenView : Control { self.videoBackgroundView = nil } - if videoLink_incoming == nil { + if videoLink_large == nil { let current: PeerCallPhotoView if let view = self.photoView { current = view @@ -688,7 +796,7 @@ final class PeerCallScreenView : Control { current.updateState(state, arguments: arguments, transition: isNew ? .immediate : transition) if isNew { - ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: secretKeyFrame(view: current, state: state, incomingVideo: videoLink_incoming != nil)) + ContainedViewLayoutTransition.immediate.updateFrame(view: current, frame: secretKeyFrame(view: current, state: state, largeVideo: videoLink_large != nil)) if transition.isAnimated { current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) current.layer?.animateScaleSpring(from: 0.01, to: 1, duration: 0.2, bounce: false) @@ -712,7 +820,7 @@ private extension PeerCallScreenView { func statusTooltipFrame(view: NSView, state: PeerCallState) -> NSRect { return view.centerFrameX(y: statusView.frame.maxY + 12) } - func outgoingVideoFrame(view: MetalCallVideoView, size: NSSize, videoMagnify: VideoMagnify, state: PeerCallVideoViewState) -> NSRect { + func smallVideoFrame(view: MetalCallVideoView, size: NSSize, videoMagnify: VideoMagnify, state: PeerCallVideoViewState) -> NSRect { let videoSize = state.smallVideoSize let point: NSPoint switch videoMagnify { @@ -727,21 +835,21 @@ private extension PeerCallScreenView { } return CGRect(origin: point, size: videoSize) } - func incomingVideoFrame(view: MetalCallVideoView, size: NSSize, state: PeerCallVideoViewState) -> NSRect { + func largeVideoFrame(view: MetalCallVideoView, size: NSSize, state: PeerCallVideoViewState) -> NSRect { return size.bounds } - func secretKeyFrame(view: NSView, state: PeerCallState, incomingVideo: Bool) -> NSRect { + func secretKeyFrame(view: NSView, state: PeerCallState, largeVideo: Bool) -> NSRect { if state.secretKeyViewState == .revealed { var rect = focus(NSMakeSize(200, 50)) rect.origin.y -= 30 return rect } else { var rect = focus(NSMakeSize(100, 25)) - if incomingVideo { - rect.origin.y = 16 - rect.origin.x = frame.width - rect.width - 25 + if largeVideo { + rect.origin.y = 10 + rect.origin.x = frame.width - rect.width - 10 - settingsView.frame.width } else { - rect.origin.y = 16 + rect.origin.y = 10 } return rect } diff --git a/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_settings.imageset/Contents.json b/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_settings.imageset/Contents.json new file mode 100644 index 000000000..7db90edb4 --- /dev/null +++ b/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_settings.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "callsettings.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "callsettings@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_settings.imageset/callsettings.png b/packages/PrivateCallScreen/Sources/Resources/Assets.xcassets/ic_settings.imageset/callsettings.png new file mode 100644 index 0000000000000000000000000000000000000000..d1df93e54158b74dad188ac56bcb205c3775a134 GIT binary patch literal 531 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3Ea{HEjtmSN`?>!lvI6-QsS%!O zzP=1vKsE;hV|yk83y{SK#8N=az`(SC2`(bHfEmFCNxtk9wFfG0^K@|x@!)+s%}}4k zQRH}kpTZ*(Eh{13&I@|+j%z>2UfI``VLIux%R9S9r{zwqUQH`Z{?z3?+jR3zTE2OC z_1S-u@4tPs=X+Y&{XH}HhOG{q-1vuQi=pS+R|boHLXP*;@p~su^If-|@wV4&yI+02 z>^)z&{XajaFW_4y<-y6+oP`i+n;yyoSCfn z2KMZPuXWB*>z?xF9L~Mvu$%pQ!mj%d`1~HqvPqR%^D@24$T2*5Bq;U^Q?TZ`mAUHX z`&UlWiwI~BC~1g@F+6AJm7V3D_RCW^uZDYjM|<<{62*kucDcV)49t4}Znk(ed1l#l z?Rk6WdHfTsXsocxyuAF9c;}9ZR}ThfO?O{yl4WFI!d$;0IlVpQ+ngp)%y_!`xvXWZ1o% z+uK|Bl6mmG`Ss@e-n^aJnc1t=qQnTq2*e1)2*e1)2*e1K6oG+~?6_vIInu0{RQlI# z+=%(n<{%j;`BG&|sn*tv9dT^Bd8fK}*`f!0U9Qdx^%gD>sb0;(0T-N=l?8dm zR9z9yd0r+ej|ocxI$%+tE)iUzFC6H=Sy`EqXEvCh%8v^Z!i-aI?S6_5y7Zy1wZmCi zIU~=^(V|i~3zKbyF|83TXYjpK{d3`!@J*N#kRjoqa9+49oECh^;`H@OFAvJ%8^L|D zOvhs#c!Dl{Tn;$lu9V==OTiqSRsL)j9?};MtEkpKL?reL}ag zUjz-o28|7DVH4Z0a^>i-C|^ECW`s47U{wtq^-L^hN|O?85^4>ka{7ZGB$Nwg2Jl&Z&tv z?6Oc7+D*?n=@|b}p!$W?*7iTxdm`)>mW58I^pN_Pq)lmSD?IJn zmsZ|LBJq^f`oe)8oRt-} zta+i{!ox>uvjLOO5;)*0<^Q8BCyJ=QEfO0r`BJ5bPduGE%(c?Q&0Aeh)2z2@FOAyg xB5b(%UfPV@F#<6HF#<6HF#<6HF#_dB;2$hLgb9L5(47DP002ovPDHLkV1muXAcz0} literal 0 HcmV?d00001 diff --git a/packages/PrivateCallScreen/Sources/State.swift b/packages/PrivateCallScreen/Sources/State.swift index d2898c40e..2e637bbd3 100644 --- a/packages/PrivateCallScreen/Sources/State.swift +++ b/packages/PrivateCallScreen/Sources/State.swift @@ -209,6 +209,10 @@ extension ExternalPeerCallState.State { } } +enum SmallVideoSource : Equatable { + case incoming + case outgoing +} public struct PeerCallState : Equatable { @@ -233,6 +237,8 @@ public struct PeerCallState : Equatable { var actions: [PeerCallAction] = [] + var smallVideo: SmallVideoSource = .outgoing + var statusTooltip: String? = nil public var externalState: ExternalPeerCallState = .init(state: .connecting, videoState: .notAvailable, remoteVideoState: .inactive, isMuted: false, isOutgoingVideoPaused: true, remoteAspectRatio: 1.0, remoteAudioState: .active, remoteBatteryLevel: .low, isScreenCapture: false, canBeRemoved: false) diff --git a/submodules/tgcalls b/submodules/tgcalls index 867e271bc..a76939000 160000 --- a/submodules/tgcalls +++ b/submodules/tgcalls @@ -1 +1 @@ -Subproject commit 867e271bce1d063065dc035d5e169461a20c0042 +Subproject commit a76939000517e1171afcf72354d9d066b2d66110 From 1482e4b0196ec09e93d19af13ad3f916cd048071 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 7 Mar 2024 16:41:40 +0400 Subject: [PATCH 45/50] - bugfixes --- Telegram-Mac/ApplicationContext.swift | 1 + Telegram-Mac/CallWindowController.swift | 2 +- Telegram-Mac/FragmentUsernameController.swift | 239 ++++++++++++++++++ Telegram-Mac/Info.plist | 2 +- Telegram-Mac/InteractiveTextView.swift | 2 +- Telegram-Mac/LottieLocalAnimations.swift | 4 + Telegram-Mac/tgs/fragment_username.tgs | Bin 0 -> 4492 bytes Telegram.xcodeproj/project.pbxproj | 8 + TelegramShare/Info.plist | 2 +- packages/TGUIKit/Sources/TableView.swift | 20 +- 10 files changed, 265 insertions(+), 15 deletions(-) create mode 100644 Telegram-Mac/FragmentUsernameController.swift create mode 100644 Telegram-Mac/tgs/fragment_username.tgs diff --git a/Telegram-Mac/ApplicationContext.swift b/Telegram-Mac/ApplicationContext.swift index 2c29c9460..3a17ee72b 100644 --- a/Telegram-Mac/ApplicationContext.swift +++ b/Telegram-Mac/ApplicationContext.swift @@ -526,6 +526,7 @@ final class AuthorizedApplicationContext: NSObject, SplitViewDelegate { #if DEBUG // var peerCall: PeerCallScreen? self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + showModal(with: FragmentUsernameController(context: context, peer: .init(context.myPeer!), username: "pro"), for: context.window) // showModal(with: TimeRangeSelectorController(context: context, from: .init(hours: 1, minutes: 30), to: .init(hours: 22, minutes: 30), title: "Monday", ok: "Save", fromString: "Opening Time", toString: "Closing Time"), for: context.window) // peerCall = PeerCallScreen(external: PeerCallArguments(engine: context.engine, peerId: context.peerId, makeAvatar: { view, peer in // let control = view as? AvatarControl ?? AvatarControl(font: .avatar(17)) diff --git a/Telegram-Mac/CallWindowController.swift b/Telegram-Mac/CallWindowController.swift index 8881e243f..c5a24d65b 100644 --- a/Telegram-Mac/CallWindowController.swift +++ b/Telegram-Mac/CallWindowController.swift @@ -1468,7 +1468,7 @@ func closeCall(minimisize: Bool = false) { private var peerCall: PeerCallScreen? func applyUIPCallResult(_ context: AccountContext, _ result:PCallResult) { - #if arch(arm64) + #if arch(arm64) && (BETA || DEBUG) callScreen(context, result) diff --git a/Telegram-Mac/FragmentUsernameController.swift b/Telegram-Mac/FragmentUsernameController.swift new file mode 100644 index 000000000..3dd97fdbd --- /dev/null +++ b/Telegram-Mac/FragmentUsernameController.swift @@ -0,0 +1,239 @@ +// +// FragmentUsernameController.swift +// Telegram +// +// Created by Mikhail Filimonov on 07.03.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import TGUIKit +import SwiftSignalKit + +private final class Arguments { + let context: AccountContext + init(context: AccountContext) { + self.context = context + } +} + +private struct State : Equatable { + var price: Int + var username: String + var peer: EnginePeer +} + +private final class RowItem : GeneralRowItem { + let peer: EnginePeer + let context: AccountContext + let price: Int + let username: String + let headerLayout: TextViewLayout + let infoLayout: TextViewLayout + init(_ initialSize: NSSize, stableId: AnyHashable, peer: EnginePeer, context: AccountContext, price: Int, username: String) { + self.peer = peer + self.context = context + self.price = price + self.username = username + + let headerText = "[@\(username)]() is a collectible\nusername that belongs to" + let attr = parseMarkdownIntoAttributedString(headerText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .medium(.text), textColor: theme.colors.text), bold: MarkdownAttributeSet(font: .bold(.text), textColor: theme.colors.text), link: MarkdownAttributeSet(font: .medium(.text), textColor: theme.colors.link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback("", { _ in + + })) + })) + + let infoText = "The @lean username was acquired on\nFragment on 1 Mar 2024 for \(clown) 6000 ($15200).\n\n[Copy Link]()" + let infoAttr = parseMarkdownIntoAttributedString(infoText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.text), bold: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.text), link: MarkdownAttributeSet(font: .medium(.title), textColor: theme.colors.link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback("", { _ in + + })) + })).mutableCopy() as! NSMutableAttributedString + let range = infoText.nsstring.range(of: clown) + + infoAttr.replaceCharacters(in: range, with: "") + infoAttr.insert(.embedded(name: "Icon_Reply_Group", color: theme.colors.text, resize: false), at: range.location) + + self.headerLayout = .init(attr, alignment: .center) + self.headerLayout.measure(width: initialSize.width - 40) + + + self.infoLayout = .init(infoAttr, alignment: .center) + self.infoLayout.measure(width: initialSize.width - 40) + + super.init(initialSize, stableId: stableId, viewType: .legacy) + } + + override var height: CGFloat { + var height: CGFloat = 70 + height += 20 + height += headerLayout.layoutSize.height + height += 20 + height += 30 + height += 20 + height += infoLayout.layoutSize.height + return height + } + override func viewClass() -> AnyClass { + return RowView.self + } +} + +private final class RowView: GeneralContainableRowView { + + private class PeerView : Control { + private let avatar = AvatarControl(font: .avatar(12)) + private let textView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(avatar) + addSubview(textView) + textView.userInteractionEnabled = false + textView.isSelectable = false + avatar.setFrameSize(NSMakeSize(30, 30)) + scaleOnClick = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(_ peer: EnginePeer, context: AccountContext, presentation: TelegramPresentationTheme, maxWidth: CGFloat) { + self.avatar.setPeer(account: context.account, peer: peer._asPeer()) + + let layout = TextViewLayout(.initialize(string: peer._asPeer().displayTitle, color: presentation.colors.text, font: .medium(.text))) + layout.measure(width: maxWidth - 40) + textView.update(layout) + self.backgroundColor = presentation.colors.background + + self.setFrameSize(NSMakeSize(layout.layoutSize.width + 10 + avatar.frame.width + 10, 30)) + + self.layer?.cornerRadius = frame.height / 2 + } + + override func layout() { + super.layout() + textView.centerY(x: avatar.frame.maxX + 10) + } + } + + private let peerView: PeerView = .init(frame: .zero) + private let iconView = View(frame: NSMakeRect(0, 0, 70, 70)) + private let stickerView = MediaAnimatedStickerView(frame: NSMakeRect(0, 0, 60, 60)) + private let headerView = TextView() + private let infoView = InteractiveTextView(frame: .zero) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(peerView) + addSubview(headerView) + addSubview(iconView) + addSubview(infoView) + iconView.addSubview(stickerView) + headerView.isSelectable = false + iconView.layer?.cornerRadius = iconView.frame.height / 2 + + infoView.textView.userInteractionEnabled = true + infoView.userInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + iconView.centerX(y: 0) + stickerView.center() + headerView.centerX(y: stickerView.frame.maxY + 20) + peerView.centerX(y: headerView.frame.maxY + 20) + infoView.centerX(y: peerView.frame.maxY + 20) + } + + override var backdorColor: NSColor { + return .clear + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + guard let item = item as? RowItem else { + return + } + infoView.set(text: item.infoLayout, context: item.context) + headerView.update(item.headerLayout) + peerView.update(item.peer, context: item.context, presentation: theme, maxWidth: item.blockWidth) + iconView.backgroundColor = theme.colors.accent + + stickerView.update(with: LocalAnimatedSticker.fragment_username.file, size: stickerView.frame.size, context: item.context, table: nil, parameters: LocalAnimatedSticker.fragment_username.parameters, animated: animated) + } +} + +private let _id_header = InputDataIdentifier("_id_header") + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_header, equatable: .init(state), comparable: nil, item: { initialSize, stableId in + return RowItem(initialSize, stableId: stableId, peer: state.peer, context: arguments.context, price: state.price, username: state.username)})) + + entries.append(.sectionId(sectionId, type: .customModern(10))) + sectionId += 1 + + return entries +} + +func FragmentUsernameController(context: AccountContext, peer: EnginePeer, username: String) -> InputDataModalController { + + let actionsDisposable = DisposableSet() + + let initialState = State(price: 6000, username: "lean", peer: peer) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + var close:(()->Void)? = nil + + let arguments = Arguments(context: context) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: "") + + controller.onDeinit = { + actionsDisposable.dispose() + } + + let modalInteractions = ModalInteractions(acceptTitle: "Learn More", accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController +} + + +/* + + */ + + diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 73ecfdf09..0d282d9fa 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260644 + 260661 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/InteractiveTextView.swift b/Telegram-Mac/InteractiveTextView.swift index bf648ca66..eb6e06d07 100644 --- a/Telegram-Mac/InteractiveTextView.swift +++ b/Telegram-Mac/InteractiveTextView.swift @@ -11,7 +11,7 @@ import TGUIKit import TelegramCore final class InteractiveTextView : Control { - private let textView = TextView() + let textView = TextView() var isLite: Bool = false diff --git a/Telegram-Mac/LottieLocalAnimations.swift b/Telegram-Mac/LottieLocalAnimations.swift index 58ed0fce0..f94a04a03 100644 --- a/Telegram-Mac/LottieLocalAnimations.swift +++ b/Telegram-Mac/LottieLocalAnimations.swift @@ -328,6 +328,8 @@ enum LocalAnimatedSticker : String { case business_location case business_quick_reply + case fragment_username + var file: TelegramMediaFile { let resource:LocalBundleResource = LocalBundleResource(name: self.rawValue, ext: "tgs") return TelegramMediaFile(fileId: MediaId(namespace: 0, id: MediaId.Id(resource.name.hashValue)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgsticker", size: nil, attributes: [.Sticker(displayText: "", packReference: nil, maskData: nil), .Animated, .FileName(fileName: "telegram-animoji.tgs")]) @@ -451,6 +453,8 @@ enum LocalAnimatedSticker : String { playPolicy = .onceEnd case .business_greeting_message: playPolicy = .onceEnd + case .fragment_username: + playPolicy = .onceEnd default: playPolicy = .loop hidePlayer = false diff --git a/Telegram-Mac/tgs/fragment_username.tgs b/Telegram-Mac/tgs/fragment_username.tgs new file mode 100644 index 0000000000000000000000000000000000000000..f93fd6d746df32fb00594bfe9e5eef882c6b92b2 GIT binary patch literal 4492 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 literal 0 HcmV?d00001 diff --git a/Telegram.xcodeproj/project.pbxproj b/Telegram.xcodeproj/project.pbxproj index 040fae6b3..b12e46a5b 100644 --- a/Telegram.xcodeproj/project.pbxproj +++ b/Telegram.xcodeproj/project.pbxproj @@ -421,6 +421,8 @@ 279BD40629B1D79000A8B640 /* ContextInlineWebViewRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279BD40529B1D79000A8B640 /* ContextInlineWebViewRowItem.swift */; }; 279BD40E29BB283700A8B640 /* menu_atsign.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 279BD40D29BB283700A8B640 /* menu_atsign.tgs */; }; 279BD41029BB2CFD00A8B640 /* InvitePrivacyLimitedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279BD40F29BB2CFD00A8B640 /* InvitePrivacyLimitedController.swift */; }; + 279DF45B2B999D7D00E644A7 /* FragmentUsernameController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279DF45A2B999D7D00E644A7 /* FragmentUsernameController.swift */; }; + 279DF45D2B99A29200E644A7 /* fragment_username.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 279DF45C2B99A29200E644A7 /* fragment_username.tgs */; }; 279E8ECB274259EE000E1BD5 /* TGVideoCameraMovie in Frameworks */ = {isa = PBXBuildFile; productRef = 279E8ECA274259EE000E1BD5 /* TGVideoCameraMovie */; }; 279E8ECE27425CBD000E1BD5 /* InAppVideoServices in Frameworks */ = {isa = PBXBuildFile; productRef = 279E8ECD27425CBD000E1BD5 /* InAppVideoServices */; }; 279FD2A0273C1234004F08A1 /* BuildConfig in Frameworks */ = {isa = PBXBuildFile; productRef = 279FD29F273C1234004F08A1 /* BuildConfig */; }; @@ -1941,6 +1943,8 @@ 279BD40B29B71FA200A8B640 /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = System/Library/Frameworks/Network.framework; sourceTree = SDKROOT; }; 279BD40D29BB283700A8B640 /* menu_atsign.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = menu_atsign.tgs; sourceTree = ""; }; 279BD40F29BB2CFD00A8B640 /* InvitePrivacyLimitedController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitePrivacyLimitedController.swift; sourceTree = ""; }; + 279DF45A2B999D7D00E644A7 /* FragmentUsernameController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FragmentUsernameController.swift; sourceTree = ""; }; + 279DF45C2B99A29200E644A7 /* fragment_username.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = fragment_username.tgs; sourceTree = ""; }; 279E8EC9274259B7000E1BD5 /* TGVideoCameraMovie */ = {isa = PBXFileReference; lastKnownFileType = folder; name = TGVideoCameraMovie; path = packages/TGVideoCameraMovie; sourceTree = ""; }; 279E8ECC27425BAE000E1BD5 /* InAppVideoServices */ = {isa = PBXFileReference; lastKnownFileType = folder; name = InAppVideoServices; path = packages/InAppVideoServices; sourceTree = ""; }; 279FD29C273C106C004F08A1 /* minizip */ = {isa = PBXFileReference; lastKnownFileType = folder; name = minizip; path = submodules/minizip; sourceTree = ""; }; @@ -5455,6 +5459,7 @@ 2751018029D6C0AD004CEFF3 /* EditBotUsernameController.swift */, 276790902AF3D11E00E9975C /* ChannelReactionsController.swift */, 2765535F2B68BB7B0007F2FB /* GroupEmojiPackController.swift */, + 279DF45A2B999D7D00E644A7 /* FragmentUsernameController.swift */, ); name = controllers; sourceTree = ""; @@ -5844,6 +5849,7 @@ 2750B8C72B887EB00035C37F /* business_location.tgs */, 2750B8BF2B887E9A0035C37F /* business_greeting_message.tgs */, 2750B8BE2B887E990035C37F /* business_hours.tgs */, + 279DF45C2B99A29200E644A7 /* fragment_username.tgs */, 2750B8C12B887E9C0035C37F /* business_quick_reply.tgs */, 27F4F17E28463BF000A3B4D4 /* premium_addone.tgs */, 27C50ADA2AC6F86D00FED2E4 /* premium_gift_3.tgs */, @@ -6534,6 +6540,7 @@ 27CB17522763284500DD25E0 /* menu_unblock.tgs in Resources */, 9FDE0A8F21AD41C2001546D7 /* emoji1014.txt in Resources */, 27336735276241820027441E /* menu_plus.tgs in Resources */, + 279DF45D2B99A29200E644A7 /* fragment_username.tgs in Resources */, 27CB17582763738E00DD25E0 /* menu_share.tgs in Resources */, 27803BF227A3E944007B0FA9 /* menu_folder_sport.tgs in Resources */, D07450F52340F28D00769D7F /* wallet_success_created.tgs in Resources */, @@ -6925,6 +6932,7 @@ 27A69765273D092C00B33415 /* CallWindowController.swift in Sources */, 27F7EEC0293F53E600B6E324 /* UserInfoResetPhotoItem.swift in Sources */, 27F2BBC727BE76D000A37463 /* Auth_Email.swift in Sources */, + 279DF45B2B999D7D00E644A7 /* FragmentUsernameController.swift in Sources */, 27A69764273D090F00B33415 /* SoundEffectPlayQueue.swift in Sources */, 27A69763273D08E900B33415 /* OngoingCallContext.swift in Sources */, 27A69762273D08B600B33415 /* PCallSession.swift in Sources */, diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 1f3c1c0e8..e65d999a0 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260644 + 260661 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/TGUIKit/Sources/TableView.swift b/packages/TGUIKit/Sources/TableView.swift index 51a02f852..d591f24e6 100644 --- a/packages/TGUIKit/Sources/TableView.swift +++ b/packages/TGUIKit/Sources/TableView.swift @@ -1997,19 +1997,17 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele self.tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row)) return - } else { - if let item = self.optionalItem(at: row) { - let animated = visibleRows().contains(row) && item.view != nil && animated - NSAnimationContext.current.duration = animated ? duration : 0.0 - NSAnimationContext.current.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.tableView.beginUpdates() - self.tableView.removeRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none]) - self.tableView.insertRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none]) - self.tableView.endUpdates() - } } } - + if let item = self.optionalItem(at: row) { + let animated = visibleRows().contains(row) && item.view != nil && animated + NSAnimationContext.current.duration = animated ? duration : 0.0 + NSAnimationContext.current.timingFunction = CAMediaTimingFunction(name: .easeOut) + self.tableView.beginUpdates() + self.tableView.removeRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none]) + self.tableView.insertRows(at: IndexSet(integer: row), withAnimation: animated ? options : [.none]) + self.tableView.endUpdates() + } } From 9947751ee4aa3a2495a4c6f9de60d80350672122 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 7 Mar 2024 17:11:12 +0400 Subject: [PATCH 46/50] - bugfixes --- Telegram-Mac/ChatListRowItem.swift | 2 +- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- packages/TGUIKit/Sources/TableView.swift | 7 ++++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Telegram-Mac/ChatListRowItem.swift b/Telegram-Mac/ChatListRowItem.swift index 3d4b50bb0..370f5f5a6 100644 --- a/Telegram-Mac/ChatListRowItem.swift +++ b/Telegram-Mac/ChatListRowItem.swift @@ -754,7 +754,7 @@ class ChatListRowItem: TableRowItem { var filtered: [ChatListFilter] = [] for tab in folders.tabs { if tab != filter { - if tab.contains(peer, groupId: groupId._asGroup(), isRemovedFromTotalUnreadCount: false, isUnread: readState?.count != 0, isContact: isContact) { + if tab.contains(peer, groupId: groupId._asGroup(), isRemovedFromTotalUnreadCount: isMuted, isUnread: readState?.count != 0, isContact: isContact) { filtered.append(tab) } } diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 0d282d9fa..3a54ba676 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260661 + 260663 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index e65d999a0..ee4528733 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260661 + 260663 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/TGUIKit/Sources/TableView.swift b/packages/TGUIKit/Sources/TableView.swift index d591f24e6..c382b6914 100644 --- a/packages/TGUIKit/Sources/TableView.swift +++ b/packages/TGUIKit/Sources/TableView.swift @@ -2649,7 +2649,12 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele resetScrollNotifies() if transition.isEmpty { - return + switch transition.state { + case .none, .saveVisible: + return + default: + break + } } let previousList = self.list From 2da18dbec76d78396bcb25db9352d9b824fbfbe2 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 7 Mar 2024 17:13:46 +0400 Subject: [PATCH 47/50] - bugfixes --- Telegram-Mac/CallWindowController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Telegram-Mac/CallWindowController.swift b/Telegram-Mac/CallWindowController.swift index c5a24d65b..c323c40c2 100644 --- a/Telegram-Mac/CallWindowController.swift +++ b/Telegram-Mac/CallWindowController.swift @@ -1400,7 +1400,7 @@ func makeKeyAndOrderFrontCallWindow() -> Bool { func showCallWindow(_ session:PCallSession) { - #if DEBUG + #if arch(arm64) && (BETA || DEBUG) callScreen(session.accountContext, .success(session)) return #endif From a5f87e573a5260be64c2d277390efe6d8d76bf93 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Fri, 8 Mar 2024 18:08:01 +0400 Subject: [PATCH 48/50] - bugfixes --- Telegram-Mac/ApplicationContext.swift | 26 +- .../menu_search.imageset/Contents.json | 22 + .../menu_search.imageset/menu_search.png | Bin 0 -> 311 bytes .../menu_search.imageset/menu_search@2x.png | Bin 0 -> 442 bytes ...aticBusinessMessageSetupChatContents.swift | 4 +- Telegram-Mac/ChatHeaderController.swift | 4 +- Telegram-Mac/CoreExtension.swift | 20 + .../FragmentMonetizationController.swift | 778 ++++++++++++++++++ .../FragmentMonetizationPromoController.swift | 319 +++++++ Telegram-Mac/FragmentUsernameController.swift | 22 +- Telegram-Mac/GeneralRowItem.swift | 12 +- Telegram-Mac/InAppLinks.swift | 11 +- Telegram-Mac/Info.plist | 2 +- Telegram-Mac/LottieLocalAnimations.swift | 1 + Telegram-Mac/PeerRequestJoinRowItem.swift | 56 +- .../RequestJoinMemberListController.swift | 55 +- Telegram-Mac/WebpageModalController.swift | 4 + Telegram-Mac/en.lproj/Localizable.strings | Bin 857252 -> 857514 bytes Telegram-Mac/tgs/menu/menu_search.tgs | Bin 0 -> 801 bytes Telegram.xcodeproj/project.pbxproj | 22 +- TelegramShare/Info.plist | 2 +- .../Sources/Localization/Localizable.swift | 6 + packages/TGUIKit/Sources/TableView.swift | 9 +- 23 files changed, 1303 insertions(+), 72 deletions(-) create mode 100644 Telegram-Mac/Assets.xcassets/menu_search.imageset/Contents.json create mode 100644 Telegram-Mac/Assets.xcassets/menu_search.imageset/menu_search.png create mode 100644 Telegram-Mac/Assets.xcassets/menu_search.imageset/menu_search@2x.png create mode 100644 Telegram-Mac/FragmentMonetizationController.swift create mode 100644 Telegram-Mac/FragmentMonetizationPromoController.swift create mode 100644 Telegram-Mac/tgs/menu/menu_search.tgs diff --git a/Telegram-Mac/ApplicationContext.swift b/Telegram-Mac/ApplicationContext.swift index 3a17ee72b..b892160ed 100644 --- a/Telegram-Mac/ApplicationContext.swift +++ b/Telegram-Mac/ApplicationContext.swift @@ -524,23 +524,21 @@ final class AuthorizedApplicationContext: NSObject, SplitViewDelegate { #if DEBUG - // var peerCall: PeerCallScreen? - self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in - showModal(with: FragmentUsernameController(context: context, peer: .init(context.myPeer!), username: "pro"), for: context.window) -// showModal(with: TimeRangeSelectorController(context: context, from: .init(hours: 1, minutes: 30), to: .init(hours: 22, minutes: 30), title: "Monday", ok: "Save", fromString: "Opening Time", toString: "Closing Time"), for: context.window) -// peerCall = PeerCallScreen(external: PeerCallArguments(engine: context.engine, peerId: context.peerId, makeAvatar: { view, peer in -// let control = view as? AvatarControl ?? AvatarControl(font: .avatar(17)) -// control.setFrameSize(NSMakeSize(120, 120)) -// control.userInteractionEnabled = false -// control.setPeer(account: context.account, peer: peer) -// return control -// })) -// peerCall?.show() -// context.bindings.rootNavigation().push(ChatListController(context, modal: false, mode: .savedMessagesChats)) - //setCustomAppIcon(fromPath: "/Users/mikerenoir/Downloads/AppIcon.icns") + self.context.window.set(handler: { _ -> KeyHandlerResult in + showModal(with: FragmentMonetizationPromoController(context: context, peerId: context.peerId), for: context.window) return .invoked }, with: self, for: .T, priority: .supreme, modifierFlags: [.command]) + self.context.window.set(handler: { _ -> KeyHandlerResult in + showModal(with: FragmentUsernameController(context: context, peer: .init(context.myPeer!), username: "news"), for: context.window) + return .invoked + }, with: self, for: .Y, priority: .supreme, modifierFlags: [.command]) + + self.context.window.set(handler: { _ -> KeyHandlerResult in + context.bindings.rootNavigation().push(FragmentMonetizationController(context: context, peerId: context.peerId)) + return .invoked + }, with: self, for: .R, priority: .supreme, modifierFlags: [.command]) + #endif diff --git a/Telegram-Mac/Assets.xcassets/menu_search.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/menu_search.imageset/Contents.json new file mode 100644 index 000000000..e3b18ffed --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/menu_search.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "menu_search.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "menu_search@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/menu_search.imageset/menu_search.png b/Telegram-Mac/Assets.xcassets/menu_search.imageset/menu_search.png new file mode 100644 index 0000000000000000000000000000000000000000..0f5496d1bbe752364e509c6b3ca67a521f6927ba GIT binary patch literal 311 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|&H|6fVg?3oVGw3ym^DWND9BhG z|E_O; z<;ZEi^onJ(o=KDRSM#0ApNR&|2on+7QN3cKSHus)WliZ{9K|ynbP`u>WDikaI?tQa z`+~sLevV&Zm$xOV1#wo#pJRAyamjks>Ib$5|9@bKNK$=xeJ`sR&|eImu6{1-oD!M< DIP-I( literal 0 HcmV?d00001 diff --git a/Telegram-Mac/Assets.xcassets/menu_search.imageset/menu_search@2x.png b/Telegram-Mac/Assets.xcassets/menu_search.imageset/menu_search@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..61799b304fd2293f043a72b41452979c0278778a GIT binary patch literal 442 zcmV;r0Y(0aP)_kifg3a%C_IlBB#JS2ME~IT-uE!(fI9-m z0hLOnEH9Ww<7Au^{qCH0qz*Hpoqn7S8EweX;(R0%d&kyJmfFzKG@%tpM42qT_7Q=d|rT<$D>!I&CoKiTi zcP`00rmqBX( NSAttributedString { @@ -3708,6 +3718,7 @@ extension NSAttributedString { return attach } + static func embedded(name: String, color: NSColor, resize: Bool) -> NSAttributedString { let file = TelegramMediaFile(fileId: .init(namespace: 0, id: 0), partialReference: nil, resource: LocalBundleResource(name: name, ext: "", color: color, resize: resize), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "bundle/jpeg", size: nil, attributes: []) @@ -3721,6 +3732,15 @@ extension NSAttributedString { return attr } + + static func embeddedAnimated(_ file: TelegramMediaFile) -> NSAttributedString { + let attr = NSMutableAttributedString() + + let emoji: String = clown + attr.append(string: emoji) + attr.addAttribute(TextInputAttributes.embedded, value: InlineStickerItem(source: .attribute(.init(fileId: file.fileId.id, file: file, emoji: emoji))), range: NSMakeRange(0, emoji.length)) + return attr + } } diff --git a/Telegram-Mac/FragmentMonetizationController.swift b/Telegram-Mac/FragmentMonetizationController.swift new file mode 100644 index 000000000..62fa59d2f --- /dev/null +++ b/Telegram-Mac/FragmentMonetizationController.swift @@ -0,0 +1,778 @@ +// +// FragmentMonetizationController.swift +// Telegram +// +// Created by Mikhail Filimonov on 07.03.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import Postbox +import TGUIKit +import SwiftSignalKit +import CurrencyFormat +import InputView + + +private func insertSymbolIntoMiddle(of string: String, with symbol: Character) -> String { + var modifiedString = string + let middleIndex = modifiedString.index(modifiedString.startIndex, offsetBy: modifiedString.count / 2) + modifiedString.insert(contentsOf: [symbol], at: middleIndex) + return modifiedString +} + + +private extension String { + var pretty: String { + let range = self.nsstring.range(of: ".") + if range.location != NSNotFound { + return self.nsstring.substring(to: min(range.location + 3, self.length)) + } + return self + } +} + +private extension NSAttributedString { + var smallDecemial: NSAttributedString { + let range = self.string.nsstring.range(of: ".") + if range.location != NSNotFound { + let attr = self.mutableCopy() as! NSMutableAttributedString + + let font = attr.attribute(.font, at: 0, effectiveRange: nil) as? NSFont + if let font = font, let updated = NSFont(name: font.fontName, size: font.pointSize / 1.5) { + attr.addAttribute(.font, value: updated, range: NSMakeRange(range.location, attr.range.length - range.lowerBound)) + } + return attr + + } else { + return self + } + + } +} + +private final class TransactionRowItem : GeneralRowItem { + let context: AccountContext + let transaction: State.Transaction + let title: TextViewLayout + let address: TextViewLayout? + let date: TextViewLayout + let amount: TextViewLayout + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, transaction: State.Transaction, viewType: GeneralViewType) { + self.context = context + self.transaction = transaction + + let titleText: String + switch transaction.source { + case .incoming: + titleText = "Proceeds from Ads" + case .withdraw: + titleText = "Balance Withdrawal to" + } + + self.title = .init(.initialize(string: titleText, color: theme.colors.text, font: .normal(.title)), maximumNumberOfLines: 1) + + switch transaction.source { + case .incoming: + address = nil + case var .withdraw(wallet): + wallet = insertSymbolIntoMiddle(of: wallet, with: "\n") + address = .init(.initialize(string: wallet, color: theme.colors.text, font: .code(.text))) + } + self.date = .init(.initialize(string: stringForFullDate(timestamp: transaction.date), color: theme.colors.grayText, font: .normal(.text))) + + let amountAttr = NSMutableAttributedString() + let justAmount = NSAttributedString.initialize(string: formatCurrencyAmount(transaction.amount, currency: "TON").pretty, color: theme.colors.text, font: .medium(.header)).smallDecemial + amountAttr.append(justAmount) + amountAttr.append(string: " TON", color: theme.colors.text, font: .medium(.header)) + switch transaction.source { + case .incoming: + amountAttr.insert(.initialize(string: "+", font: .medium(.header)), at: 0) + amountAttr.addAttribute(.foregroundColor, value: theme.colors.greenUI, range: amountAttr.range) + case .withdraw: + amountAttr.insert(.initialize(string: "-", font: .medium(.header)), at: 0) + amountAttr.addAttribute(.foregroundColor, value: theme.colors.redUI, range: amountAttr.range) + } + + self.amount = .init(amountAttr) + + + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + self.amount.measure(width: blockWidth) + self.title.measure(width: blockWidth - self.amount.layoutSize.width - viewType.innerInset.left - viewType.innerInset.right) + self.date.measure(width: blockWidth - self.amount.layoutSize.width - viewType.innerInset.left - viewType.innerInset.right) + self.address?.measure(width: blockWidth - self.amount.layoutSize.width - viewType.innerInset.left - viewType.innerInset.right) + + return true + } + + override func viewClass() -> AnyClass { + return TransactioRowView.self + } + + override var height: CGFloat { + var height: CGFloat = 0 + + height += viewType.innerInset.top + height += title.layoutSize.height + height += 3 + if let address { + height += address.layoutSize.height + height += 3 + } + height += date.layoutSize.height + + height += viewType.innerInset.bottom + return height + } +} + +private final class TransactioRowView : GeneralContainableRowView { + private let titleView = TextView() + private let dateView = TextView() + private let amountView = TextView() + private var addressView: TextView? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(titleView) + addSubview(dateView) + addSubview(amountView) + + titleView.userInteractionEnabled = false + titleView.isSelectable = false + + dateView.userInteractionEnabled = false + dateView.isSelectable = false + + amountView.userInteractionEnabled = false + amountView.isSelectable = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + + guard let item = item as? TransactionRowItem else { + return + } + + titleView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, item.viewType.innerInset.top)) + + var dateY: CGFloat = titleView.frame.maxY + 3 + if let addressView { + addressView.setFrameOrigin(NSMakePoint(titleView.frame.minX, titleView.frame.maxY + 3)) + dateY = addressView.frame.maxY + 3 + } + dateView.setFrameOrigin(NSMakePoint(titleView.frame.minX, dateY)) + + amountView.centerY(x: containerView.frame.width - amountView.frame.width - item.viewType.innerInset.right) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? TransactionRowItem else { + return + } + + titleView.update(item.title) + amountView.update(item.amount) + dateView.update(item.date) + + if let address = item.address { + let current: TextView + if let view = self.addressView { + current = view + } else { + current = TextView() + current.userInteractionEnabled = false + current.isSelectable = false + addSubview(current) + self.addressView = current + } + current.update(address) + } else if let view = self.addressView { + performSubviewRemoval(view, animated: animated) + self.addressView = nil + } + + needsLayout = true + } +} + +private final class OverviewRowItem : GeneralRowItem { + + struct Overview: Equatable { + let tonAmount: Int64 + let usdAmount: Int64 + let info: String + } + + let context: AccountContext + + let overview: Overview + + let amount: TextViewLayout + let info: TextViewLayout + + + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, overview: Overview, viewType: GeneralViewType) { + self.context = context + self.overview = overview + + + let amountAttr = NSMutableAttributedString() + let justAmount = NSAttributedString.initialize(string: formatCurrencyAmount(overview.tonAmount, currency: "TON").pretty, color: theme.colors.text, font: .medium(.header)).smallDecemial + amountAttr.append(justAmount) + amountAttr.append(string: " ~", color: theme.colors.grayText, font: .normal(.text)) + + let justAmount2 = NSAttributedString.initialize(string: formatCurrencyAmount(overview.usdAmount, currency: "USD").pretty, color: theme.colors.grayText, font: .normal(.text)).smallDecemial + amountAttr.append(justAmount2) + + self.amount = .init(amountAttr) + + self.info = .init(.initialize(string: overview.info, color: theme.colors.grayText, font: .normal(.text))) + + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + amount.measure(width: blockWidth - viewType.innerInset.left - viewType.innerInset.right - 22) + info.measure(width: blockWidth - viewType.innerInset.left - viewType.innerInset.right) + return true + } + + override func viewClass() -> AnyClass { + return OverviewRowView.self + } + + override var height: CGFloat { + var height: CGFloat = 0 + height += viewType.innerInset.top + height += amount.layoutSize.height + height += 2 + height += info.layoutSize.height + + height += viewType.innerInset.bottom + return height + } +} + +private final class OverviewRowView : GeneralContainableRowView { + private let amountView = TextView() + private let infoView = TextView() + private let icon = MediaAnimatedStickerView(frame: NSMakeRect(0, 0, 20, 20)) + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(amountView) + addSubview(infoView) + addSubview(icon) + + amountView.userInteractionEnabled = false + amountView.isSelectable = false + + infoView.userInteractionEnabled = false + infoView.isSelectable = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func layout() { + super.layout() + guard let item = item as? OverviewRowItem else { + return + } + icon.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, item.viewType.innerInset.top - 3)) + amountView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left + 22, item.viewType.innerInset.top)) + infoView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, amountView.frame.maxY + 2)) + + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? OverviewRowItem else { + return + } + + icon.update(with: LocalAnimatedSticker.brilliant_static.file, size: icon.frame.size, context: item.context, table: item.table, parameters: LocalAnimatedSticker.brilliant_static.parameters, animated: animated) + + amountView.update(item.amount) + infoView.update(item.info) + } +} + +private final class BalanceRowItem : GeneralRowItem { + let context: AccountContext + let tonBalance: TextViewLayout + let usdBalance: TextViewLayout + let balance: State.Balance + + fileprivate let interactions: TextView_Interactions + fileprivate let updateState:(Updated_ChatTextInputState)->Void + + + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, balance: State.Balance, viewType: GeneralViewType, interactions: TextView_Interactions, updateState:@escaping(Updated_ChatTextInputState)->Void, transfer:@escaping()->Void) { + self.context = context + self.balance = balance + self.interactions = interactions + self.updateState = updateState + + let tonBalance = NSAttributedString.initialize(string: formatCurrencyAmount(balance.ton, currency: "TON").pretty, color: theme.colors.text, font: .medium(40)).smallDecemial + let usdBalance = NSAttributedString.initialize(string: "~" + formatCurrencyAmount(balance.usd, currency: "USD").pretty, color: theme.colors.grayText, font: .normal(.text)).smallDecemial + + self.tonBalance = .init(tonBalance) + self.usdBalance = .init(usdBalance) + + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + var withdrawText: String { + return "Transfer \(formatCurrencyAmount(balance.ton, currency: "TON").pretty) TON" + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + self.tonBalance.measure(width: blockWidth - viewType.innerInset.left - viewType.innerInset.right) + self.usdBalance.measure(width: blockWidth - viewType.innerInset.left - viewType.innerInset.right) + + return true + } + + override func viewClass() -> AnyClass { + return BalanceRowView.self + } + + var inputHeight: CGFloat { + let attr = NSMutableAttributedString() + attr.append(self.interactions.presentation.inputText) + let str: NSMutableAttributedString = .init() + attr.addAttribute(.font, value: NSFont.normal(.text), range: attr.range) + let size = attr.sizeFittingWidth(blockWidth - viewType.innerInset.left - viewType.innerInset.right - 20) + return max(40, size.height + 24) + } + + override var height: CGFloat { + var height: CGFloat = 0 + height += viewType.innerInset.top + + height += 46 + height -= 3 + height += usdBalance.layoutSize.height + + + if balance.ton > 0 { + height += viewType.innerInset.top + height += 40 + + height += viewType.innerInset.top * 2 + height += inputHeight + } + + height += viewType.innerInset.left + return height + } +} + +private final class BalanceRowView : GeneralContainableRowView { + + + private final class WithdrawInput : View { + + private weak var item: BalanceRowItem? + let inputView: UITextView = UITextView(frame: NSMakeRect(0, 0, 100, 40)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(inputView) + + inputView.placeholder = "Enter your TON address" + + layer?.cornerRadius = 10 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(item: BalanceRowItem, animated: Bool) { + self.item = item + self.backgroundColor = theme.colors.grayForeground.withAlphaComponent(0.6) + + + inputView.context = item.context + inputView.interactions.max_height = 500 + inputView.interactions.min_height = 13 + inputView.interactions.emojiPlayPolicy = .onceEnd + inputView.interactions.canTransform = false + + item.interactions.min_height = 13 + item.interactions.max_height = 500 + item.interactions.emojiPlayPolicy = .onceEnd + item.interactions.canTransform = false + + + + + item.interactions.filterEvent = { event in + return true + } + + self.inputView.set(item.interactions.presentation.textInputState()) + + self.inputView.interactions = item.interactions + + item.interactions.inputDidUpdate = { [weak self] state in + guard let `self` = self else { + return + } + self.set(state) + self.inputDidUpdateLayout(animated: true) + } + + } + + + var textWidth: CGFloat { + return frame.width - 20 + } + + func textViewSize() -> (NSSize, CGFloat) { + let w = textWidth + let height = inputView.height(for: w) + return (NSMakeSize(w, min(max(height, inputView.min_height), inputView.max_height)), height) + } + + private func inputDidUpdateLayout(animated: Bool) { + self.updateLayout(size: frame.size, transition: animated ? .animated(duration: 0.2, curve: .easeOut) : .immediate) + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + guard let item else { + return + } + + let (textSize, textHeight) = textViewSize() + + transition.updateFrame(view: inputView, frame: CGRect(origin: CGPoint(x: 10, y: 7), size: textSize)) + inputView.updateLayout(size: textSize, textHeight: textHeight, transition: transition) + } + + private func set(_ state: Updated_ChatTextInputState) { + guard let item else { + return + } + item.updateState(state) + + item.redraw(animated: true) + } + + + } + + private let tonBalanceView = TextView() + private let tonBalanceContainer = View() + private let tonView = MediaAnimatedStickerView(frame: NSMakeRect(0, 0, 46, 46)) + private let usdBalanceView = TextView() + + private var withdrawAction: TextButton? + private var withdrawInput: WithdrawInput? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + tonBalanceContainer.addSubview(tonView) + tonBalanceContainer.addSubview(tonBalanceView) + addSubview(tonBalanceContainer) + addSubview(usdBalanceView) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + super.updateLayout(size: size, transition: transition) + + guard let item = item as? BalanceRowItem else { + return + } + + transition.updateFrame(view: tonBalanceContainer, frame: tonBalanceContainer.centerFrameX(y: item.viewType.innerInset.top)) + transition.updateFrame(view: tonView, frame: tonView.centerFrameY(x: 0, addition: -3)) + transition.updateFrame(view: tonBalanceView, frame: tonBalanceView.centerFrameY(x: tonView.frame.maxX)) + + + transition.updateFrame(view: usdBalanceView, frame: usdBalanceView.centerFrameX(y: tonBalanceContainer.frame.maxY - 4)) + + var withdrawY: CGFloat = containerView.frame.height - item.viewType.innerInset.left + + if let withdrawAction { + withdrawY -= withdrawAction.frame.height + transition.updateFrame(view: withdrawAction, frame: CGRect(origin: NSMakePoint(item.viewType.innerInset.left, withdrawY), size: withdrawAction.frame.size)) + withdrawY -= item.viewType.innerInset.left + } + if let withdrawInput { + withdrawY -= withdrawInput.frame.height + transition.updateFrame(view: withdrawInput, frame: CGRect(origin: NSMakePoint(item.viewType.innerInset.left, withdrawY), size: NSMakeSize(containerView.frame.width - item.viewType.innerInset.left - item.viewType.innerInset.right, item.inputHeight))) + withdrawInput.updateLayout(size: withdrawInput.frame.size, transition: transition) + } + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? BalanceRowItem else { + return + } + + usdBalanceView.update(item.usdBalance) + tonBalanceView.update(item.tonBalance) + tonView.update(with: LocalAnimatedSticker.brilliant_static.file, size: tonView.frame.size, context: item.context, table: item.table, parameters: LocalAnimatedSticker.brilliant_static.parameters, animated: animated) + + tonBalanceContainer.setFrameSize(tonBalanceContainer.subviewsWidthSize) + + if item.balance.ton > 0 { + let currentAction: TextButton + if let withdrawAction { + currentAction = withdrawAction + } else { + currentAction = TextButton() + currentAction.scaleOnClick = true + currentAction.autohighlight = false + currentAction.set(font: .medium(.text), for: .Normal) + addSubview(currentAction) + self.withdrawAction = currentAction + } + + let blockWidth = item.blockWidth - item.viewType.innerInset.left - item.viewType.innerInset.right + + currentAction.set(color: theme.colors.underSelectedColor, for: .Normal) + currentAction.set(background: theme.colors.accent, for: .Normal) + currentAction.layer?.cornerRadius = 10 + currentAction.set(text: item.withdrawText, for: .Normal) + currentAction.sizeToFit(.zero, NSMakeSize(blockWidth, 40), thatFit: true) + + + let currentInput: WithdrawInput + if let withdrawInput { + currentInput = withdrawInput + } else { + currentInput = WithdrawInput(frame: NSMakeRect(0, 0, blockWidth, item.inputHeight)) + addSubview(currentInput) + self.withdrawInput = currentInput + } + currentInput.update(item: item, animated: animated) + + } else { + if let withdrawInput { + performSubviewRemoval(withdrawInput, animated: animated) + self.withdrawInput = nil + } + if let withdrawAction { + performSubviewRemoval(withdrawAction, animated: animated) + self.withdrawAction = nil + } + } + + updateLayout(size: self.frame.size, transition: animated ? .animated(duration: 0.2, curve: .easeOut) : .immediate) + } + + override var firstResponder: NSResponder? { + return withdrawInput?.inputView.inputView + } +} + +private final class Arguments { + let context: AccountContext + let interactions: TextView_Interactions + let updateState:(Updated_ChatTextInputState)->Void + init(context: AccountContext, interactions: TextView_Interactions, updateState:@escaping(Updated_ChatTextInputState)->Void) { + self.context = context + self.interactions = interactions + self.updateState = updateState + } +} + +private struct State : Equatable { + + struct Transaction : Equatable { + enum Source : Equatable { + case incoming + case withdraw(String) + } + let date: Int32 + let source: Source + let amount: Int64 + let uniqueId: Int64 + } + struct Balance : Equatable { + var ton: Int64 + var usd: Int64 + } + struct Overview : Equatable { + let balance: Balance + let last: Balance + let all: Balance + } + + + var overview: Overview + var balance: Balance + var transactions: [Transaction] + +} + +private let _id_overview = InputDataIdentifier("_id_overview") +private let _id_balance = InputDataIdentifier("_id_balance") +private let _id_transaction = InputDataIdentifier("_id_transaction") + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + do { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + struct Tuple : Equatable { + let overview: OverviewRowItem.Overview + let viewType: GeneralViewType + } + + let tuples: [Tuple] = [.init(overview: .init(tonAmount: state.overview.balance.ton, usdAmount: state.overview.balance.usd, info: "Balance Available to Withdraw"), viewType: .firstItem), + .init(overview: .init(tonAmount: state.overview.last.ton, usdAmount: state.overview.last.usd, info: "Proceeds Since Last Withdrawal"), viewType: .innerItem), + .init(overview: .init(tonAmount: state.overview.all.ton, usdAmount: state.overview.all.usd, info: "Total Lifetime Proceeds"), viewType: .lastItem)] + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("PROCEEDS OVERVIEW"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + for tuple in tuples { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_overview, equatable: .init(tuple), comparable: nil, item: { initialSize, stableId in + return OverviewRowItem(initialSize, stableId: stableId, context: arguments.context, overview: tuple.overview, viewType: tuple.viewType) + })) + } + + } + + + do { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("AVAILABLE BALANCE"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_balance, equatable: .init(state), comparable: nil, item: { initialSize, stableId in + return BalanceRowItem(initialSize, stableId: stableId, context: arguments.context, balance: state.balance, viewType: .singleItem, interactions: arguments.interactions, updateState: arguments.updateState, transfer: { + + }) + })) + entries.append(.desc(sectionId: sectionId, index: index, text: .markdown("We will transfer your balance to the TON wallet address you specify. [Learn More >]()", linkHandler: { _ in + + }), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + } + + + + do { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("TRANSACTION HISTORY"), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + struct Tuple : Equatable { + let transaction: State.Transaction + let viewType: GeneralViewType + } + var tuples: [Tuple] = [] + for (i, transaction) in state.transactions.enumerated() { + tuples.append(.init(transaction: transaction, viewType: bestGeneralViewType(state.transactions, for: i))) + } + + for tuple in tuples { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_transaction, equatable: .init(tuple), comparable: nil, item: { initialSize, stableId in + return TransactionRowItem(initialSize, stableId: stableId, context: arguments.context, transaction: tuple.transaction, viewType: tuple.viewType) + })) + } + } + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func FragmentMonetizationController(context: AccountContext, peerId: PeerId) -> InputDataController { + + let actionsDisposable = DisposableSet() + + let initialState = State(overview: .init(balance: .init(ton: 10000000000, usd: 1000), last: .init(ton: 10000000000, usd: 100), all: .init(ton: 10000000000, usd: 10000)), balance: .init(ton: 10000000000, usd: 10000), transactions: [.init(date: Int32(Date().timeIntervalSince1970), source: .incoming, amount: 10000000000, uniqueId: arc4random64()), .init(date: Int32(Date().timeIntervalSince1970), source: .withdraw("UQCMOXxD-f8LSWWbXQowKxqTr3zMY-X1wMTyWp3B-LR6syif"), amount: 10000000000, uniqueId: arc4random64())]) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let textInteractions = TextView_Interactions() + + + textInteractions.processEnter = { event in + return false + } + textInteractions.processAttriburedCopy = { attributedString in + return globalLinkExecutor.copyAttributedString(attributedString) + } + textInteractions.processPaste = { pasteboard in + if let data = pasteboard.data(forType: .kInApp) { + let decoder = AdaptedPostboxDecoder() + if let decoded = try? decoder.decode(ChatTextInputState.self, from: data) { + let state = decoded.unique(isPremium: true) + textInteractions.update { _ in + return textInteractions.insertText(state.attributedString()) + } + return true + } + } + return false + } + + let arguments = Arguments(context: context, interactions: textInteractions, updateState: { state in + textInteractions.update { _ in + return state + } + }) + + let signal = statePromise.get() |> deliverOnMainQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: "Monetization", hasDone: false) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + return controller + +} diff --git a/Telegram-Mac/FragmentMonetizationPromoController.swift b/Telegram-Mac/FragmentMonetizationPromoController.swift new file mode 100644 index 000000000..8fc8c452e --- /dev/null +++ b/Telegram-Mac/FragmentMonetizationPromoController.swift @@ -0,0 +1,319 @@ +// +// FragmentMonetizationPromo.swift +// Telegram +// +// Created by Mikhail Filimonov on 07.03.2024. +// Copyright © 2024 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import Postbox +import TGUIKit +import SwiftSignalKit + +private final class Arguments { + let context: AccountContext + init(context: AccountContext) { + self.context = context + } +} + +private struct State : Equatable { + +} + + +private final class RowItem : GeneralRowItem { + + struct Option { + let image: CGImage + let header: TextViewLayout + let text: TextViewLayout + let width: CGFloat + init(image: CGImage, header: TextViewLayout, text: TextViewLayout, width: CGFloat) { + self.image = image + self.header = header + self.text = text + self.width = width + self.header.measure(width: width - 40) + self.text.measure(width: width - 40) + } + var size: NSSize { + return NSMakeSize(width, header.layoutSize.height + 5 + text.layoutSize.height) + } + } + let context: AccountContext + let headerLayout: TextViewLayout + let infoLayout: TextViewLayout + let infoHeaderLayout: TextViewLayout + + let options: [Option] + + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext) { + self.context = context + + let headerText = NSAttributedString.initialize(string: "Earn From Your Channel", color: theme.colors.text, font: .medium(.title)) + + let infoHeaderAttr = NSAttributedString.initialize(string: "What's \(clown) TON", color: theme.colors.text, font: .medium(.title)).mutableCopy() as! NSMutableAttributedString + infoHeaderAttr.insertEmbedded(.embeddedAnimated(LocalAnimatedSticker.brilliant_static.file), for: clown) + + + let infoText = "TON is a blockchain platform and cryptocurrency that Telegram uses for its record scalability and ultra low commissions on transactions.\n[Learn More >]()" + + let infoAttr = parseMarkdownIntoAttributedString(infoText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.text), bold: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.text), link: MarkdownAttributeSet(font: .medium(.title), textColor: theme.colors.link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback("", { _ in + + })) + })).mutableCopy() as! NSMutableAttributedString + + + self.headerLayout = .init(headerText, alignment: .center) + self.headerLayout.measure(width: initialSize.width - 40) + + self.infoHeaderLayout = .init(infoHeaderAttr, alignment: .center) + self.infoHeaderLayout.measure(width: initialSize.width - 80) + + self.infoLayout = .init(infoAttr, alignment: .center) + self.infoLayout.measure(width: initialSize.width - 80) + + var options:[Option] = [] + + options.append(.init(image: theme.icons.channel_feature_link_icon, header: .init(.initialize(string: "Telegram Ads", color: theme.colors.text, font: .medium(.text))), text: .init(.initialize(string: "Telegram can display ads in your channel.", color: theme.colors.grayText, font: .normal(.text))), width: initialSize.width - 40)) + + options.append(.init(image: theme.icons.channel_feature_link_icon, header: .init(.initialize(string: "50:50 revenue split", color: theme.colors.text, font: .medium(.text))), text: .init(.initialize(string: "You receive 50% of the ad revenue in TON.", color: theme.colors.grayText, font: .normal(.text))), width: initialSize.width - 40)) + + + options.append(.init(image: theme.icons.channel_feature_link_icon, header: .init(.initialize(string: "Flexible withdrawals", color: theme.colors.text, font: .medium(.text))), text: .init(.initialize(string: "You can withdraw your TON any time.", color: theme.colors.grayText, font: .normal(.text))), width: initialSize.width - 40)) + + + self.options = options + + super.init(initialSize, stableId: stableId, viewType: .legacy) + } + + override var height: CGFloat { + var height: CGFloat = 70 + height += 20 + height += headerLayout.layoutSize.height + height += 20 + for option in options { + height += option.size.height + height += 20 + } + //block + height += 20 + height += infoHeaderLayout.layoutSize.height + height += 10 + height += infoLayout.layoutSize.height + height += 10 + + return height + } + override func viewClass() -> AnyClass { + return RowView.self + } +} + +private final class RowView: GeneralContainableRowView { + + final class OptionView : View { + private let imageView = ImageView() + private let titleView = TextView() + private let infoView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(titleView) + addSubview(infoView) + + titleView.userInteractionEnabled = false + titleView.isSelectable = false + + infoView.userInteractionEnabled = false + infoView.isSelectable = false + } + + func update(option: RowItem.Option) { + self.titleView.update(option.header) + self.infoView.update(option.text) + self.imageView.image = option.image + self.imageView.sizeToFit() + } + + override func layout() { + super.layout() + titleView.setFrameOrigin(NSMakePoint(40, 0)) + infoView.setFrameOrigin(NSMakePoint(40, titleView.frame.maxY + 5)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + private let iconView = View(frame: NSMakeRect(0, 0, 70, 70)) + private let stickerView = MediaAnimatedStickerView(frame: NSMakeRect(0, 0, 60, 60)) + private let headerView = TextView() + + private let infoBlock = View() + private let infoHeaderView = InteractiveTextView(frame: .zero) + private let infoView = InteractiveTextView(frame: .zero) + + private let optionsView = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(headerView) + addSubview(iconView) + infoBlock.addSubview(infoHeaderView) + infoBlock.addSubview(infoView) + addSubview(infoBlock) + iconView.addSubview(stickerView) + + addSubview(optionsView) + + headerView.isSelectable = false + iconView.layer?.cornerRadius = iconView.frame.height / 2 + + infoView.textView.userInteractionEnabled = true + infoView.userInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + iconView.centerX(y: 0) + stickerView.center() + headerView.centerX(y: stickerView.frame.maxY + 20) + + optionsView.centerX(y: headerView.frame.maxY + 20) + + infoBlock.centerX(y: optionsView.frame.maxY + 20) + + infoHeaderView.centerX(y: 10) + infoView.centerX(y: infoHeaderView.frame.maxY + 10) + + var y: CGFloat = 0 + for subview in optionsView.subviews { + subview.centerX(y: y) + y += subview.frame.height + y += 20 + } + } + + override var backdorColor: NSColor { + return .clear + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + guard let item = item as? RowItem else { + return + } + infoHeaderView.set(text: item.infoHeaderLayout, context: item.context) + infoView.set(text: item.infoLayout, context: item.context) + + headerView.update(item.headerLayout) + iconView.backgroundColor = theme.colors.accent + + stickerView.update(with: LocalAnimatedSticker.fragment_username.file, size: stickerView.frame.size, context: item.context, table: nil, parameters: LocalAnimatedSticker.fragment_username.parameters, animated: animated) + + infoBlock.setFrameSize(NSMakeSize(frame.width - 40, 10 + infoHeaderView.frame.height + 10 + infoView.frame.height + 10)) + + infoBlock.backgroundColor = theme.colors.listBackground + infoBlock.layer?.cornerRadius = 10 + + + while optionsView.subviews.count > item.options.count { + optionsView.subviews.last?.removeFromSuperview() + } + while optionsView.subviews.count < item.options.count { + optionsView.addSubview(OptionView(frame: .zero)) + } + + var optionsSize = NSMakeSize(0, 0) + for (i, option) in item.options.enumerated() { + let view = optionsView.subviews[i] as! OptionView + view.update(option: option) + view.setFrameSize(option.size) + optionsSize = NSMakeSize(max(option.width, optionsSize.width), option.size.height + optionsSize.height) + if i != item.options.count - 1 { + optionsSize.height += 20 + } + } + + optionsView.setFrameSize(optionsSize) + + + needsLayout = true + } +} + + +private let _id_header = InputDataIdentifier("_id_header") + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_header, equatable: .init(state), comparable: nil, item: { initialSize, stableId in + return RowItem(initialSize, stableId: stableId, context: arguments.context) + })) + + entries.append(.sectionId(sectionId, type: .customModern(10))) + sectionId += 1 + + return entries +} +func FragmentMonetizationPromoController(context: AccountContext, peerId: PeerId) -> InputDataModalController { + + let actionsDisposable = DisposableSet() + + let initialState = State() + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: "") + + controller.onDeinit = { + actionsDisposable.dispose() + } + + let modalInteractions = ModalInteractions(acceptTitle: "Understood", accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, singleButton: true, customTheme: { + .init(background: theme.colors.background, grayForeground: theme.colors.background, activeBackground: theme.colors.background, listBackground: theme.colors.background) + }) + + + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, size: NSMakeSize(340, 0)) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + controller.getBackgroundColor = { + theme.colors.background + } + + return modalController +} diff --git a/Telegram-Mac/FragmentUsernameController.swift b/Telegram-Mac/FragmentUsernameController.swift index 3dd97fdbd..38379dc47 100644 --- a/Telegram-Mac/FragmentUsernameController.swift +++ b/Telegram-Mac/FragmentUsernameController.swift @@ -44,16 +44,15 @@ private final class RowItem : GeneralRowItem { })) })) - let infoText = "The @lean username was acquired on\nFragment on 1 Mar 2024 for \(clown) 6000 ($15200).\n\n[Copy Link]()" + let infoText = "The **@lean** username was acquired on\nFragment on 1 Mar 2024 for \(clown)** 6000** (~$15200).\n\n[Copy Link]()" let infoAttr = parseMarkdownIntoAttributedString(infoText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.text), bold: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.text), link: MarkdownAttributeSet(font: .medium(.title), textColor: theme.colors.link), linkAttribute: { contents in return (NSAttributedString.Key.link.rawValue, inAppLink.callback("", { _ in })) })).mutableCopy() as! NSMutableAttributedString - let range = infoText.nsstring.range(of: clown) - - infoAttr.replaceCharacters(in: range, with: "") - infoAttr.insert(.embedded(name: "Icon_Reply_Group", color: theme.colors.text, resize: false), at: range.location) + + infoAttr.insertEmbedded(.embeddedAnimated(LocalAnimatedSticker.brilliant_static.file), for: clown) + infoAttr.detectBoldColorInString(with: .medium(.text)) self.headerLayout = .init(attr, alignment: .center) self.headerLayout.measure(width: initialSize.width - 40) @@ -105,7 +104,7 @@ private final class RowView: GeneralContainableRowView { let layout = TextViewLayout(.initialize(string: peer._asPeer().displayTitle, color: presentation.colors.text, font: .medium(.text))) layout.measure(width: maxWidth - 40) textView.update(layout) - self.backgroundColor = presentation.colors.background + self.backgroundColor = presentation.colors.listBackground self.setFrameSize(NSMakeSize(layout.layoutSize.width + 10 + avatar.frame.width + 10, 30)) @@ -180,7 +179,8 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { sectionId += 1 entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_header, equatable: .init(state), comparable: nil, item: { initialSize, stableId in - return RowItem(initialSize, stableId: stableId, peer: state.peer, context: arguments.context, price: state.price, username: state.username)})) + return RowItem(initialSize, stableId: stableId, peer: state.peer, context: arguments.context, price: state.price, username: state.username) + })) entries.append(.sectionId(sectionId, type: .customModern(10))) sectionId += 1 @@ -216,7 +216,9 @@ func FragmentUsernameController(context: AccountContext, peer: EnginePeer, usern let modalInteractions = ModalInteractions(acceptTitle: "Learn More", accept: { [weak controller] in _ = controller?.returnKeyAction() - }, singleButton: true) + }, singleButton: true, customTheme: { + .init(background: theme.colors.background, grayForeground: theme.colors.background, activeBackground: theme.colors.background, listBackground: theme.colors.background) + }) let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) @@ -228,6 +230,10 @@ func FragmentUsernameController(context: AccountContext, peer: EnginePeer, usern modalController?.modal?.close() } + controller.getBackgroundColor = { + theme.colors.background + } + return modalController } diff --git a/Telegram-Mac/GeneralRowItem.swift b/Telegram-Mac/GeneralRowItem.swift index a4f2ef70e..d3b43794e 100644 --- a/Telegram-Mac/GeneralRowItem.swift +++ b/Telegram-Mac/GeneralRowItem.swift @@ -218,22 +218,22 @@ enum GeneralViewType : Equatable { } static var firstItem: GeneralViewType { - return .modern(position: .first, insets: NSEdgeInsetsMake(12, 16, 12, 16)) + return .modern(position: .first, insets: NSEdgeInsetsMake(10, 14, 10, 14)) } static var innerItem: GeneralViewType { - return .modern(position: .inner, insets: NSEdgeInsetsMake(12, 16, 12, 16)) + return .modern(position: .inner, insets: NSEdgeInsetsMake(10, 14, 10, 14)) } static var lastItem: GeneralViewType { - return .modern(position: .last, insets: NSEdgeInsetsMake(12, 16, 12, 16)) + return .modern(position: .last, insets: NSEdgeInsetsMake(10, 14, 10, 14)) } static var singleItem: GeneralViewType { - return .modern(position: .single, insets: NSEdgeInsetsMake(12, 16, 12, 16)) + return .modern(position: .single, insets: NSEdgeInsetsMake(10, 14, 10, 14)) } static var textTopItem: GeneralViewType { - return .modern(position: .single, insets: NSEdgeInsetsMake(0, 16, 5, 0)) + return .modern(position: .single, insets: NSEdgeInsetsMake(0, 14, 3, 0)) } static var textBottomItem: GeneralViewType { - return .modern(position: .single, insets: NSEdgeInsetsMake(5, 16, 0, 0)) + return .modern(position: .single, insets: NSEdgeInsetsMake(3, 14, 0, 0)) } static var separator: GeneralViewType { return .modern(position: .single, insets: NSEdgeInsetsMake(0, 0, 0, 0)) diff --git a/Telegram-Mac/InAppLinks.swift b/Telegram-Mac/InAppLinks.swift index be7163a66..e0d49e975 100644 --- a/Telegram-Mac/InAppLinks.swift +++ b/Telegram-Mac/InAppLinks.swift @@ -742,7 +742,16 @@ func execute(inapp:inAppLink, afterComplete: @escaping(Bool)->Void = { _ in }) { $0 is ChatController } as? ChatController - chat?.chatInteraction.invokeInitialAction(action: action) + if chat == nil { + switch action { + case let .openWebview(botPeer, botApp, url): + showModal(with: WebpageModalController(context: context, url: url, title: botApp.title, requestData: nil, chatInteraction: nil, thumbFile: MenuAnimation.menu_folder_bot.file, botPeer: botPeer.peer), for: context.window) + default: + break + } + } else { + chat?.chatInteraction.invokeInitialAction(action: action) + } } diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 3a54ba676..4ee9dbd18 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260663 + 260777 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/Telegram-Mac/LottieLocalAnimations.swift b/Telegram-Mac/LottieLocalAnimations.swift index f94a04a03..c37518031 100644 --- a/Telegram-Mac/LottieLocalAnimations.swift +++ b/Telegram-Mac/LottieLocalAnimations.swift @@ -282,6 +282,7 @@ enum LocalAnimatedSticker : String { case menu_quote case menu_boost case menu_boost_plus + case menu_search case emoji_category_activities case emoji_category_angry diff --git a/Telegram-Mac/PeerRequestJoinRowItem.swift b/Telegram-Mac/PeerRequestJoinRowItem.swift index 14ace0d80..a0d026dc1 100644 --- a/Telegram-Mac/PeerRequestJoinRowItem.swift +++ b/Telegram-Mac/PeerRequestJoinRowItem.swift @@ -41,7 +41,7 @@ final class PeerRequestJoinRowItem: GeneralRowItem { } else { self.aboutLayout = nil } - self.dateLayout = TextViewLayout(.initialize(string: DateUtils.string(forMessageListDate: Int32(data.timeInterval)), color: theme.colors.grayText, font: .normal(.text))) + self.dateLayout = TextViewLayout(.initialize(string: DateUtils.string(forMessageListDate: Int32(data.timeInterval)), color: theme.colors.grayText, font: .normal(.small))) if data.added || data.dismissed { let text: String @@ -60,10 +60,20 @@ final class PeerRequestJoinRowItem: GeneralRowItem { override var height: CGFloat { let inset = viewType.innerInset - var height: CGFloat = max(inset.top + nameLayout.layoutSize.height + 30 + inset.bottom * 2, 40 + inset.top + inset.bottom) + let topDivided = inset.top / 2 + var height: CGFloat = 0 + height += inset.top + height += max(16, nameLayout.layoutSize.height) + height += topDivided + if let about = self.aboutLayout { - height += inset.top / 2 + about.layoutSize.height + height += topDivided + height += about.layoutSize.height } + + height += 30 + height += inset.bottom + return height } @@ -88,7 +98,7 @@ private final class PeerRequestJoinRowView: GeneralContainableRowView { private let avatar = AvatarControl(font: .avatar(14)) private let timeView = TextView() private let nameView = TextView() - private let aboutView = TextView() + private var aboutView: TextView? private let addButton = TextButton() private let dismissButton = TextButton() @@ -99,7 +109,6 @@ private final class PeerRequestJoinRowView: GeneralContainableRowView { avatar.setFrameSize(NSMakeSize(40, 40)) addSubview(avatar) addSubview(timeView) - addSubview(aboutView) addSubview(nameView) addSubview(addButton) addSubview(dismissButton) @@ -140,6 +149,10 @@ private final class PeerRequestJoinRowView: GeneralContainableRowView { fatalError("init(coder:) has not been implemented") } + override var additionBorderInset: CGFloat { + return avatar.frame.width + 16 + } + override func layout() { super.layout() @@ -147,12 +160,20 @@ private final class PeerRequestJoinRowView: GeneralContainableRowView { return } let inset = item.viewType.innerInset + let topDivided = inset.top / 2 avatar.setFrameOrigin(NSMakePoint(inset.left, inset.top)) - nameView.setFrameOrigin(NSMakePoint(avatar.frame.maxX + inset.left, inset.top)) + nameView.setFrameOrigin(NSMakePoint(avatar.frame.maxX + inset.left, inset.top - 2)) timeView.setFrameOrigin(NSMakePoint(containerView.frame.width - timeView.frame.width - inset.right, inset.top)) - aboutView.setFrameOrigin(NSMakePoint(nameView.frame.minX, nameView.frame.maxY + inset.top / 2)) - addButton.setFrameOrigin(NSMakePoint(nameView.frame.minX, aboutView.frame.maxY + inset.top)) - dismissButton.setFrameOrigin(NSMakePoint(addButton.frame.maxX + 20, addButton.frame.minY)) + + var buttonY: CGFloat = nameView.frame.maxY + topDivided + 2 + + if let aboutView { + aboutView.setFrameOrigin(NSMakePoint(nameView.frame.minX, nameView.frame.maxY + topDivided)) + buttonY = aboutView.frame.maxY + topDivided + 2 + } + + addButton.setFrameOrigin(NSMakePoint(nameView.frame.minX, buttonY)) + dismissButton.setFrameOrigin(NSMakePoint(addButton.frame.maxX + 10, addButton.frame.minY)) statusView?.setFrameOrigin(NSMakePoint(nameView.frame.minX, dismissButton.frame.minY + 5)) progressIndicator?.setFrameOrigin(NSMakePoint(nameView.frame.minX, dismissButton.frame.minY)) @@ -166,9 +187,24 @@ private final class PeerRequestJoinRowView: GeneralContainableRowView { } self.timeView.update(item.dateLayout) - self.aboutView.update(item.aboutLayout) self.nameView.update(item.nameLayout) + + if let aboutLayout = item.aboutLayout { + let current: TextView + if let view = self.aboutView { + current = view + } else { + current = TextView() + addSubview(current) + self.aboutView = current + } + current.update(aboutLayout) + } else if let view = self.aboutView { + performSubviewRemoval(view, animated: animated) + self.aboutView = nil + } + avatar.setPeer(account: item.context.account, peer: item.data.peer.peer) self.addButton.set(text: item.isChannel ? strings().requestJoinListApproveChannel : strings().requestJoinListApproveGroup, for: .Normal) diff --git a/Telegram-Mac/RequestJoinMemberListController.swift b/Telegram-Mac/RequestJoinMemberListController.swift index 4bee505a5..3cf929e77 100644 --- a/Telegram-Mac/RequestJoinMemberListController.swift +++ b/Telegram-Mac/RequestJoinMemberListController.swift @@ -166,9 +166,10 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { if state.searchResult == nil { + entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 - entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().requestJoinListListHeaderCountable(importers.count)), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(strings().requestJoinListListHeaderCountable(Int(importerState.count))), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) index += 1 } for (i, importer) in importers.enumerated() { @@ -389,24 +390,42 @@ func RequestJoinMemberListController(context: AccountContext, peerId: PeerId, ma var updateBarIsHidden:((Bool)->Void)? = nil - let controller = InputDataController(dataSignal: signal, title: strings().requestJoinListTitle, removeAfterDisappear: false, customRightButton: { controller in - let bar = ImageBarView(controller: controller, theme.icons.chatSearch) - bar.button.set(handler: { _ in - updateSearchValue { current in - switch current { - case .none: - return .visible(searchData) - case .visible: - return .none({ searchState in - updateState { current in - var current = current - current.searchState = searchState - return current - } - }) + let controller = InputDataController(dataSignal: signal, title: strings().requestJoinListTitle, removeAfterDisappear: false, customRightButton: { [weak manager] controller in + let bar = ImageBarView(controller: controller, theme.icons.chatActions) + bar.button.contextMenu = { + + let state = stateValue.with { $0 } + + let menu = ContextMenu() + menu.addItem(ContextMenuItem(strings().contextMenuSearch, handler: { + updateSearchValue { current in + switch current { + case .none: + return .visible(searchData) + case .visible: + return .none({ searchState in + updateState { current in + var current = current + current.searchState = nil + return current + } + }) + } } + }, itemImage: MenuAnimation.menu_search.value)) + + if state.state?.importers.count != 0 { + menu.addItem(ContextMenuItem(strings().requestJoinListApproveAll, handler: { + manager?.updateAll(action: .approve) + }, itemImage: MenuAnimation.menu_add.value)) + + menu.addItem(ContextMenuItem(strings().requestJoinListDismissAll, handler: { + manager?.updateAll(action: .deny) + }, itemImage: MenuAnimation.menu_clear_history.value)) } - }, for: .Click) + + return menu + } bar.button.autohighlight = false bar.button.scaleOnClick = true updateBarIsHidden = { [weak bar] isHidden in @@ -431,7 +450,7 @@ func RequestJoinMemberListController(context: AccountContext, peerId: PeerId, ma return .none({ searchState in updateState { current in var current = current - current.searchState = searchState + current.searchState = nil return current } }) diff --git a/Telegram-Mac/WebpageModalController.swift b/Telegram-Mac/WebpageModalController.swift index 69a666d0b..0a9d4c82f 100644 --- a/Telegram-Mac/WebpageModalController.swift +++ b/Telegram-Mac/WebpageModalController.swift @@ -1526,6 +1526,10 @@ class WebpageModalController: ModalViewController, WKNavigationDelegate, WKUIDel override var hasNextResponder: Bool { return false } + + override var hasBorder: Bool { + return false + } } diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index e7433498e73b48e5922e874b104451d6eae856ea..3dd0fb05aafbcfd7e00cb72f880a44accda88074 100644 GIT binary patch delta 192 zcmZ3o(qz?YlZFF7M2#)7Pc1lEgUvc(`N*8NKFq2GxCE#HY_=V-}h|Eto?FVzEFphtl*PZ0vlK Z8(O*A=R|P;F((j%*xcLaMDcL!0svh&HK_mq delta 62 zcmZ3r+GNQ}lZFF7M2#)7Pc1lEgUvc(*^#qvrL~C!=W_YW(^l#ds#FG5OV@C O7Z7uAFN@|e*#!WzxfP!P diff --git a/Telegram-Mac/tgs/menu/menu_search.tgs b/Telegram-Mac/tgs/menu/menu_search.tgs new file mode 100644 index 0000000000000000000000000000000000000000..49372f1d7cc930a816219befc15cc6697cb681ca GIT binary patch literal 801 zcmV++1K#`}iwFoJyy|5D18rq)bzgI3VRB<=E^2dcZUF69-EY${5dT*qKf6)vd@So5 z2(&jq8sepjCX=MJG)*EWEvu^j@0@*3mJb&R7-&dnP3-v2_xZcCcP2-&B%#Z=%LpZF zNtTgE$+jj-7%CWi7{8Gvi(%ZDvE0L^eO?|OM3z0Y)x)|fvRpbGL}Xc}_ll;;+lDMx z2_=O%j|YlS;jW-n7H5k3eM?N~D$I0i_;M zR<|C7pO+v92ZP`ZY+}p?EIkwJnHs~ykm*deYT;4wy8$&1>;gwtjKW_6o2nXn zpcde1wPF<(=6TdSKzLRSvC+UmI{zaYScEEWLkho60T6g#@y>)dGT~K8s4#b9$Tb+9 z&wvY0$s6H@BW?nNn{8T^fSOv!yli!%egGh4$uB@C@S#RRYKGs0l3@ZhU zlI2-$IxfmWoLrms3K`$EG;!^&8G-^yy2+iky-qTXy%upYUr z=C>*#_00U_DME8_bkMMZVgLvK8b+h*5xG%9ceYI`R*kSx!Y+>=r6`+qCHF*?>ae`Z zS_d*UxHlm*c(X)qn(gFTji*?%_EPu9rT+dXif1YHMG#&u_Qj1GQvLdO3Vn7wd2X!g z;ACclhOTYjCFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260663 + 260777 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/packages/Localization/Sources/Localization/Localizable.swift b/packages/Localization/Sources/Localization/Localizable.swift index ebe406532..5ec40c135 100644 --- a/packages/Localization/Sources/Localization/Localizable.swift +++ b/packages/Localization/Sources/Localization/Localizable.swift @@ -6743,6 +6743,8 @@ public final class L10n { public static var contextAlertCopied: String { return L10n.tr("Localizable", "Context.Alert.Copied") } /// This link will only work for members of this chat public static var contextAlertCopyPrivate: String { return L10n.tr("Localizable", "Context.Alert.CopyPrivate") } + /// Search + public static var contextMenuSearch: String { return L10n.tr("Localizable", "Context.Menu.Search") } /// %d public static func conversationFreeTranscriptionCooldownTooltipCountable(_ p1: Int) -> String { return L10n.tr("Localizable", "Conversation.FreeTranscriptionCooldownTooltip_countable", p1) @@ -12699,12 +12701,16 @@ public final class L10n { } /// Members Requests public static var requestJoinListTitle: String { return L10n.tr("Localizable", "RequestJoin.List.Title") } + /// Approve All + public static var requestJoinListApproveAll: String { return L10n.tr("Localizable", "RequestJoin.List.Approve.All") } /// Add to Channel public static var requestJoinListApproveChannel: String { return L10n.tr("Localizable", "RequestJoin.List.Approve.Channel") } /// Dismiss public static var requestJoinListApproveDismiss: String { return L10n.tr("Localizable", "RequestJoin.List.Approve.Dismiss") } /// Add to Group public static var requestJoinListApproveGroup: String { return L10n.tr("Localizable", "RequestJoin.List.Approve.Group") } + /// Dismiss All + public static var requestJoinListDismissAll: String { return L10n.tr("Localizable", "RequestJoin.List.Dismiss.All") } /// You have no pending requests to join the channel public static var requestJoinListEmpty2Channel: String { return L10n.tr("Localizable", "RequestJoin.List.Empty2.Channel") } /// You have no pending requests to join the group diff --git a/packages/TGUIKit/Sources/TableView.swift b/packages/TGUIKit/Sources/TableView.swift index c382b6914..cf1735ac1 100644 --- a/packages/TGUIKit/Sources/TableView.swift +++ b/packages/TGUIKit/Sources/TableView.swift @@ -2648,14 +2648,6 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele resetScrollNotifies() - if transition.isEmpty { - switch transition.state { - case .none, .saveVisible: - return - default: - break - } - } let previousList = self.list @@ -2948,6 +2940,7 @@ open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,Sele updateState(state) }) updateState(nil) + currentSearchState = nil firstSearchAppear = true case let .visible(data): searchView.change(pos: NSZeroPoint, animated: true) From c883649f4e61650eed5960956f0596fec12ef80d Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Mon, 11 Mar 2024 10:24:19 +0300 Subject: [PATCH 49/50] - bugfixes --- Telegram-Mac/BusinessHoursController.swift | 12 ++++++++++++ Telegram-Mac/BusinessTimezonesController.swift | 2 +- Telegram-Mac/Info.plist | 2 +- TelegramShare/Info.plist | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Telegram-Mac/BusinessHoursController.swift b/Telegram-Mac/BusinessHoursController.swift index 32b75d2aa..214cb00f8 100644 --- a/Telegram-Mac/BusinessHoursController.swift +++ b/Telegram-Mac/BusinessHoursController.swift @@ -30,6 +30,18 @@ private func wrappedMinuteRange(range: Range, dayIndexOffset: Int = 0) -> I extension TimeZoneList.Item { + + var gmtText: String { + let hoursFromGMT = TimeInterval(self.utcOffset) / 60.0 / 60.0 + let gmtText = "\(hoursFromGMT)" + .replacingOccurrences(of: ".5", with: ":30") + .replacingOccurrences(of: ".0", with: "") + if hoursFromGMT >= 0 { + return "\(strings().businessHoursUTC)+\(gmtText)" + } else { + return "\(strings().businessHoursUTC)\(gmtText)" + } + } var text: String { let hoursFromGMT = TimeInterval(self.utcOffset) / 60.0 / 60.0 let gmtText = "\(hoursFromGMT)" diff --git a/Telegram-Mac/BusinessTimezonesController.swift b/Telegram-Mac/BusinessTimezonesController.swift index 51204743b..537e353f6 100644 --- a/Telegram-Mac/BusinessTimezonesController.swift +++ b/Telegram-Mac/BusinessTimezonesController.swift @@ -61,7 +61,7 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { })) } else { for (i, timezone) in list.enumerated() { - entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_timezone(timezone), data: .init(name: timezone.text, color: theme.colors.text, type: timezone == state.selected ? .image(theme.icons.poll_selected) : .none, viewType: .legacy, action: { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_timezone(timezone), data: .init(name: timezone.title, color: theme.colors.text, type: timezone == state.selected ? .image(theme.icons.poll_selected) : .none, viewType: .legacy, description: timezone.gmtText, descTextColor: theme.colors.grayText, action: { arguments.toggle(timezone) }))) } diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index 4ee9dbd18..fb47a04fd 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -35,7 +35,7 @@ CFBundleVersion - 260777 + 260779 ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index e24cb4737..aaffb8c45 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 260777 + 260779 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion From 81b264f4216616b394cc8302cd86c4bb8839091f Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Mon, 11 Mar 2024 10:38:41 +0300 Subject: [PATCH 50/50] version bump --- Telegram.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Telegram.xcodeproj/project.pbxproj b/Telegram.xcodeproj/project.pbxproj index 52f996123..46f527a2a 100644 --- a/Telegram.xcodeproj/project.pbxproj +++ b/Telegram.xcodeproj/project.pbxproj @@ -7933,7 +7933,7 @@ "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.9; + MARKETING_VERSION = 10.9.1; ONLY_ACTIVE_ARCH = NO; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = ( @@ -7985,7 +7985,7 @@ "$(PROJECT_DIR)/core-xprojects/openssl/build/openssl/lib", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.9; + MARKETING_VERSION = 10.9.1; ONLY_ACTIVE_ARCH = YES; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/macosx/libswiftAppKit.dylib"; @@ -8032,7 +8032,7 @@ "$(PROJECT_DIR)/core-xprojects/openssl/build/openssl/lib", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.9; + MARKETING_VERSION = 10.9.1; ONLY_ACTIVE_ARCH = NO; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/macosx/libswiftAppKit.dylib"; @@ -8149,7 +8149,7 @@ "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", ); MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 10.9; + MARKETING_VERSION = 10.9.1; ONLY_ACTIVE_ARCH = YES; OTHER_CODE_SIGN_FLAGS = ""; OTHER_LDFLAGS = (