Various improvements

This commit is contained in:
Ilya Laktyushin
2025-09-29 06:42:09 +04:00
parent 88068f3ccc
commit d126717ec1
15 changed files with 306 additions and 137 deletions

View File

@@ -15148,3 +15148,5 @@ Error: %8$@";
"ProfileColorSetup.NoProfileGiftsPlaceholder" = "You don't have any gifts you can use as styles for your profile.";
"ProfileColorSetup.NoNameGiftsPlaceholder" = "You don't have any gifts you can use as styles for your name.";
"ProfileColorSetup.BrowseGiftsForPurchase" = "Browse gifts available for purchase >";
"VoiceChat.RemovedConferencePeerText" = "You removed %@ from this call. They will no longer be able to join using the invite link.";

View File

@@ -186,6 +186,20 @@ public final class MultilineTextWithEntitiesComponent: Component {
fatalError("init(coder:) has not been implemented")
}
public func attributes(at point: CGPoint) -> (Int, [NSAttributedString.Key: Any])? {
if let result = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.textNode.frame.minX, y: point.y - self.textNode.frame.minY)) {
return result
}
return nil
}
public var isSpoilerConcealed: Bool {
if let dustNode = self.textNode.dustNode, !dustNode.isRevealed {
return true
}
return false
}
public func updateVisibility(_ isVisible: Bool) {
self.textNode.visibility = isVisible
}

View File

@@ -14,6 +14,7 @@ import TextFormat
import TelegramPresentationData
import ReactionSelectionNode
import BundleIconComponent
import LottieComponent
import Markdown
private let glassColor = UIColor(rgb: 0x25272e, alpha: 0.72)
@@ -22,6 +23,7 @@ final class MessageItemComponent: Component {
public enum Icon: Equatable {
case peer(EnginePeer)
case icon(String)
case animation(String)
}
private let context: AccountContext
@@ -30,7 +32,7 @@ final class MessageItemComponent: Component {
private let text: String
private let entities: [MessageTextEntity]
private let availableReactions: [ReactionItem]?
private let avatarTapped: () -> Void
private let openPeer: ((EnginePeer) -> Void)?
init(
context: AccountContext,
@@ -39,7 +41,7 @@ final class MessageItemComponent: Component {
text: String,
entities: [MessageTextEntity],
availableReactions: [ReactionItem]?,
avatarTapped: @escaping () -> Void = {}
openPeer: ((EnginePeer) -> Void)?
) {
self.context = context
self.icon = icon
@@ -47,7 +49,7 @@ final class MessageItemComponent: Component {
self.text = text
self.entities = entities
self.availableReactions = availableReactions
self.avatarTapped = avatarTapped
self.openPeer = openPeer
}
static func == (lhs: MessageItemComponent, rhs: MessageItemComponent) -> Bool {
@@ -77,6 +79,9 @@ final class MessageItemComponent: Component {
private let text: ComponentView<Empty>
weak var standaloneReactionAnimation: StandaloneReactionAnimation?
private var cachedEntities: [MessageTextEntity]?
private var entityFiles: [MediaId: TelegramMediaFile] = [:]
private var component: MessageItemComponent?
override init(frame: CGRect) {
@@ -95,12 +100,21 @@ final class MessageItemComponent: Component {
self.addSubview(self.container)
self.container.addSubview(self.background)
self.container.addSubview(self.avatarNode.view)
self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.avatarTapped)))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func avatarTapped() {
guard let component = self.component, case let .peer(peer) = component.icon else {
return
}
component.openPeer?(peer)
}
func animateFrom(globalFrame: CGRect, cornerRadius: CGFloat, textSnapshotView: UIView, transition: ComponentTransition) {
guard let superview = self.superview?.superview?.superview else {
return
@@ -143,8 +157,17 @@ final class MessageItemComponent: Component {
transition.animateScale(view: self.avatarNode.view, from: 0.01, to: 1.0)
}
private var cachedEntities: [MessageTextEntity]?
private var entityFiles: [MediaId: TelegramMediaFile] = [:]
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if !self.avatarNode.isHidden, self.avatarNode.frame.contains(point) {
return true
}
if let textView = self.text.view as? MultilineTextWithEntitiesComponent.View, let (_, attributes) = textView.attributes(at: self.convert(point, to: textView)) {
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Spoiler)], textView.isSpoilerConcealed {
return true
}
}
return false
}
func update(component: MessageItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
@@ -170,38 +193,9 @@ final class MessageItemComponent: Component {
let avatarSize = CGSize(width: component.isNotification ? 30.0 : 28.0, height: component.isNotification ? 30.0 : 28.0)
let avatarSpacing: CGFloat = 10.0
let iconSpacing: CGFloat = 10.0
let rightInset: CGFloat = 13.0
let rightInset: CGFloat = component.isNotification ? 15.0 : 13.0
let avatarFrame = CGRect(origin: CGPoint(x: avatarInset, y: avatarInset), size: avatarSize)
if case let .peer(peer) = component.icon {
if peer.smallProfileImage != nil {
self.avatarNode.setPeerV2(
context: component.context,
theme: theme,
peer: peer,
authorOfMessage: nil,
overrideImage: nil,
emptyColor: nil,
clipStyle: .round,
synchronousLoad: true,
displayDimensions: avatarSize
)
} else {
self.avatarNode.setPeer(
context: component.context,
theme: theme,
peer: peer,
clipStyle: .round,
synchronousLoad: true,
displayDimensions: avatarSize
)
}
}
if self.avatarNode.bounds.isEmpty {
self.avatarNode.frame = avatarFrame
} else {
transition.setFrame(view: self.avatarNode.view, frame: avatarFrame)
}
var peerName = ""
if !component.isNotification, case let .peer(peer) = component.icon {
@@ -256,6 +250,8 @@ final class MessageItemComponent: Component {
spacing = avatarSpacing
case .icon:
spacing = iconSpacing
case .animation:
spacing = iconSpacing
}
let textSize = self.text.update(
@@ -268,7 +264,8 @@ final class MessageItemComponent: Component {
text: .plain(attributedText),
maximumNumberOfLines: 0,
lineSpacing: 0.0,
spoilerColor: .white
spoilerColor: .white,
handleSpoilers: true
)),
environment: {},
containerSize: CGSize(width: availableSize.width - avatarInset - avatarSize.width - spacing - rightInset, height: .greatestFiniteMagnitude)
@@ -276,7 +273,37 @@ final class MessageItemComponent: Component {
let size = CGSize(width: avatarInset + avatarSize.width + spacing + textSize.width + rightInset, height: max(minimalHeight, textSize.height + 15.0))
if case let .icon(iconName) = component.icon {
switch component.icon {
case let .peer(peer):
if peer.smallProfileImage != nil {
self.avatarNode.setPeerV2(
context: component.context,
theme: theme,
peer: peer,
authorOfMessage: nil,
overrideImage: nil,
emptyColor: nil,
clipStyle: .round,
synchronousLoad: true,
displayDimensions: avatarSize
)
} else {
self.avatarNode.setPeer(
context: component.context,
theme: theme,
peer: peer,
clipStyle: .round,
synchronousLoad: true,
displayDimensions: avatarSize
)
}
if self.avatarNode.bounds.isEmpty {
self.avatarNode.frame = avatarFrame
} else {
transition.setFrame(view: self.avatarNode.view, frame: avatarFrame)
}
self.avatarNode.isHidden = false
case let .icon(iconName):
let iconSize = self.icon.update(
transition: transition,
component: AnyComponent(BundleIconComponent(name: iconName, tintColor: .white)),
@@ -290,6 +317,31 @@ final class MessageItemComponent: Component {
}
transition.setFrame(view: iconView, frame: iconFrame)
}
self.avatarNode.isHidden = true
case let .animation(animationName):
let iconSize = self.icon.update(
transition: transition,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(
name: animationName
),
placeholderColor: nil,
startingPosition: .end,
size: CGSize(width: 40.0, height: 40.0),
loop: false
)),
environment: {},
containerSize: CGSize(width: 40.0, height: 40.0)
)
let iconFrame = CGRect(origin: CGPoint(x: avatarInset - 3.0, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
if let iconView = self.icon.view as? LottieComponent.View {
if iconView.superview == nil {
self.container.addSubview(iconView)
iconView.playOnce()
}
transition.setFrame(view: iconView, frame: iconFrame)
}
self.avatarNode.isHidden = true
}
let textFrame = CGRect(origin: CGPoint(x: avatarInset + avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
@@ -365,8 +417,6 @@ final class MessageItemComponent: Component {
}
}
}
return size
}
}

View File

@@ -34,17 +34,20 @@ final class MessageListComponent: Component {
private let items: [Item]
private let availableReactions: [ReactionItem]?
private let sendActionTransition: SendActionTransition?
private let openPeer: (EnginePeer) -> Void
init(
context: AccountContext,
items: [Item],
availableReactions: [ReactionItem]?,
sendActionTransition: SendActionTransition?
sendActionTransition: SendActionTransition?,
openPeer: @escaping (EnginePeer) -> Void
) {
self.context = context
self.items = items
self.availableReactions = availableReactions
self.sendActionTransition = sendActionTransition
self.openPeer = openPeer
}
static func == (lhs: MessageListComponent, rhs: MessageListComponent) -> Bool {
@@ -130,6 +133,15 @@ final class MessageListComponent: Component {
}
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for (_, itemView) in self.itemViews {
if let view = itemView.view, view.point(inside: self.convert(point, to: view), with: event) {
return true
}
}
return false
}
func update(component: MessageListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
@@ -167,7 +179,8 @@ final class MessageListComponent: Component {
isNotification: item.isNotification,
text: item.text,
entities: item.entities,
availableReactions: component.availableReactions
availableReactions: component.availableReactions,
openPeer: component.openPeer
)),
environment: {},
containerSize: CGSize(width: maxWidth, height: .greatestFiniteMagnitude)

View File

@@ -227,6 +227,8 @@ final class VideoChatParticipantAvatarComponent: Component {
self.isUpdating = false
}
self.isUserInteractionEnabled = false
let previousComponent = self.component
self.component = component

View File

@@ -693,8 +693,7 @@ final class VideoChatScreenComponent: Component {
} else {
text = title.isEmpty ? environment.strings.VoiceChat_EditTitleRemoveSuccess : environment.strings.VoiceChat_EditTitleSuccess(title).string
}
self.presentUndoOverlay(content: .voiceChatFlag(text: text), action: { _ in return false })
self.presentToast(icon: .animation("anim_vcflag"), text: text, duration: 3)
})
environment.controller()?.present(controller, in: .window(.root))
})
@@ -752,9 +751,7 @@ final class VideoChatScreenComponent: Component {
switch result {
case .linkCopied:
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with { $0 }
self.environment?.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_linkcopied", scale: 0.08, colors: ["info1.info1.stroke": UIColor.clear, "info2.info2.Fill": UIColor.clear], title: nil, text: presentationData.strings.CallList_ToastCallLinkCopied_Text, customUndoText: presentationData.strings.CallList_ToastCallLinkCopied_Action, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
return false
}), in: .current)
self.presentToast(icon: .icon("anim_linkcopied"), text: presentationData.strings.CallList_ToastCallLinkCopied_Text, duration: 3)
case .openCall:
break
}
@@ -841,8 +838,7 @@ final class VideoChatScreenComponent: Component {
} else {
text = ""
}
environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: isSavedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
self.presentToast(icon: .icon(isSavedMessages ? "anim_savedmessages" : "anim_forward"), text: text, duration: 3)
})
}
shareController.actionCompleted = { [weak self] in
@@ -850,7 +846,7 @@ final class VideoChatScreenComponent: Component {
return
}
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
self.presentToast(icon: .icon("anim_linkcopied"), text: presentationData.strings.VoiceChat_InviteLinkCopiedText, duration: 3)
}
environment.controller()?.present(shareController, in: .window(.root))
})
@@ -893,8 +889,7 @@ final class VideoChatScreenComponent: Component {
} else {
text = ""
}
environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: isSavedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current)
self.presentToast(icon: .icon(isSavedMessages ? "anim_savedmessages" : "anim_forward"), text: text, duration: 3)
})
}
shareController.actionCompleted = { [weak self] in
@@ -902,7 +897,7 @@ final class VideoChatScreenComponent: Component {
return
}
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
environment.controller()?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
self.presentToast(icon: .icon("anim_linkcopied"), text: presentationData.strings.VoiceChat_InviteLinkCopiedText, duration: 3)
}
environment.controller()?.present(shareController, in: .window(.root))
}
@@ -1839,7 +1834,7 @@ final class VideoChatScreenComponent: Component {
} else {
text = environment.strings.VoiceChat_DisplayAsSuccess(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
}
self.displayToast(icon: .peer(peer), text: text, duration: 3)
self.presentToast(icon: .peer(peer), text: text, duration: 3)
})
self.memberEventsDisposable?.dispose()
@@ -1866,7 +1861,7 @@ final class VideoChatScreenComponent: Component {
if displayEvent {
let text = environment.strings.VoiceChat_PeerJoinedText("**\(event.peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder))**").string
self.displayToast(icon: .peer(event.peer), text: text, duration: 3)
self.presentToast(icon: .peer(event.peer), text: text, duration: 3)
}
}
} else {
@@ -3936,6 +3931,8 @@ final class VideoChatScreenComponent: Component {
icon = .peer(peer)
case let .icon(name):
icon = .icon(name)
case let .animation(name):
icon = .animation(name)
}
messageItems.insert(
MessageListComponent.Item(
@@ -3957,7 +3954,13 @@ final class VideoChatScreenComponent: Component {
context: call.accountContext,
items: messageItems,
availableReactions: self.reactionItems,
sendActionTransition: sendActionTransition
sendActionTransition: sendActionTransition,
openPeer: { [weak self] peer in
guard let self else {
return
}
self.openPeer(peer)
}
)),
environment: {},
containerSize: CGSize(width: isTwoColumnLayout ? mainColumnWidth : min(440.0, availableSize.width - environment.safeInsets.left - environment.safeInsets.right), height: availableSize.height - messagesBottomInset)
@@ -3975,7 +3978,6 @@ final class VideoChatScreenComponent: Component {
let messagesListFrame = CGRect(origin: CGPoint(x: messageListOriginX, y: availableSize.height - messagesListSize.height - messagesBottomInset), size: messagesListSize)
if let messagesListView = self.messagesList.view {
if messagesListView.superview == nil {
messagesListView.isUserInteractionEnabled = false
self.containerView.addSubview(messagesListView)
}
transition.setFrame(view: messagesListView, frame: messagesListFrame)

View File

@@ -136,7 +136,7 @@ extension VideoChatScreenComponent.View {
} else {
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
}
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
self.presentToast(icon: .peer(EnginePeer(participant.peer)), text: text, duration: 3)
}
} else {
if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) {
@@ -154,7 +154,7 @@ extension VideoChatScreenComponent.View {
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true })
self.presentToast(icon: .animation("anim_savedmessages"), text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string, duration: 3)
})
})]), in: .window(.root))
} else {
@@ -248,7 +248,7 @@ extension VideoChatScreenComponent.View {
} else {
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
}
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
self.presentToast(icon: .peer(peer), text: text, duration: 3)
}
}))
} else if case let .legacyGroup(groupPeer) = groupPeer {
@@ -320,7 +320,7 @@ extension VideoChatScreenComponent.View {
} else {
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
}
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
self.presentToast(icon: .peer(peer), text: text, duration: 3)
}
}))
}
@@ -361,7 +361,7 @@ extension VideoChatScreenComponent.View {
if let link {
UIPasteboard.general.string = link
self.presentUndoOverlay(content: .linkCopied(title: nil, text: environment.strings.VoiceChat_InviteLinkCopiedText), action: { _ in return false })
self.presentToast(icon: .animation("anim_linkcopied"), text: environment.strings.VoiceChat_InviteLinkCopiedText, duration: 3)
}
})
}

View File

@@ -249,7 +249,7 @@ extension VideoChatScreenComponent.View {
iconName = "Call/ToastMessagesDisabled"
text = environment.strings.VoiceChat_ToastMessagesDisabled
}
self.displayToast(icon: .icon(iconName), text: text, duration: 3)
self.presentToast(icon: .icon(iconName), text: text, duration: 3)
})))
}
@@ -483,7 +483,7 @@ extension VideoChatScreenComponent.View {
text = environment.strings.VoiceChat_RecordingStarted
}
self.presentUndoOverlay(content: .voiceChatRecording(text: text), action: { _ in return false })
self.presentToast(icon: .animation("anim_vcrecord"), text: text, duration: 3)
groupCall.playTone(.recordingStarted)
})
environment.controller()?.present(controller, in: .window(.root))

View File

@@ -133,7 +133,7 @@ extension VideoChatScreenComponent.View {
}).start()
}
self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditBioSuccess, timeout: nil, customUndoText: nil), action: { _ in return false })
self.presentToast(icon: .animation("anim_infotip"), text: environment.strings.VoiceChat_EditBioSuccess, duration: 4)
})
environment.controller()?.present(controller, in: .window(.root))
}
@@ -155,7 +155,7 @@ extension VideoChatScreenComponent.View {
}
let _ = currentCall.accountContext.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).startStandalone()
self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditNameSuccess, timeout: nil, customUndoText: nil), action: { _ in return false })
self.presentToast(icon: .animation("anim_infotip"), text: environment.strings.VoiceChat_EditNameSuccess, duration: 4)
})
environment.controller()?.present(controller, in: .window(.root))
}
@@ -190,7 +190,8 @@ extension VideoChatScreenComponent.View {
f(.default)
if let participantPeer = participant.peer {
self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(participantPeer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true })
let text = environment.strings.VoiceChat_UserCanNowSpeak(participantPeer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
self.presentToast(icon: .animation("anim_vcspeak"), text: text, duration: 3)
}
})))
} else {
@@ -248,23 +249,10 @@ extension VideoChatScreenComponent.View {
items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in
return generateTintedImage(image: openIcon, color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
guard let self, let environment = self.environment, let currentCall = self.currentCall else {
guard let self else {
return
}
guard let controller = environment.controller() as? VideoChatScreenV2Impl, let navigationController = controller.parentNavigationController else {
return
}
let context = currentCall.accountContext
controller.dismiss(completion: { [weak navigationController] in
Queue.mainQueue().after(0.1) {
guard let navigationController else {
return
}
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil))
}
})
self.openPeer(peer)
f(.dismissWithoutContent)
})))
@@ -314,15 +302,14 @@ extension VideoChatScreenComponent.View {
if groupCall.isConference {
groupCall.kickPeer(id: peer.id)
//TODO:localize
self.presentUndoOverlay(content: .banned(text: "You removed \(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)) from this call. They will no longer be able to join using the invite link."), action: { _ in return false })
self.presentToast(icon: .animation("anim_banned"), text: environment.strings.VoiceChat_RemovedConferencePeerText(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string, duration: 3)
} else {
if let callPeerId = groupCall.peerId {
let _ = groupCall.accountContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: groupCall.accountContext.engine, peerId: callPeerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start()
groupCall.removedPeer(peer.id)
}
self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false })
self.presentToast(icon: .animation("anim_banned"), text: environment.strings.VoiceChat_RemovedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string, duration: 3)
}
}))
@@ -425,6 +412,24 @@ extension VideoChatScreenComponent.View {
environment.controller()?.presentInGlobalOverlay(contextController)
}
func openPeer(_ peer: EnginePeer) {
guard let environment = self.environment, let currentCall = self.currentCall else {
return
}
guard let controller = environment.controller() as? VideoChatScreenV2Impl, let navigationController = controller.parentNavigationController else {
return
}
let context = currentCall.accountContext
controller.dismiss(completion: { [weak navigationController] in
Queue.mainQueue().after(0.1) {
guard let navigationController else {
return
}
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, purposefulAction: {}, peekData: nil))
}
})
}
private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) {
guard let currentCall = self.currentCall else {
return

View File

@@ -161,7 +161,7 @@ final class VideoChatTitleComponent: Component {
let _ = activityStatusNode.transitionToState(.recordingVoice(NSAttributedString(string: value, font: Font.regular(13.0), textColor: UIColor(rgb: 0x34c759)), UIColor(rgb: 0x34c759)), animation: .none)
let activityStatusSize = activityStatusNode.updateLayout(CGSize(width: currentSize.width, height: 100.0), alignment: .center)
let activityStatusFrame = CGRect(origin: CGPoint(x: floor((currentSize.width - activityStatusSize.width) * 0.5), y: statusView.center.y - activityStatusSize.height * 0.5), size: activityStatusSize)
let activityStatusFrame = CGRect(origin: CGPoint(x: floor((currentSize.width - activityStatusSize.width) * 0.5), y: statusView.center.y - activityStatusSize.height * 0.5 + 7.0), size: activityStatusSize)
let activityStatusNodeView = activityStatusNode.view
activityStatusNodeView.center = activityStatusFrame.center

View File

@@ -5,10 +5,11 @@ import TelegramCore
enum VideoChatNotificationIcon {
case peer(EnginePeer)
case icon(String)
case animation(String)
}
extension VideoChatScreenComponent.View {
func displayToast(icon: VideoChatNotificationIcon, text: String, duration: Int32) {
func presentToast(icon: VideoChatNotificationIcon, text: String, duration: Int32) {
let id = Int64.random(in: 0 ..< .max)
let expiresOn = Int32(CFAbsoluteTimeGetCurrent()) + duration

View File

@@ -193,11 +193,12 @@ public final class ListSectionContentView: UIView {
}
}
var separatorInset: CGFloat = 0.0
let separatorRightInset: CGFloat = configuration.style == .glass ? 16.0 : 0.0
if let itemComponentView = itemComponentView as? ListSectionComponentChildView {
separatorInset = itemComponentView.separatorInset
}
let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: width - separatorInset, height: UIScreenPixel))
let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: width - separatorInset - separatorRightInset, height: UIScreenPixel))
if isAdded && itemComponentView is ListSubSectionComponent.View {
readyItem.itemView.frame = itemFrame
@@ -311,6 +312,14 @@ public final class ListSectionContentView: UIView {
public final class ListSectionComponent: Component {
public typealias ChildView = ListSectionComponentChildView
public final class TransitionHint {
public let forceUpdate: Bool
public init(forceUpdate: Bool) {
self.forceUpdate = forceUpdate
}
}
public enum Background: Equatable {
case none(clipped: Bool)
case all
@@ -417,6 +426,11 @@ public final class ListSectionComponent: Component {
func update(component: ListSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
var forceUpdate = false
if let hint = transition.userData(TransitionHint.self) {
forceUpdate = hint.forceUpdate
}
let headerSideInset: CGFloat = 16.0
var contentHeight: CGFloat = 0.0
@@ -472,6 +486,7 @@ public final class ListSectionComponent: Component {
transition: itemTransition,
component: item.component,
environment: {},
forceUpdate: forceUpdate,
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
)

View File

@@ -29,6 +29,7 @@ final class GiftListItemComponent: Component {
let starGifts: [StarGift]
let selectedId: Int64?
let selectionUpdated: (StarGift.UniqueGift) -> Void
let onTabChange: () -> Void
let tag: AnyObject?
let updated: (ComponentTransition) -> Void
@@ -41,6 +42,7 @@ final class GiftListItemComponent: Component {
starGifts: [StarGift],
selectedId: Int64?,
selectionUpdated: @escaping (StarGift.UniqueGift) -> Void,
onTabChange: @escaping () -> Void,
tag: AnyObject?,
updated: @escaping (ComponentTransition) -> Void
) {
@@ -52,6 +54,7 @@ final class GiftListItemComponent: Component {
self.starGifts = starGifts
self.selectedId = selectedId
self.selectionUpdated = selectionUpdated
self.onTabChange = onTabChange
self.tag = tag
self.updated = updated
}
@@ -150,12 +153,17 @@ final class GiftListItemComponent: Component {
return
}
let previousGiftId = self.selectedGiftId
self.selectedGiftId = id
if id == 0 {
self.resaleGiftsState = nil
self.resaleGiftsDisposable.set(nil)
} else {
if previousGiftId == 0 {
component.onTabChange()
}
let resaleGiftsContext: ResaleGiftsContext
if let current = self.resaleGiftsContexts[id] {
resaleGiftsContext = current
@@ -173,7 +181,6 @@ final class GiftListItemComponent: Component {
self.resaleGiftsState = state
if !self.isUpdating {
let transition: ComponentTransition = isFirstTime ? .easeInOut(duration: 0.25) : .immediate
self.state?.updated(transition: transition)
component.updated(transition)
}
isFirstTime = false
@@ -182,7 +189,6 @@ final class GiftListItemComponent: Component {
if !self.isUpdating {
let transition: ComponentTransition = .easeInOut(duration: 0.25)
self.state?.updated(transition: transition)
component.updated(transition)
}
}

View File

@@ -188,6 +188,18 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
self.addSubnode(self.maskNode)
}
var hasBackground = false
let subject: PeerInfoCoverComponent.Subject?
if let status = item.peer?.emojiStatus, case .starGift = status.content {
subject = .status(status)
hasBackground = true
} else if let peer = item.peer {
subject = .peer(peer)
hasBackground = peer.profileColor != nil
} else {
subject = nil
}
if params.isStandalone {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: self.backgroundNode, alpha: item.showBackground ? 1.0 : 0.0)
@@ -195,7 +207,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
self.backgroundNode.isHidden = false
self.topStripeNode.isHidden = true
self.bottomStripeNode.isHidden = false
self.bottomStripeNode.isHidden = hasBackground
self.maskNode.isHidden = true
self.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width, height: separatorHeight))
@@ -239,14 +251,6 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
let avatarSize: CGFloat = 104.0
let avatarFrame = CGRect(origin: CGPoint(x: floor((coverFrame.width - avatarSize) * 0.5), y: coverFrame.minY + item.topInset + 24.0), size: CGSize(width: avatarSize, height: avatarSize))
let subject: PeerInfoCoverComponent.Subject?
if let status = item.peer?.emojiStatus, case .starGift = status.content {
subject = .status(status)
} else if let peer = item.peer {
subject = .peer(peer)
} else {
subject = nil
}
let _ = self.background.update(
transition: .immediate,
component: AnyComponent(PeerInfoCoverComponent(
@@ -314,7 +318,7 @@ final class PeerNameColorProfilePreviewItemNode: ListViewItemNode {
credibilityIcon = .emojiStatus(emojiStatus)
} else if peer.isVerified {
credibilityIcon = .verified
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled && (peer.id != item.context.account.peerId) {
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {
credibilityIcon = .premium
} else {
credibilityIcon = .none

View File

@@ -46,9 +46,11 @@ final class UserAppearanceScreenComponent: Component {
public final class TransitionHint {
public let animateTabChange: Bool
public let forceGiftsUpdate: Bool
public init(animateTabChange: Bool) {
public init(animateTabChange: Bool = false, forceGiftsUpdate: Bool = false) {
self.animateTabChange = animateTabChange
self.forceGiftsUpdate = forceGiftsUpdate
}
}
@@ -173,6 +175,8 @@ final class UserAppearanceScreenComponent: Component {
}
private var currentSection: Section = .profile
private let previewShadowView = UIImageView(image: generatePreviewShadowImage())
private let profilePreview = ComponentView<Empty>()
private let profileColorSection = ComponentView<Empty>()
private let profileResetColorSection = ComponentView<Empty>()
@@ -183,7 +187,6 @@ final class UserAppearanceScreenComponent: Component {
private let nameGiftsSection = ComponentView<Empty>()
private var isUpdating: Bool = false
private var forceNextUpdate = false
private var component: UserAppearanceScreenComponent?
private(set) weak var state: EmptyComponentState?
@@ -246,6 +249,7 @@ final class UserAppearanceScreenComponent: Component {
self.scrollView.alwaysBounceVertical = true
self.edgeEffectView = EdgeEffectView()
self.edgeEffectView.isUserInteractionEnabled = false
super.init(frame: frame)
@@ -256,6 +260,8 @@ final class UserAppearanceScreenComponent: Component {
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
self.containerView.addSubview(self.previewShadowView)
self.addSubview(self.edgeEffectView)
self.backButton.action = { [weak self] _, _ in
@@ -947,12 +953,6 @@ final class UserAppearanceScreenComponent: Component {
self.isUpdating = false
}
var forceUpdate = false
if self.forceNextUpdate {
self.forceNextUpdate = false
forceUpdate = true
}
let environment = environment[EnvironmentType.self].value
let themeUpdated = self.environment?.theme !== environment.theme
self.environment = environment
@@ -963,8 +963,10 @@ final class UserAppearanceScreenComponent: Component {
let theme = environment.theme
var animateTabChange = false
var forceGiftsUpdate = false
if let hint = transition.userData(TransitionHint.self) {
animateTabChange = hint.animateTabChange
forceGiftsUpdate = hint.forceGiftsUpdate
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
@@ -1130,14 +1132,14 @@ final class UserAppearanceScreenComponent: Component {
self.selectedProfileGift = nil
self.updatedPeerProfileColor = nil
self.updatedPeerProfileEmoji = nil
self.updatedPeerStatus = nil
}
}
} else {
self.currentSection = updatedSection
self.state?.updated(transition: .easeInOut(duration: 0.3).withUserData(TransitionHint(animateTabChange: true)))
}
}
}
}
)
),
environment: {},
@@ -1162,6 +1164,8 @@ final class UserAppearanceScreenComponent: Component {
let itemCornerRadius: CGFloat = 26.0
transition.setTintColor(view: self.previewShadowView, color: environment.theme.list.itemBlocksBackgroundColor)
switch self.currentSection {
case .profile:
var transition = transition
@@ -1209,7 +1213,10 @@ final class UserAppearanceScreenComponent: Component {
}
transition.setFrame(view: profilePreviewView, frame: profilePreviewFrame)
}
contentHeight += profilePreviewSize.height - 18.0
contentHeight += profilePreviewSize.height - 38.0
transition.setFrame(view: self.previewShadowView, frame: profilePreviewFrame.insetBy(dx: -30.0, dy: -30.0))
previewTransition.setAlpha(view: self.previewShadowView, alpha: !self.scrolledUp ? 1.0 : 0.0)
var profileLogoContents: [AnyComponentWithIdentity<Empty>] = []
profileLogoContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
@@ -1257,12 +1264,14 @@ final class UserAppearanceScreenComponent: Component {
return
}
if self.selectedProfileGift != nil {
} else {
self.selectedProfileGift = nil
self.updatedPeerProfileColor = nil
self.updatedPeerProfileEmoji = nil
self.updatedPeerStatus = nil
}
self.currentSection = .name
self.state?.updated(transition: .easeInOut(duration: 0.3).withUserData(TransitionHint(animateTabChange: true)))
}
}
)),
items: [
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor(
@@ -1391,8 +1400,10 @@ final class UserAppearanceScreenComponent: Component {
if let status = resolvedState.emojiStatus, case let .starGift(id, _, _, _, _, _, _, _, _) = status.content {
selectedGiftId = id
}
let listTransition = transition.withUserData(ListSectionComponent.TransitionHint(forceUpdate: forceGiftsUpdate))
let giftsSectionSize = self.profileGiftsSection.update(
transition: transition,
transition: listTransition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
style: .glass,
@@ -1454,13 +1465,18 @@ final class UserAppearanceScreenComponent: Component {
self.state?.updated(transition: .spring(duration: 0.4))
}
},
onTabChange: { [weak self] in
guard let self else {
return
}
if let sectionView = self.profileGiftsSection.view {
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: sectionView.frame.minY - 240.0), animated: true)
}
},
tag: giftListTag,
updated: { [weak self] transition in
if let self {
self.forceNextUpdate = true
if !self.isUpdating {
self.state?.updated(transition: transition)
}
if let self, !self.isUpdating {
self.state?.updated(transition: transition.withUserData(TransitionHint(forceGiftsUpdate: true)))
}
}
)
@@ -1469,8 +1485,8 @@ final class UserAppearanceScreenComponent: Component {
displaySeparators: false
)),
environment: {},
forceUpdate: forceUpdate,
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
forceUpdate: forceGiftsUpdate,
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude)
)
let giftsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: giftsSectionSize)
if let giftsSectionView = self.profileGiftsSection.view {
@@ -1569,7 +1585,10 @@ final class UserAppearanceScreenComponent: Component {
}
transition.setFrame(view: namePreviewView, frame: namePreviewFrame)
}
contentHeight += namePreviewSize.height - 18.0
contentHeight += namePreviewSize.height - 38.0
transition.setFrame(view: self.previewShadowView, frame: namePreviewFrame.insetBy(dx: -30.0, dy: -30.0))
previewTransition.setAlpha(view: self.previewShadowView, alpha: !self.scrolledUp ? 1.0 : 0.0)
let nameColorSectionSize = self.nameColorSection.update(
transition: transition,
@@ -1655,8 +1674,9 @@ final class UserAppearanceScreenComponent: Component {
}
}
let listTransition = transition.withUserData(ListSectionComponent.TransitionHint(forceUpdate: forceGiftsUpdate))
let giftsSectionSize = self.nameGiftsSection.update(
transition: transition,
transition: listTransition,
component: AnyComponent(ListSectionComponent(
theme: environment.theme,
style: .glass,
@@ -1692,12 +1712,18 @@ final class UserAppearanceScreenComponent: Component {
self.updatedPeerNameEmoji = peerColor.backgroundEmojiId
self.state?.updated(transition: .spring(duration: 0.4))
},
onTabChange: { [weak self] in
guard let self else {
return
}
if let sectionView = self.nameGiftsSection.view {
self.scrollView.setContentOffset(CGPoint(x: 0.0, y: sectionView.frame.minY - 240.0), animated: true)
}
},
tag: giftListTag,
updated: { [weak self] transition in
if let self {
if !self.isUpdating {
self.state?.updated(transition: transition)
}
if let self, !self.isUpdating {
self.state?.updated(transition: transition.withUserData(TransitionHint(forceGiftsUpdate: true)))
}
}
)
@@ -1706,8 +1732,8 @@ final class UserAppearanceScreenComponent: Component {
displaySeparators: false
)),
environment: {},
forceUpdate: forceUpdate,
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
forceUpdate: forceGiftsUpdate,
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude)
)
let giftsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: giftsSectionSize)
if let giftsSectionView = self.nameGiftsSection.view {
@@ -1830,14 +1856,14 @@ final class UserAppearanceScreenComponent: Component {
transition.setAlpha(view: buttonView, alpha: 1.0)
}
let edgeEffectHeight: CGFloat = availableSize.height - buttonY + 24.0
let edgeEffectHeight: CGFloat = availableSize.height - buttonY + 36.0
let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - edgeEffectHeight), size: CGSize(width: availableSize.width, height: edgeEffectHeight))
transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame)
self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, rect: edgeEffectFrame, edge: .bottom, edgeSize: edgeEffectFrame.height, transition: transition)
self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, alpha: 1.0, rect: edgeEffectFrame, edge: .bottom, edgeSize: edgeEffectFrame.height, transition: transition)
let previousBounds = self.scrollView.bounds
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
let scrollViewFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 30.0), size: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight - 30.0))
let scrollViewFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 50.0), size: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight - 50.0))
if self.scrollView.frame != scrollViewFrame {
self.scrollView.frame = scrollViewFrame
}
@@ -1856,7 +1882,7 @@ final class UserAppearanceScreenComponent: Component {
}
self.topOverscrollLayer.backgroundColor = environment.theme.list.itemBlocksBackgroundColor.cgColor
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: sideInset, y: -1000.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: 1315.0))
self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: sideInset, y: -1000.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: 1340.0))
self.updateScrolling(transition: transition)
@@ -2044,3 +2070,32 @@ final class TopBottomCornersComponent: Component {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private func generatePreviewShadowImage() -> UIImage {
let cornerRadius: CGFloat = 26.0
let shadowInset: CGFloat = 30.0
let side = (cornerRadius + 5.0) * 2.0
let fullSide = shadowInset * 2.0 + side
return generateImage(CGSize(width: fullSide, height: fullSide), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let edgeHeight = shadowInset + cornerRadius + 11.0
context.clip(to: CGRect(x: shadowInset, y: size.height - edgeHeight, width: side, height: edgeHeight))
let rect = CGRect(origin: .zero, size: CGSize(width: fullSide, height: fullSide)).insetBy(dx: shadowInset + 1.0, dy: shadowInset + 2.0)
let path = CGPath(roundedRect: rect, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
let drawShadow = {
context.addPath(path)
context.setShadow(offset: CGSize(), blur: 65.0, color: UIColor.black.cgColor)
context.setFillColor(UIColor.black.cgColor)
context.fillPath()
}
drawShadow()
drawShadow()
drawShadow()
})!.stretchableImage(withLeftCapWidth: Int(shadowInset + cornerRadius + 5), topCapHeight: Int(shadowInset + cornerRadius + 5)).withRenderingMode(.alwaysTemplate)
}