@@ -399,6 +399,14 @@ public class ChatPollItem: ChatRowItem {
399399 }
400400
401401 public let isTranslateLoading : Bool
402+
403+ fileprivate var canCopyText : Bool {
404+ guard let message = message , ! message . flags . contains ( . CopyProtected ) else {
405+ return false
406+ }
407+ let peer = chatInteraction . peer . flatMap { $0 . id == message . id . peerId ? $0 : nil } ?? message . peers [ message . id . peerId ]
408+ return peer . map { ! $0 . isCopyProtected ( cachedData : chatInteraction . presentation . cachedData ) } ?? false
409+ }
402410
403411 public override init ( _ initialSize : NSSize , _ chatInteraction : ChatInteraction , _ context : any AccountContextProtocol , _ object : ChatHistoryEntry , theme : TelegramPresentationTheme ) {
404412
@@ -527,7 +535,11 @@ public class ChatPollItem: ChatRowItem {
527535 optionText . append ( string : option . text , color : self . presentation . chat . textColor ( isIncoming , renderType == . bubble ) , font : nameFont )
528536 InlineStickerItem . apply ( to : optionText , associatedMedia : message ? . associatedMedia ?? [ : ] , entities : option . entities , isPremium : context . isPremium )
529537
530- let nameLayout = TextViewLayout ( optionText , alwaysStaticItems : true )
538+ let nameLayout = TextViewLayout ( optionText , selectText : self . presentation . chat . selectText ( isIncoming , renderType == . bubble ) , alwaysStaticItems : true , mayItems : canCopyText )
539+ nameLayout . interactions . copyAttributedString = LinkHooks . shared . globalLinkExecutor ( ) . copyAttributedString
540+ nameLayout . interactions . copy = { [ weak self ] in
541+ return self ? . canCopyText != true
542+ }
531543
532544 let wrapper = PollOption ( option : option , nameText : nameLayout , percent : percent , realPercent : realPercent , voteCount : votedCount , isSelected : isSelected , isIncoming : isIncoming , isBubbled : renderType == . bubble , voted : voted , isLoading : object . additionalData . pollStateData . identifiers . contains ( option . opaqueIdentifier ) && object . additionalData . pollStateData . isLoading , presentation : self . presentation , isCorrect : isCorrect , isQuiz : poll . isQuiz , isMultipleSelected : object . additionalData . pollStateData . identifiers . contains ( option . opaqueIdentifier ) , isMultipleChoice : poll . isMultiple , optionMedia : option . media , anyOptionHasMedia : anyOptionHasMedia , hideResults : hideResults && ( voted || object . additionalData . pollStateData . isLoading ) , vote : { [ weak self ] control in
533545 self ? . voteOption ( option , for : control )
@@ -563,8 +575,42 @@ public class ChatPollItem: ChatRowItem {
563575 if let message = message , ! message . text . isEmpty {
564576 let descAttr = ChatMessageItem . applyMessageEntities ( with : message . attributes , for : message . text , message : message , context : context , fontSize : theme . fontSize , openInfo : { [ weak chatInteraction ] peerId , toChat , postId , action in
565577 chatInteraction ? . openInfo ( peerId , toChat , postId , action )
566- } , textColor : self . presentation . chat . textColor ( isIncoming , renderType == . bubble ) , linkColor : self . presentation . chat . linkColor ( isIncoming , renderType == . bubble ) , isDark : theme . colors . isDark , bubbled : renderType == . bubble ) . mutableCopy ( ) as! NSMutableAttributedString
567- self . descriptionText = TextViewLayout ( descAttr , alwaysStaticItems : true )
578+ } , botCommand : { [ weak chatInteraction ] command in
579+ chatInteraction ? . sendPlainText ( command )
580+ } , hashtag : { [ weak chatInteraction ] tag in
581+ chatInteraction ? . hashtag ( tag )
582+ } , applyProxy : { [ weak chatInteraction ] proxy in
583+ chatInteraction ? . applyProxy ( proxy )
584+ } , textColor : self . presentation . chat . textColor ( isIncoming , renderType == . bubble ) , linkColor : self . presentation . chat . linkColor ( isIncoming , renderType == . bubble ) , openBank : { [ weak chatInteraction ] bank in
585+ chatInteraction ? . openBank ( bank )
586+ } , isDark : theme . colors . isDark , bubbled : renderType == . bubble , openPhoneNumber : { [ weak chatInteraction ] phone in
587+ chatInteraction ? . openPhoneNumberContextMenu ( phone )
588+ } , openTonAddress : { [ weak chatInteraction ] address in
589+ chatInteraction ? . openTonAddressContextMenu ( address )
590+ } , ignoreLinks : ! object . additionalData . canHighlightLinks && isIncoming ) . mutableCopy ( ) as! NSMutableAttributedString
591+ let layout = TextViewLayout ( descAttr , selectText : self . presentation . chat . selectText ( isIncoming , renderType == . bubble ) , strokeLinks : renderType == . bubble , alwaysStaticItems : true , disableTooltips : false , mayItems : canCopyText )
592+ layout . interactions = LinkHooks . shared . globalLinkExecutor ( )
593+ layout . interactions . copy = { [ weak self ] in
594+ return self ? . canCopyText != true
595+ }
596+ layout . interactions . topWindow = { [ weak self ] in
597+ return self ? . menuAdditionView ?? . single ( nil )
598+ }
599+ layout . interactions . menuItems = { [ weak self ] type in
600+ if let strongSelf = self , let message = strongSelf . message {
601+ return chatMenuItems ( for : message , entry : strongSelf . entry , textLayout : ( strongSelf . descriptionText , type ) , chatInteraction : strongSelf . chatInteraction )
602+ }
603+ return . complete ( )
604+ }
605+ let processURL = layout . interactions . processURL
606+ let makeLinkType = layout . interactions . makeLinkType
607+ layout . interactions . processURL = { [ weak self ] link in
608+ if case . code = makeLinkType ( ( link , " " ) ) , self ? . canCopyText != true {
609+ return
610+ }
611+ processURL ( link )
612+ }
613+ self . descriptionText = layout
568614 }
569615
570616 let titleAttr = NSMutableAttributedString ( )
@@ -573,7 +619,11 @@ public class ChatPollItem: ChatRowItem {
573619 InlineStickerItem . apply ( to : titleAttr , associatedMedia : message ? . associatedMedia ?? [ : ] , entities : poll . textEntities , isPremium : context . isPremium )
574620
575621
576- self . titleText = TextViewLayout ( titleAttr , alwaysStaticItems : true )
622+ self . titleText = TextViewLayout ( titleAttr , selectText : self . presentation . chat . selectText ( isIncoming , renderType == . bubble ) , alwaysStaticItems : true , mayItems : canCopyText )
623+ self . titleText . interactions . copyAttributedString = LinkHooks . shared . globalLinkExecutor ( ) . copyAttributedString
624+ self . titleText . interactions . copy = { [ weak self ] in
625+ return self ? . canCopyText != true
626+ }
577627
578628
579629 let typeText : String = self . isBotQuiz ? strings ( ) . chatQuizTextType : poll . title
@@ -715,7 +765,7 @@ public class ChatPollItem: ChatRowItem {
715765 } else {
716766 source = . general
717767 }
718- return chatMenuItems ( for : message , entry : entry , textLayout : nil , chatInteraction : chatInteraction , source : source )
768+ return chatMenuItems ( for : message , entry : entry , textLayout : foundIndex == nil ? ( ( view as? ChatPollItemView ) ? . menuTextLayout ( at : location ) , nil ) : nil , chatInteraction : chatInteraction , source : source )
719769 }
720770 return super . menuItems ( in : location )
721771 }
@@ -968,7 +1018,8 @@ public final class ChatPollItemView : ChatRowView {
9681018
9691019 contentNode . change ( size : NSMakeSize ( contentFrameModifier ( item ) . width , item . contentSize . height ) , animated : animated )
9701020 contentNode . update ( with : item , animated : animated )
971-
1021+ updateTextSelection ( updatePermissions : true )
1022+
9721023 contentNode . updateLayout ( item : item , size : contentNode . frame . size , transition : animated ? . animated ( duration : 0.2 , curve : . easeOut ) : . immediate )
9731024 }
9741025
@@ -1003,18 +1054,71 @@ public final class ChatPollItemView : ChatRowView {
10031054 fatalError ( " init(coder:) has not been implemented " )
10041055 }
10051056
1057+ public override func notify ( with value : Any , oldValue : Any , animated : Bool ) {
1058+ super . notify ( with : value , oldValue : oldValue , animated : animated )
1059+ updateTextSelection ( )
1060+ }
1061+
1062+ private func updateTextSelection ( updatePermissions : Bool = false ) {
1063+ guard let item = item as? ChatPollItem else { return }
1064+ let canCopy = item . canCopyText
1065+ if ! canCopy , selectManager . find ( item . stableId ) != nil {
1066+ selectManager . removeAll ( )
1067+ }
1068+ var views : [ TextView ] = [ contentNode . titleView . textView ] + contentNode . options . map { $0 . nameView . textView }
1069+ if let descriptionView = contentNode . descriptionView {
1070+ views . append ( descriptionView )
1071+ }
1072+ for view in views {
1073+ if updatePermissions || view . textLayout ? . mayItems != canCopy {
1074+ view . isSelectable = canCopy
1075+ view . textLayout ? . mayItems = canCopy
1076+ }
1077+ if ! canCopy , let layout = view . textLayout , layout . selectedRange . range . location != NSNotFound || ! layout . additionalSelections . isEmpty {
1078+ layout . clearSelect ( )
1079+ layout . additionalSelections = [ ]
1080+ view . needsDisplay = true
1081+ }
1082+ }
1083+ }
1084+
1085+ func menuTextLayout ( at location : NSPoint ) -> TextViewLayout ? {
1086+ let views : [ TextView ] = [ contentNode . titleView . textView ] + [ contentNode . descriptionView ] . compactMap { $0 }
1087+ for view in views {
1088+ let point = view . convert ( location , from : contentView )
1089+ if view . bounds . contains ( point ) {
1090+ return view . textLayout
1091+ }
1092+ }
1093+ return nil
1094+ }
1095+
10061096 public override func canStartTextSelecting ( _ event : NSEvent ) -> Bool {
1007-
1008- let point = contentView . convert ( event . locationInWindow , from : nil )
1009- return NSPointInRect ( point , NSMakeRect ( 0 , contentNode . titleView . frame . minY , contentNode . frame . width , contentNode . titleView . frame . height ) )
1097+ for view in selectableTextViews {
1098+ let point = view . convert ( event . locationInWindow , from : nil )
1099+ if NSPointInRect ( point , view . bounds ) {
1100+ return true
1101+ }
1102+ }
1103+ return false
10101104 }
1011-
1105+
10121106 override var textSelectionSources : ChatTextSelectionSources {
10131107 return . views ( selectableTextViews )
10141108 }
10151109
10161110 public override var selectableTextViews : [ TextView ] {
1017- return [ contentNode . titleView . textView ]
1111+ guard let item = item as? ChatPollItem , item . canCopyText else {
1112+ return [ ]
1113+ }
1114+ var views : [ TextView ] = [ contentNode . titleView . textView ]
1115+ if let descriptionView = contentNode . descriptionView {
1116+ views . append ( descriptionView )
1117+ }
1118+ for option in contentNode . options {
1119+ views . append ( option . nameView . textView )
1120+ }
1121+ return views
10181122 }
10191123
10201124 public override func canMultiselectTextIn ( _ location : NSPoint ) -> Bool {
@@ -1061,7 +1165,7 @@ public final class ChatPollItemView : ChatRowView {
10611165
10621166private final class PollOptionView : Control {
10631167 private var percentView : ImageView ?
1064- private let nameView : InteractiveTextView = InteractiveTextView ( frame : . zero )
1168+ fileprivate let nameView : InteractiveTextView = InteractiveTextView ( frame : . zero )
10651169 private var selectingView : ImageView ?
10661170 private let progressBackgroundView : View = View ( frame : NSZeroRect )
10671171 private let progressView : LinearProgressControl = LinearProgressControl ( progressHeight : 5 )
@@ -1075,11 +1179,52 @@ private final class PollOptionView : Control {
10751179
10761180 private var selectedImageView : ImageView ?
10771181
1182+ private var startMouseDownPoint : NSPoint ?
1183+ private var didDrag : Bool = false
1184+
1185+ override func mouseDown ( with event : NSEvent ) {
1186+ startMouseDownPoint = event . locationInWindow
1187+ didDrag = false
1188+ if ! event . modifierFlags . contains ( . control ) {
1189+ selectManager . removeAll ( )
1190+ }
1191+ super . mouseDown ( with : event )
1192+ }
1193+
1194+ override func mouseDragged ( with event : NSEvent ) {
1195+ if let start = startMouseDownPoint {
1196+ if chatTextSelectionDidDrag ( from : start , to : event . locationInWindow ) {
1197+ didDrag = true
1198+ }
1199+ }
1200+ super . mouseDragged ( with : event )
1201+ }
1202+
1203+ override func mouseUp ( with event : NSEvent ) {
1204+ if let start = startMouseDownPoint {
1205+ if chatTextSelectionDidDrag ( from : start , to : event . locationInWindow ) {
1206+ didDrag = true
1207+ }
1208+ }
1209+ defer {
1210+ startMouseDownPoint = nil
1211+ didDrag = false
1212+ }
1213+ super . mouseUp ( with : event )
1214+ }
1215+
1216+ override func accessibilityPerformPress ( ) -> Bool {
1217+ startMouseDownPoint = nil
1218+ didDrag = false
1219+ return super . accessibilityPerformPress ( )
1220+ }
1221+
10781222 private var option : PollOption ?
10791223 private weak var pollItem : ChatPollItem ?
10801224 required init ( frame frameRect : NSRect ) {
10811225 super . init ( frame : frameRect )
10821226 nameView . userInteractionEnabled = false
1227+ nameView . textView . isSelectable = true
10831228 progressView . hasMinumimVisibility = true
10841229 progressBackgroundView . layer ? . cornerRadius = 2.5
10851230 addSubview ( nameView )
@@ -1095,9 +1240,13 @@ private final class PollOptionView : Control {
10951240
10961241 progressView . isEventLess = true
10971242 progressBackgroundView . isEventLess = true
1098-
1243+
10991244 set ( handler : { [ weak self ] control in
1100- self ? . option ? . vote ( control )
1245+ guard let self = self else { return }
1246+ if self . startMouseDownPoint != nil && ( self . didDrag || ! selectManager . isEmpty ) {
1247+ return
1248+ }
1249+ self . option ? . vote ( control )
11011250 } , for : . Click )
11021251 }
11031252
@@ -1659,7 +1808,7 @@ private final class PollOptionView : Control {
16591808
16601809private final class PollView : Control {
16611810 fileprivate var mediaContentView : ChatMediaContentView ?
1662- private var descriptionView : TextView ?
1811+ fileprivate var descriptionView : TextView ?
16631812 fileprivate let titleView : InteractiveTextView = InteractiveTextView ( frame : . zero )
16641813 private let typeView : TextView = TextView ( )
16651814 private var actionButton : TextButton ?
@@ -1736,6 +1885,7 @@ private final class PollView : Control {
17361885 self . descriptionView = descView
17371886 addSubview ( descView )
17381887 }
1888+ descView . isSelectable = item . canCopyText
17391889 descView . update ( descriptionText )
17401890 } else {
17411891 self . descriptionView ? . removeFromSuperview ( )
0 commit comments