mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 11:23:48 +00:00
[WIP] Video chat UI
This commit is contained in:
parent
f203693f89
commit
c6cf576694
@ -114,6 +114,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
let layout: Layout
|
let layout: Layout
|
||||||
let expandedInsets: UIEdgeInsets
|
let expandedInsets: UIEdgeInsets
|
||||||
let safeInsets: UIEdgeInsets
|
let safeInsets: UIEdgeInsets
|
||||||
|
let openParticipantContextMenu: (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void
|
||||||
let updateMainParticipant: (VideoParticipantKey?) -> Void
|
let updateMainParticipant: (VideoParticipantKey?) -> Void
|
||||||
let updateIsMainParticipantPinned: (Bool) -> Void
|
let updateIsMainParticipantPinned: (Bool) -> Void
|
||||||
let updateIsExpandedUIHidden: (Bool) -> Void
|
let updateIsExpandedUIHidden: (Bool) -> Void
|
||||||
@ -128,6 +129,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
layout: Layout,
|
layout: Layout,
|
||||||
expandedInsets: UIEdgeInsets,
|
expandedInsets: UIEdgeInsets,
|
||||||
safeInsets: UIEdgeInsets,
|
safeInsets: UIEdgeInsets,
|
||||||
|
openParticipantContextMenu: @escaping (EnginePeer.Id, ContextExtractedContentContainingView, ContextGesture?) -> Void,
|
||||||
updateMainParticipant: @escaping (VideoParticipantKey?) -> Void,
|
updateMainParticipant: @escaping (VideoParticipantKey?) -> Void,
|
||||||
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
|
updateIsMainParticipantPinned: @escaping (Bool) -> Void,
|
||||||
updateIsExpandedUIHidden: @escaping (Bool) -> Void
|
updateIsExpandedUIHidden: @escaping (Bool) -> Void
|
||||||
@ -141,6 +143,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
self.layout = layout
|
self.layout = layout
|
||||||
self.expandedInsets = expandedInsets
|
self.expandedInsets = expandedInsets
|
||||||
self.safeInsets = safeInsets
|
self.safeInsets = safeInsets
|
||||||
|
self.openParticipantContextMenu = openParticipantContextMenu
|
||||||
self.updateMainParticipant = updateMainParticipant
|
self.updateMainParticipant = updateMainParticipant
|
||||||
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
|
self.updateIsMainParticipantPinned = updateIsMainParticipantPinned
|
||||||
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
|
self.updateIsExpandedUIHidden = updateIsExpandedUIHidden
|
||||||
@ -1015,12 +1018,21 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
rightAccessoryComponent: rightAccessoryComponent,
|
rightAccessoryComponent: rightAccessoryComponent,
|
||||||
selectionState: .none,
|
selectionState: .none,
|
||||||
hasNext: false,
|
hasNext: false,
|
||||||
action: { [weak self] peer, _, _ in
|
extractedTheme: PeerListItemComponent.ExtractedTheme(
|
||||||
guard let self else {
|
inset: 2.0,
|
||||||
|
background: UIColor(white: 0.1, alpha: 1.0)
|
||||||
|
),
|
||||||
|
action: { [weak self] peer, _, itemView in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let _ = self
|
component.openParticipantContextMenu(peer.id, itemView.extractedContainerView, nil)
|
||||||
let _ = peer
|
},
|
||||||
|
contextAction: { [weak self] peer, sourceView, gesture in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.openParticipantContextMenu(peer.id, sourceView, gesture)
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
@ -1381,7 +1393,7 @@ final class VideoChatParticipantsComponent: Component {
|
|||||||
gridParticipants.append(videoParticipant)
|
gridParticipants.append(videoParticipant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hasVideo {
|
if !hasVideo || component.layout.videoColumn != nil {
|
||||||
if participant.peer.id == component.call.accountContext.account.peerId {
|
if participant.peer.id == component.call.accountContext.account.peerId {
|
||||||
listParticipants.insert(participant, at: 0)
|
listParticipants.insert(participant, at: 0)
|
||||||
} else {
|
} else {
|
||||||
|
@ -22,6 +22,14 @@ import ShareController
|
|||||||
import AvatarNode
|
import AvatarNode
|
||||||
import TelegramAudio
|
import TelegramAudio
|
||||||
|
|
||||||
|
import DeleteChatPeerActionSheetItem
|
||||||
|
import PeerListItemComponent
|
||||||
|
import LegacyComponents
|
||||||
|
import LegacyUI
|
||||||
|
import WebSearchUI
|
||||||
|
import MapResourceToAvatarSizes
|
||||||
|
import LegacyMediaPickerUI
|
||||||
|
|
||||||
private final class VideoChatScreenComponent: Component {
|
private final class VideoChatScreenComponent: Component {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
|
|
||||||
@ -97,6 +105,10 @@ private final class VideoChatScreenComponent: Component {
|
|||||||
|
|
||||||
private var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState?
|
private var expandedParticipantsVideoState: VideoChatParticipantsComponent.ExpandedVideoState?
|
||||||
|
|
||||||
|
private let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil)
|
||||||
|
private let updateAvatarDisposable = MetaDisposable()
|
||||||
|
private var currentUpdatingAvatar: (TelegramMediaImageRepresentation, Float)?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.containerView = UIView()
|
self.containerView = UIView()
|
||||||
self.containerView.clipsToBounds = true
|
self.containerView.clipsToBounds = true
|
||||||
@ -125,6 +137,7 @@ private final class VideoChatScreenComponent: Component {
|
|||||||
self.displayAsPeersDisposable?.dispose()
|
self.displayAsPeersDisposable?.dispose()
|
||||||
self.audioOutputStateDisposable?.dispose()
|
self.audioOutputStateDisposable?.dispose()
|
||||||
self.inviteLinksDisposable?.dispose()
|
self.inviteLinksDisposable?.dispose()
|
||||||
|
self.updateAvatarDisposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateIn() {
|
func animateIn() {
|
||||||
@ -714,6 +727,637 @@ private final class VideoChatScreenComponent: Component {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openParticipantContextMenu(id: EnginePeer.Id, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) {
|
||||||
|
guard let component = self.component, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let members = self.members, let participant = members.participants.first(where: { $0.peer.id == id }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let muteStatePromise = Promise<GroupCallParticipantsContext.Participant.MuteState?>(participant.muteState)
|
||||||
|
|
||||||
|
let itemsForEntry: (GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { [weak self] muteState in
|
||||||
|
guard let self, let component = self.component, let environment = self.environment else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
guard let callState = self.callState else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
var items: [ContextMenuItem] = []
|
||||||
|
|
||||||
|
var hasVolumeSlider = false
|
||||||
|
let peer = participant.peer
|
||||||
|
if let muteState = muteState, !muteState.canUnmute || muteState.mutedByYou {
|
||||||
|
} else {
|
||||||
|
if callState.canManageCall || callState.myPeerId != id {
|
||||||
|
hasVolumeSlider = true
|
||||||
|
|
||||||
|
let minValue: CGFloat
|
||||||
|
if callState.canManageCall && callState.adminIds.contains(peer.id) && muteState != nil {
|
||||||
|
minValue = 0.01
|
||||||
|
} else {
|
||||||
|
minValue = 0.0
|
||||||
|
}
|
||||||
|
items.append(.custom(VoiceChatVolumeContextItem(minValue: minValue, value: participant.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { [weak self] newValue, finished in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if finished && newValue.isZero {
|
||||||
|
let updatedMuteState = component.call.updateMuteState(peerId: peer.id, isMuted: true)
|
||||||
|
muteStatePromise.set(.single(updatedMuteState))
|
||||||
|
} else {
|
||||||
|
component.call.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished)
|
||||||
|
}
|
||||||
|
}), true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if callState.myPeerId == id && !hasVolumeSlider && ((participant.about?.isEmpty ?? true) || participant.peer.smallProfileImage == nil) {
|
||||||
|
items.append(.custom(VoiceChatInfoContextItem(text: environment.strings.VoiceChat_ImproveYourProfileText, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Tip"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}), true))
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer.id == callState.myPeerId {
|
||||||
|
if participant.hasRaiseHand {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_CancelSpeakRequest, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/RevokeSpeak"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.call.lowerHand()
|
||||||
|
|
||||||
|
f(.default)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
items.append(.action(ContextMenuActionItem(text: peer.smallProfileImage == nil ? environment.strings.VoiceChat_AddPhoto : environment.strings.VoiceChat_ChangePhoto, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Camera"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
f(.default)
|
||||||
|
Queue.mainQueue().after(0.1) {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.openAvatarForEditing(fromGallery: false, completion: {})
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
items.append(.action(ContextMenuActionItem(text: (participant.about?.isEmpty ?? true) ? environment.strings.VoiceChat_AddBio : environment.strings.VoiceChat_EditBio, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
f(.default)
|
||||||
|
|
||||||
|
Queue.mainQueue().after(0.1) {
|
||||||
|
guard let self, let component = self.component, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let maxBioLength: Int
|
||||||
|
if peer.id.namespace == Namespaces.Peer.CloudUser {
|
||||||
|
maxBioLength = 70
|
||||||
|
} else {
|
||||||
|
maxBioLength = 100
|
||||||
|
}
|
||||||
|
let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in
|
||||||
|
guard let self, let component = self.component, let environment = self.environment, let bio else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if peer.id.namespace == Namespaces.Peer.CloudUser {
|
||||||
|
let _ = (component.call.accountContext.engine.accountData.updateAbout(about: bio)
|
||||||
|
|> `catch` { _ -> Signal<Void, NoError> in
|
||||||
|
return .complete()
|
||||||
|
}).start()
|
||||||
|
} else {
|
||||||
|
let _ = (component.call.accountContext.engine.peers.updatePeerDescription(peerId: peer.id, description: bio)
|
||||||
|
|> `catch` { _ -> Signal<Void, NoError> in
|
||||||
|
return .complete()
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditBioSuccess, timeout: nil, customUndoText: nil), action: { _ in return false })
|
||||||
|
})
|
||||||
|
environment.controller()?.present(controller, in: .window(.root))
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
if let peer = peer as? TelegramUser {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_ChangeName, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ChangeName"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
f(.default)
|
||||||
|
|
||||||
|
Queue.mainQueue().after(0.1) {
|
||||||
|
guard let self, let component = self.component, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let controller = voiceChatUserNameController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in
|
||||||
|
guard let self, let component = self.component, let environment = self.environment, let (firstName, lastName) = firstAndLastName else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = component.call.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 })
|
||||||
|
})
|
||||||
|
environment.controller()?.present(controller, in: .window(.root))
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (callState.canManageCall || callState.adminIds.contains(component.call.accountContext.account.peerId)) {
|
||||||
|
if callState.adminIds.contains(peer.id) {
|
||||||
|
if let _ = muteState {
|
||||||
|
} else {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true)
|
||||||
|
f(.default)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let muteState = muteState, !muteState.canUnmute {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmutePeer, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: participant.hasRaiseHand ? "Call/Context Menu/AllowToSpeak" : "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
guard let self, let component = self.component, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false)
|
||||||
|
f(.default)
|
||||||
|
|
||||||
|
self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(EnginePeer(participant.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true })
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true)
|
||||||
|
f(.default)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let muteState = muteState, muteState.mutedByYou {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmuteForMe, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false)
|
||||||
|
f(.default)
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MuteForMe, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor)
|
||||||
|
}, action: { [weak self] _, f in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true)
|
||||||
|
f(.default)
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let openTitle: String
|
||||||
|
let openIcon: UIImage?
|
||||||
|
if [Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup].contains(peer.id.namespace) {
|
||||||
|
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
|
||||||
|
openTitle = environment.strings.VoiceChat_OpenChannel
|
||||||
|
openIcon = UIImage(bundleImageName: "Chat/Context Menu/Channels")
|
||||||
|
} else {
|
||||||
|
openTitle = environment.strings.VoiceChat_OpenGroup
|
||||||
|
openIcon = UIImage(bundleImageName: "Chat/Context Menu/Groups")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
openTitle = environment.strings.Conversation_ContextMenuSendMessage
|
||||||
|
openIcon = UIImage(bundleImageName: "Chat/Context Menu/Message")
|
||||||
|
}
|
||||||
|
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 component = self.component, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let controller = environment.controller() as? VideoChatScreenV2Impl, let navigationController = controller.parentNavigationController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = component.call.accountContext
|
||||||
|
environment.controller()?.dismiss(completion: { [weak navigationController] in
|
||||||
|
Queue.mainQueue().after(0.3) {
|
||||||
|
guard let navigationController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(peer)), keepStack: .always, purposefulAction: {}, peekData: nil))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
f(.dismissWithoutContent)
|
||||||
|
})))
|
||||||
|
|
||||||
|
if (callState.canManageCall && !callState.adminIds.contains(peer.id)), peer.id != component.call.peerId {
|
||||||
|
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
|
||||||
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
|
||||||
|
}, action: { [weak self] c, _ in
|
||||||
|
c?.dismiss(completion: {
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(component.call.peerId)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] chatPeer in
|
||||||
|
guard let self, let component = self.component, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
||||||
|
let actionSheet = ActionSheetController(presentationData: presentationData)
|
||||||
|
var items: [ActionSheetItem] = []
|
||||||
|
|
||||||
|
let nameDisplayOrder = presentationData.nameDisplayOrder
|
||||||
|
items.append(DeleteChatPeerActionSheetItem(context: component.call.accountContext, peer: EnginePeer(peer), chatPeer: EnginePeer(chatPeer), action: .removeFromGroup, strings: environment.strings, nameDisplayOrder: nameDisplayOrder))
|
||||||
|
|
||||||
|
items.append(ActionSheetButtonItem(title: environment.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||||
|
actionSheet?.dismissAnimated()
|
||||||
|
|
||||||
|
guard let self, let component = self.component, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = component.call.accountContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: component.call.accountContext.engine, peerId: component.call.peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start()
|
||||||
|
component.call.removedPeer(peer.id)
|
||||||
|
|
||||||
|
self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(EnginePeer(peer).displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false })
|
||||||
|
}))
|
||||||
|
|
||||||
|
actionSheet.setItemGroups([
|
||||||
|
ActionSheetItemGroup(items: items),
|
||||||
|
ActionSheetItemGroup(items: [
|
||||||
|
ActionSheetButtonItem(title: environment.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||||
|
actionSheet?.dismissAnimated()
|
||||||
|
})
|
||||||
|
])
|
||||||
|
])
|
||||||
|
environment.controller()?.present(actionSheet, in: .window(.root))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = muteStatePromise.get()
|
||||||
|
|> map { muteState -> [ContextMenuItem] in
|
||||||
|
return itemsForEntry(muteState)
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
||||||
|
let contextController = ContextController(
|
||||||
|
presentationData: presentationData,
|
||||||
|
source: .extracted(ParticipantExtractedContentSource(contentView: sourceView)),
|
||||||
|
items: items |> map { items in
|
||||||
|
return ContextController.Items(content: .list(items))
|
||||||
|
},
|
||||||
|
recognizer: nil,
|
||||||
|
gesture: gesture
|
||||||
|
)
|
||||||
|
|
||||||
|
environment.controller()?.forEachController({ controller in
|
||||||
|
if let controller = controller as? UndoOverlayController {
|
||||||
|
controller.dismiss()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
environment.controller()?.presentInGlobalOverlay(contextController)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let callState = self.callState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let peerId = callState.myPeerId
|
||||||
|
|
||||||
|
let _ = (component.call.accountContext.engine.data.get(
|
||||||
|
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||||
|
TelegramEngine.EngineData.Item.Configuration.SearchBots()
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in
|
||||||
|
guard let self, let component = self.component, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let peer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
|
||||||
|
|
||||||
|
let legacyController = LegacyController(presentation: .custom, theme: environment.theme)
|
||||||
|
legacyController.statusBar.statusBarStyle = .Ignore
|
||||||
|
|
||||||
|
let emptyController = LegacyEmptyController(context: legacyController.context)!
|
||||||
|
let navigationController = makeLegacyNavigationController(rootController: emptyController)
|
||||||
|
navigationController.setNavigationBarHidden(true, animated: false)
|
||||||
|
navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0)
|
||||||
|
|
||||||
|
legacyController.bind(controller: navigationController)
|
||||||
|
|
||||||
|
self.endEditing(true)
|
||||||
|
environment.controller()?.present(legacyController, in: .window(.root))
|
||||||
|
|
||||||
|
var hasPhotos = false
|
||||||
|
if !peer.profileImageRepresentations.isEmpty {
|
||||||
|
hasPhotos = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)!
|
||||||
|
mixin.forceDark = true
|
||||||
|
mixin.stickersContext = LegacyPaintStickersContext(context: component.call.accountContext)
|
||||||
|
let _ = self.currentAvatarMixin.swap(mixin)
|
||||||
|
mixin.requestSearchController = { [weak self] assetsController in
|
||||||
|
guard let self, let component = self.component, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let controller = WebSearchController(context: component.call.accountContext, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in
|
||||||
|
assetsController?.dismiss()
|
||||||
|
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.updateProfilePhoto(result)
|
||||||
|
}))
|
||||||
|
controller.navigationPresentation = .modal
|
||||||
|
environment.controller()?.push(controller)
|
||||||
|
|
||||||
|
if fromGallery {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mixin.didFinishWithImage = { [weak self] image in
|
||||||
|
if let image = image {
|
||||||
|
completion()
|
||||||
|
self?.updateProfilePhoto(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mixin.didFinishWithVideo = { [weak self] image, asset, adjustments in
|
||||||
|
if let image = image, let asset = asset {
|
||||||
|
completion()
|
||||||
|
self?.updateProfileVideo(image, asset: asset, adjustments: adjustments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mixin.didFinishWithDelete = { [weak self] in
|
||||||
|
guard let self, let environment = self.environment else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let proceed = { [weak self] in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = self.currentAvatarMixin.swap(nil)
|
||||||
|
let postbox = component.call.accountContext.account.postbox
|
||||||
|
self.updateAvatarDisposable.set((component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in
|
||||||
|
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
|
||||||
|
})
|
||||||
|
|> deliverOnMainQueue).start())
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionSheet = ActionSheetController(presentationData: presentationData)
|
||||||
|
let items: [ActionSheetItem] = [
|
||||||
|
ActionSheetButtonItem(title: environment.strings.Settings_RemoveConfirmation, color: .destructive, action: { [weak actionSheet] in
|
||||||
|
actionSheet?.dismissAnimated()
|
||||||
|
proceed()
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
actionSheet.setItemGroups([
|
||||||
|
ActionSheetItemGroup(items: items),
|
||||||
|
ActionSheetItemGroup(items: [
|
||||||
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||||
|
actionSheet?.dismissAnimated()
|
||||||
|
})
|
||||||
|
])
|
||||||
|
])
|
||||||
|
environment.controller()?.present(actionSheet, in: .window(.root))
|
||||||
|
}
|
||||||
|
mixin.didDismiss = { [weak self, weak legacyController] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = self.currentAvatarMixin.swap(nil)
|
||||||
|
legacyController?.dismiss()
|
||||||
|
}
|
||||||
|
let menuController = mixin.present()
|
||||||
|
if let menuController = menuController {
|
||||||
|
menuController.customRemoveFromParentViewController = { [weak legacyController] in
|
||||||
|
legacyController?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateProfilePhoto(_ image: UIImage) {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let callState = self.callState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let data = image.jpegData(compressionQuality: 0.6) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let peerId = callState.myPeerId
|
||||||
|
|
||||||
|
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||||
|
component.call.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
|
||||||
|
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)
|
||||||
|
|
||||||
|
self.currentUpdatingAvatar = (representation, 0.0)
|
||||||
|
|
||||||
|
let postbox = component.call.account.postbox
|
||||||
|
let signal = peerId.namespace == Namespaces.Peer.CloudUser ? component.call.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in
|
||||||
|
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
|
||||||
|
}) : component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: component.call.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in
|
||||||
|
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.updateAvatarDisposable.set((signal
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch result {
|
||||||
|
case .complete:
|
||||||
|
self.currentUpdatingAvatar = nil
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
case let .progress(value):
|
||||||
|
self.currentUpdatingAvatar = (representation, value)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let callState = self.callState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let data = image.jpegData(compressionQuality: 0.6) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let peerId = callState.myPeerId
|
||||||
|
|
||||||
|
let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||||
|
component.call.accountContext.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data)
|
||||||
|
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)
|
||||||
|
|
||||||
|
self.currentUpdatingAvatar = (representation, 0.0)
|
||||||
|
|
||||||
|
var videoStartTimestamp: Double? = nil
|
||||||
|
if let adjustments = adjustments, adjustments.videoStartValue > 0.0 {
|
||||||
|
videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = component.call.accountContext
|
||||||
|
let account = context.account
|
||||||
|
let signal = Signal<TelegramMediaResource, UploadPeerPhotoError> { [weak self] subscriber in
|
||||||
|
let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in
|
||||||
|
if let paintingData = adjustments.paintingData, paintingData.hasAnimation {
|
||||||
|
return LegacyPaintEntityRenderer(postbox: account.postbox, adjustments: adjustments)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4")
|
||||||
|
let uploadInterface = LegacyLiveUploadInterface(context: context)
|
||||||
|
let signal: SSignal
|
||||||
|
if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer {
|
||||||
|
let durationSignal: SSignal = SSignal(generator: { subscriber in
|
||||||
|
let disposable = (entityRenderer.duration()).start(next: { duration in
|
||||||
|
subscriber.putNext(duration)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
})
|
||||||
|
|
||||||
|
return SBlockDisposable(block: {
|
||||||
|
disposable.dispose()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
signal = durationSignal.map(toSignal: { duration -> SSignal in
|
||||||
|
if let duration = duration as? Double {
|
||||||
|
return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)!
|
||||||
|
} else {
|
||||||
|
return SSignal.single(nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} else if let asset = asset as? AVAsset {
|
||||||
|
signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)!
|
||||||
|
} else {
|
||||||
|
signal = SSignal.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
let signalDisposable = signal.start(next: { next in
|
||||||
|
if let result = next as? TGMediaVideoConversionResult {
|
||||||
|
if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) {
|
||||||
|
account.postbox.mediaBox.storeResourceData(photoResource.id, data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let timestamp = videoStartTimestamp {
|
||||||
|
videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05))
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = stat()
|
||||||
|
if stat(result.fileURL.path, &value) == 0 {
|
||||||
|
if let data = try? Data(contentsOf: result.fileURL) {
|
||||||
|
let resource: TelegramMediaResource
|
||||||
|
if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult {
|
||||||
|
resource = LocalFileMediaResource(fileId: liveUploadData.id)
|
||||||
|
} else {
|
||||||
|
resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
|
||||||
|
}
|
||||||
|
account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true)
|
||||||
|
subscriber.putNext(resource)
|
||||||
|
|
||||||
|
EngineTempBox.shared.dispose(tempFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subscriber.putCompletion()
|
||||||
|
} else if let progress = next as? NSNumber {
|
||||||
|
Queue.mainQueue().async { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.currentUpdatingAvatar = (representation, Float(truncating: progress) * 0.25)
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, error: { _ in
|
||||||
|
}, completed: nil)
|
||||||
|
|
||||||
|
let disposable = ActionDisposable {
|
||||||
|
signalDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActionDisposable {
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.updateAvatarDisposable.set((signal
|
||||||
|
|> mapToSignal { videoResource -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> in
|
||||||
|
if peerId.namespace == Namespaces.Peer.CloudUser {
|
||||||
|
return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: nil, mapResourceToAvatarSizes: { resource, representations in
|
||||||
|
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in
|
||||||
|
return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch result {
|
||||||
|
case .complete:
|
||||||
|
self.currentUpdatingAvatar = nil
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
case let .progress(value):
|
||||||
|
self.currentUpdatingAvatar = (representation, 0.25 + value * 0.75)
|
||||||
|
self.state?.updated(transition: .spring(duration: 0.4))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
private func openTitleEditing() {
|
private func openTitleEditing() {
|
||||||
guard let component = self.component else {
|
guard let component = self.component else {
|
||||||
return
|
return
|
||||||
@ -1499,6 +2143,12 @@ private final class VideoChatScreenComponent: Component {
|
|||||||
layout: participantsLayout,
|
layout: participantsLayout,
|
||||||
expandedInsets: participantsExpandedInsets,
|
expandedInsets: participantsExpandedInsets,
|
||||||
safeInsets: participantsSafeInsets,
|
safeInsets: participantsSafeInsets,
|
||||||
|
openParticipantContextMenu: { [weak self] id, sourceView, gesture in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.openParticipantContextMenu(id: id, sourceView: sourceView, gesture: gesture)
|
||||||
|
},
|
||||||
updateMainParticipant: { [weak self] key in
|
updateMainParticipant: { [weak self] key in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -1866,3 +2516,25 @@ final class VideoChatScreenV2Impl: ViewControllerComponentContainer, VoiceChatCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class ParticipantExtractedContentSource: ContextExtractedContentSource {
|
||||||
|
let keepInPlace: Bool = false
|
||||||
|
let ignoreContentTouches: Bool = false
|
||||||
|
let blurBackground: Bool = true
|
||||||
|
|
||||||
|
//let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center
|
||||||
|
|
||||||
|
private let contentView: ContextExtractedContentContainingView
|
||||||
|
|
||||||
|
init(contentView: ContextExtractedContentContainingView) {
|
||||||
|
self.contentView = contentView
|
||||||
|
}
|
||||||
|
|
||||||
|
func takeView() -> ContextControllerTakeViewInfo? {
|
||||||
|
return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func putBack() -> ContextControllerPutBackViewInfo? {
|
||||||
|
return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -180,6 +180,29 @@ public final class PeerListItemComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class ExtractedTheme: Equatable {
|
||||||
|
public let inset: CGFloat
|
||||||
|
public let background: UIColor
|
||||||
|
|
||||||
|
public init(inset: CGFloat, background: UIColor) {
|
||||||
|
self.inset = inset
|
||||||
|
self.background = background
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: ExtractedTheme, rhs: ExtractedTheme) -> Bool {
|
||||||
|
if lhs === rhs {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if lhs.inset != rhs.inset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.background != rhs.background {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let strings: PresentationStrings
|
let strings: PresentationStrings
|
||||||
@ -202,7 +225,8 @@ public final class PeerListItemComponent: Component {
|
|||||||
let selectionPosition: SelectionPosition
|
let selectionPosition: SelectionPosition
|
||||||
let isEnabled: Bool
|
let isEnabled: Bool
|
||||||
let hasNext: Bool
|
let hasNext: Bool
|
||||||
let action: (EnginePeer, EngineMessage.Id?, UIView?) -> Void
|
let extractedTheme: ExtractedTheme?
|
||||||
|
let action: (EnginePeer, EngineMessage.Id?, PeerListItemComponent.View) -> Void
|
||||||
let inlineActions: InlineActionsState?
|
let inlineActions: InlineActionsState?
|
||||||
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
|
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
|
||||||
let openStories: ((EnginePeer, AvatarNode) -> Void)?
|
let openStories: ((EnginePeer, AvatarNode) -> Void)?
|
||||||
@ -230,7 +254,8 @@ public final class PeerListItemComponent: Component {
|
|||||||
selectionPosition: SelectionPosition = .left,
|
selectionPosition: SelectionPosition = .left,
|
||||||
isEnabled: Bool = true,
|
isEnabled: Bool = true,
|
||||||
hasNext: Bool,
|
hasNext: Bool,
|
||||||
action: @escaping (EnginePeer, EngineMessage.Id?, UIView?) -> Void,
|
extractedTheme: ExtractedTheme? = nil,
|
||||||
|
action: @escaping (EnginePeer, EngineMessage.Id?, PeerListItemComponent.View) -> Void,
|
||||||
inlineActions: InlineActionsState? = nil,
|
inlineActions: InlineActionsState? = nil,
|
||||||
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil,
|
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? = nil,
|
||||||
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
|
openStories: ((EnginePeer, AvatarNode) -> Void)? = nil
|
||||||
@ -257,6 +282,7 @@ public final class PeerListItemComponent: Component {
|
|||||||
self.selectionPosition = selectionPosition
|
self.selectionPosition = selectionPosition
|
||||||
self.isEnabled = isEnabled
|
self.isEnabled = isEnabled
|
||||||
self.hasNext = hasNext
|
self.hasNext = hasNext
|
||||||
|
self.extractedTheme = extractedTheme
|
||||||
self.action = action
|
self.action = action
|
||||||
self.inlineActions = inlineActions
|
self.inlineActions = inlineActions
|
||||||
self.contextAction = contextAction
|
self.contextAction = contextAction
|
||||||
@ -337,7 +363,7 @@ public final class PeerListItemComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class View: ContextControllerSourceView, ListSectionComponent.ChildView {
|
public final class View: ContextControllerSourceView, ListSectionComponent.ChildView {
|
||||||
private let extractedContainerView: ContextExtractedContentContainingView
|
public let extractedContainerView: ContextExtractedContentContainingView
|
||||||
private let containerButton: HighlightTrackingButton
|
private let containerButton: HighlightTrackingButton
|
||||||
|
|
||||||
private let swipeOptionContainer: ListItemSwipeOptionContainer
|
private let swipeOptionContainer: ListItemSwipeOptionContainer
|
||||||
@ -432,8 +458,16 @@ public final class PeerListItemComponent: Component {
|
|||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let extractedBackgroundColor: UIColor
|
||||||
|
if let extractedTheme = component.extractedTheme {
|
||||||
|
extractedBackgroundColor = extractedTheme.background
|
||||||
|
} else {
|
||||||
|
extractedBackgroundColor = component.theme.rootController.navigationBar.blurredBackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
self.containerButton.clipsToBounds = value
|
self.containerButton.clipsToBounds = value
|
||||||
self.containerButton.backgroundColor = value ? component.theme.rootController.navigationBar.blurredBackgroundColor : nil
|
self.containerButton.backgroundColor = value ? extractedBackgroundColor : nil
|
||||||
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
|
self.containerButton.layer.cornerRadius = value ? 10.0 : 0.0
|
||||||
}
|
}
|
||||||
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
|
self.extractedContainerView.willUpdateIsExtractedToContextPreview = { [weak self] value, transition in
|
||||||
@ -500,7 +534,7 @@ public final class PeerListItemComponent: Component {
|
|||||||
guard let component = self.component, let peer = component.peer else {
|
guard let component = self.component, let peer = component.peer else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
component.action(peer, component.message?.id, self.imageNode?.view)
|
component.action(peer, component.message?.id, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func avatarButtonPressed() {
|
@objc private func avatarButtonPressed() {
|
||||||
@ -620,7 +654,16 @@ public final class PeerListItemComponent: Component {
|
|||||||
labelData = ("", .neutral)
|
labelData = ("", .neutral)
|
||||||
}
|
}
|
||||||
|
|
||||||
let contextInset: CGFloat = self.isExtractedToContextMenu ? 12.0 : 0.0
|
let contextInset: CGFloat
|
||||||
|
if self.isExtractedToContextMenu {
|
||||||
|
if let extractedTheme = component.extractedTheme {
|
||||||
|
contextInset = extractedTheme.inset
|
||||||
|
} else {
|
||||||
|
contextInset = 12.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contextInset = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
let height: CGFloat
|
let height: CGFloat
|
||||||
let titleFont: UIFont
|
let titleFont: UIFont
|
||||||
@ -1104,10 +1147,11 @@ public final class PeerListItemComponent: Component {
|
|||||||
if let rightAccessoryComponentViewImpl = self.rightAccessoryComponentView?.view, let rightAccessoryComponentSize {
|
if let rightAccessoryComponentViewImpl = self.rightAccessoryComponentView?.view, let rightAccessoryComponentSize {
|
||||||
var rightAccessoryComponentTransition = transition
|
var rightAccessoryComponentTransition = transition
|
||||||
if rightAccessoryComponentViewImpl.superview == nil {
|
if rightAccessoryComponentViewImpl.superview == nil {
|
||||||
|
rightAccessoryComponentViewImpl.isUserInteractionEnabled = false
|
||||||
rightAccessoryComponentTransition = rightAccessoryComponentTransition.withAnimation(.none)
|
rightAccessoryComponentTransition = rightAccessoryComponentTransition.withAnimation(.none)
|
||||||
self.containerButton.addSubview(rightAccessoryComponentViewImpl)
|
self.containerButton.addSubview(rightAccessoryComponentViewImpl)
|
||||||
}
|
}
|
||||||
rightAccessoryComponentTransition.setFrame(view: rightAccessoryComponentViewImpl, frame: CGRect(origin: CGPoint(x: availableSize.width - rightAccessoryComponentSize.width, y: floor((height - verticalInset * 2.0 - rightAccessoryComponentSize.width) / 2.0)), size: rightAccessoryComponentSize))
|
rightAccessoryComponentTransition.setFrame(view: rightAccessoryComponentViewImpl, frame: CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + component.sideInset) - rightAccessoryComponentSize.width, y: floor((height - verticalInset * 2.0 - rightAccessoryComponentSize.width) / 2.0)), size: rightAccessoryComponentSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
var reactionIconTransition = transition
|
var reactionIconTransition = transition
|
||||||
|
@ -589,7 +589,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
message: item.message,
|
message: item.message,
|
||||||
selectionState: .none,
|
selectionState: .none,
|
||||||
hasNext: index != viewListState.totalCount - 1 || itemLayout.premiumFooterSize != nil,
|
hasNext: index != viewListState.totalCount - 1 || itemLayout.premiumFooterSize != nil,
|
||||||
action: { [weak self] peer, messageId, sourceView in
|
action: { [weak self] peer, messageId, itemView in
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -598,7 +598,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
}
|
}
|
||||||
if let messageId {
|
if let messageId {
|
||||||
component.openMessage(peer, messageId)
|
component.openMessage(peer, messageId)
|
||||||
} else if let storyItem, let sourceView {
|
} else if let storyItem, let sourceView = itemView.imageNode?.view {
|
||||||
component.openReposts(peer, storyItem.id, sourceView)
|
component.openReposts(peer, storyItem.id, sourceView)
|
||||||
} else {
|
} else {
|
||||||
component.openPeer(peer)
|
component.openPeer(peer)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user