mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Voice Chat UI improvements
This commit is contained in:
parent
21fcb34d66
commit
30d6182a85
@ -196,13 +196,16 @@ public struct PresentationGroupCallSummaryState: Equatable {
|
|||||||
public struct PresentationGroupCallMemberState: Equatable {
|
public struct PresentationGroupCallMemberState: Equatable {
|
||||||
public var ssrc: UInt32
|
public var ssrc: UInt32
|
||||||
public var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
public var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||||
|
public var speaking: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
ssrc: UInt32,
|
ssrc: UInt32,
|
||||||
muteState: GroupCallParticipantsContext.Participant.MuteState?
|
muteState: GroupCallParticipantsContext.Participant.MuteState?,
|
||||||
|
speaking: Bool
|
||||||
) {
|
) {
|
||||||
self.ssrc = ssrc
|
self.ssrc = ssrc
|
||||||
self.muteState = muteState
|
self.muteState = muteState
|
||||||
|
self.speaking = speaking
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,11 +112,15 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func stopAnimating() {
|
public func stopAnimating() {
|
||||||
|
self.stopAnimating(duration: 0.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stopAnimating(duration: Double) {
|
||||||
guard isAnimating else { return }
|
guard isAnimating else { return }
|
||||||
isAnimating = false
|
isAnimating = false
|
||||||
|
|
||||||
mediumBlob.layer.animateScale(from: 1.0, to: 0.5, duration: 0.15, removeOnCompletion: false)
|
mediumBlob.layer.animateScale(from: 1.0, to: 0.5, duration: duration, removeOnCompletion: false)
|
||||||
bigBlob.layer.animateScale(from: 1.0, to: 0.5, duration: 0.15, removeOnCompletion: false)
|
bigBlob.layer.animateScale(from: 1.0, to: 0.5, duration: duration, removeOnCompletion: false)
|
||||||
|
|
||||||
updateBlobsState()
|
updateBlobsState()
|
||||||
|
|
||||||
|
@ -64,6 +64,69 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SpeakingParticipantsContext {
|
||||||
|
private let speakingLevelThreshold: Float = 0.15
|
||||||
|
private let cutoffTimeout: Int32 = 1
|
||||||
|
private let silentTimeout: Int32 = 2
|
||||||
|
|
||||||
|
struct Participant {
|
||||||
|
let timestamp: Int32
|
||||||
|
let level: Float
|
||||||
|
}
|
||||||
|
|
||||||
|
private var participants: [PeerId: Participant] = [:]
|
||||||
|
private let speakingParticipantsPromise = ValuePromise<Set<PeerId>>()
|
||||||
|
private var speakingParticipants = Set<PeerId>() {
|
||||||
|
didSet {
|
||||||
|
self.speakingParticipantsPromise.set(self.speakingParticipants)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(levels: [(PeerId, Float)]) {
|
||||||
|
let timestamp = Int32(CFAbsoluteTimeGetCurrent())
|
||||||
|
let currentParticipants: [PeerId: Participant] = self.participants
|
||||||
|
|
||||||
|
var validSpeakers: [PeerId: Participant] = [:]
|
||||||
|
var silentParticipants = Set<PeerId>()
|
||||||
|
var speakingParticipants = Set<PeerId>()
|
||||||
|
for (peerId, level) in levels {
|
||||||
|
if level > speakingLevelThreshold {
|
||||||
|
validSpeakers[peerId] = Participant(timestamp: timestamp, level: level)
|
||||||
|
speakingParticipants.insert(peerId)
|
||||||
|
} else {
|
||||||
|
silentParticipants.insert(peerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (peerId, participant) in currentParticipants {
|
||||||
|
if let _ = validSpeakers[peerId] {
|
||||||
|
} else {
|
||||||
|
let delta = timestamp - participant.timestamp
|
||||||
|
if silentParticipants.contains(peerId) {
|
||||||
|
if delta < silentTimeout {
|
||||||
|
validSpeakers[peerId] = participant
|
||||||
|
speakingParticipants.insert(peerId)
|
||||||
|
}
|
||||||
|
} else if delta < cutoffTimeout {
|
||||||
|
validSpeakers[peerId] = participant
|
||||||
|
speakingParticipants.insert(peerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.participants = validSpeakers
|
||||||
|
self.speakingParticipants = speakingParticipants
|
||||||
|
}
|
||||||
|
|
||||||
|
func get() -> Signal<Set<PeerId>, NoError> {
|
||||||
|
return self.speakingParticipantsPromise.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public let account: Account
|
public let account: Account
|
||||||
public let accountContext: AccountContext
|
public let accountContext: AccountContext
|
||||||
private let audioSession: ManagedAudioSession
|
private let audioSession: ManagedAudioSession
|
||||||
@ -112,6 +175,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
}
|
}
|
||||||
private var audioLevelsDisposable = MetaDisposable()
|
private var audioLevelsDisposable = MetaDisposable()
|
||||||
|
|
||||||
|
|
||||||
|
private let speakingParticipantsContext = SpeakingParticipantsContext()
|
||||||
|
|
||||||
private var participantsContextStateDisposable = MetaDisposable()
|
private var participantsContextStateDisposable = MetaDisposable()
|
||||||
private var participantsContext: GroupCallParticipantsContext?
|
private var participantsContext: GroupCallParticipantsContext?
|
||||||
|
|
||||||
@ -445,6 +511,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
if !result.isEmpty {
|
if !result.isEmpty {
|
||||||
strongSelf.audioLevelsPipe.putNext(result)
|
strongSelf.audioLevelsPipe.putNext(result)
|
||||||
}
|
}
|
||||||
|
strongSelf.speakingParticipantsContext.update(levels: result)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
self.myAudioLevelDisposable.set((callContext.myAudioLevel
|
self.myAudioLevelDisposable.set((callContext.myAudioLevel
|
||||||
@ -479,8 +546,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
state: initialState
|
state: initialState
|
||||||
)
|
)
|
||||||
self.participantsContext = participantsContext
|
self.participantsContext = participantsContext
|
||||||
self.participantsContextStateDisposable.set((participantsContext.state
|
self.participantsContextStateDisposable.set((combineLatest(participantsContext.state, self.speakingParticipantsContext.get())
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] state in
|
|> deliverOnMainQueue).start(next: { [weak self] state, speakingParticipants in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -496,7 +563,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||||||
|
|
||||||
memberStates[participant.peer.id] = PresentationGroupCallMemberState(
|
memberStates[participant.peer.id] = PresentationGroupCallMemberState(
|
||||||
ssrc: participant.ssrc,
|
ssrc: participant.ssrc,
|
||||||
muteState: participant.muteState
|
muteState: participant.muteState,
|
||||||
|
speaking: speakingParticipants.contains(participant.peer.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
strongSelf.membersValue = memberStates
|
strongSelf.membersValue = memberStates
|
||||||
|
@ -94,30 +94,49 @@ public final class VoiceChatController: ViewController {
|
|||||||
let count: Int
|
let count: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct State: Equatable {
|
||||||
|
var revealedPeerId: PeerId?
|
||||||
|
}
|
||||||
|
|
||||||
private final class Interaction {
|
private final class Interaction {
|
||||||
let updateIsMuted: (PeerId, Bool) -> Void
|
let updateIsMuted: (PeerId, Bool) -> Void
|
||||||
let invitePeer: (Peer) -> Void
|
let invitePeer: (Peer) -> Void
|
||||||
let peerContextAction: (PeerEntry, ASDisplayNode, ContextGesture?) -> Void
|
let peerContextAction: (PeerEntry, ASDisplayNode, ContextGesture?) -> Void
|
||||||
|
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
|
||||||
|
|
||||||
private var audioLevels: [PeerId: ValuePipe<Float>] = [:]
|
private var audioLevels: [PeerId: ValuePipe<Float>] = [:]
|
||||||
|
|
||||||
init(
|
init(
|
||||||
updateIsMuted: @escaping (PeerId, Bool) -> Void,
|
updateIsMuted: @escaping (PeerId, Bool) -> Void,
|
||||||
invitePeer: @escaping (Peer) -> Void,
|
invitePeer: @escaping (Peer) -> Void,
|
||||||
peerContextAction: @escaping (PeerEntry, ASDisplayNode, ContextGesture?) -> Void
|
peerContextAction: @escaping (PeerEntry, ASDisplayNode, ContextGesture?) -> Void,
|
||||||
|
setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void
|
||||||
) {
|
) {
|
||||||
self.updateIsMuted = updateIsMuted
|
self.updateIsMuted = updateIsMuted
|
||||||
self.invitePeer = invitePeer
|
self.invitePeer = invitePeer
|
||||||
self.peerContextAction = peerContextAction
|
self.peerContextAction = peerContextAction
|
||||||
|
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAudioLevel(_ peerId: PeerId) -> Signal<Float, NoError>? {
|
func getAudioLevel(_ peerId: PeerId) -> Signal<Float, NoError>? {
|
||||||
|
let signal: Signal<Float, NoError>
|
||||||
if let current = self.audioLevels[peerId] {
|
if let current = self.audioLevels[peerId] {
|
||||||
return current.signal()
|
signal = current.signal()
|
||||||
} else {
|
} else {
|
||||||
let value = ValuePipe<Float>()
|
let value = ValuePipe<Float>()
|
||||||
self.audioLevels[peerId] = value
|
self.audioLevels[peerId] = value
|
||||||
return value.signal()
|
signal = value.signal()
|
||||||
|
}
|
||||||
|
return signal
|
||||||
|
|> mapToSignal { value in
|
||||||
|
if value > 0.0 {
|
||||||
|
return .single(value)
|
||||||
|
|> then(.single(0.0) |> delay(1.0, queue: Queue.mainQueue()))
|
||||||
|
} else {
|
||||||
|
return .single(value)
|
||||||
|
}
|
||||||
|
} |> mapToThrottled { next -> Signal<Float, NoError> in
|
||||||
|
return .single(next) |> then(.complete() |> delay(0.1, queue: Queue.mainQueue()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +162,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
var state: State
|
var state: State
|
||||||
var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||||
var invited: Bool
|
var invited: Bool
|
||||||
|
var revealed: Bool?
|
||||||
|
|
||||||
var stableId: PeerId {
|
var stableId: PeerId {
|
||||||
return self.peer.id
|
return self.peer.id
|
||||||
@ -167,6 +187,9 @@ public final class VoiceChatController: ViewController {
|
|||||||
if lhs.invited != rhs.invited {
|
if lhs.invited != rhs.invited {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.revealed != rhs.revealed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +223,11 @@ public final class VoiceChatController: ViewController {
|
|||||||
icon = .microphone(false, UIColor(rgb: 0x34c759))
|
icon = .microphone(false, UIColor(rgb: 0x34c759))
|
||||||
}
|
}
|
||||||
|
|
||||||
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: self.presence, text: text, icon: icon, enabled: true, audioLevel: interaction.getAudioLevel(peer.id), action: {
|
let revealOptions: [VoiceChatParticipantItem.RevealOption] = []
|
||||||
|
|
||||||
|
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: self.presence, text: text, icon: icon, enabled: true, audioLevel: interaction.getAudioLevel(peer.id), revealOptions: revealOptions, revealed: self.revealed, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
||||||
|
interaction.setPeerIdWithRevealedOptions(peerId, fromPeerId)
|
||||||
|
}, action: {
|
||||||
interaction.invitePeer(peer)
|
interaction.invitePeer(peer)
|
||||||
}, contextAction: { node, gesture in
|
}, contextAction: { node, gesture in
|
||||||
interaction.peerContextAction(self, node, gesture)
|
interaction.peerContextAction(self, node, gesture)
|
||||||
@ -292,6 +319,12 @@ public final class VoiceChatController: ViewController {
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
let statePromise = ValuePromise(State(), ignoreRepeated: true)
|
||||||
|
let stateValue = Atomic(value: State())
|
||||||
|
let updateState: ((State) -> State) -> Void = { f in
|
||||||
|
statePromise.set(stateValue.modify { f($0) })
|
||||||
|
}
|
||||||
|
|
||||||
let invitePeer: (Peer) -> Void = { [weak self] peer in
|
let invitePeer: (Peer) -> Void = { [weak self] peer in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
@ -406,6 +439,12 @@ public final class VoiceChatController: ViewController {
|
|||||||
|
|
||||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(items), reactionItems: [], gesture: gesture)
|
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: sourceNode, keepInPlace: false)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||||
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
||||||
|
}, setPeerIdWithRevealedOptions: { peerId, _ in
|
||||||
|
updateState { state in
|
||||||
|
var updated = state
|
||||||
|
updated.revealedPeerId = peerId
|
||||||
|
return updated
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
self.contentContainer.addSubnode(self.listNode)
|
self.contentContainer.addSubnode(self.listNode)
|
||||||
@ -955,7 +994,7 @@ public final class VoiceChatController: ViewController {
|
|||||||
memberState = .listening
|
memberState = .listening
|
||||||
}
|
}
|
||||||
} else if let state = memberStates[member.peer.id] {
|
} else if let state = memberStates[member.peer.id] {
|
||||||
memberState = .listening
|
memberState = state.speaking ? .speaking : .listening
|
||||||
memberMuteState = state.muteState
|
memberMuteState = state.muteState
|
||||||
} else {
|
} else {
|
||||||
memberState = .inactive
|
memberState = .inactive
|
||||||
|
@ -37,6 +37,25 @@ public final class VoiceChatParticipantItem: ListViewItem {
|
|||||||
case invite(Bool)
|
case invite(Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct RevealOption {
|
||||||
|
public enum RevealOptionType {
|
||||||
|
case neutral
|
||||||
|
case warning
|
||||||
|
case destructive
|
||||||
|
case accent
|
||||||
|
}
|
||||||
|
|
||||||
|
public var type: RevealOptionType
|
||||||
|
public var title: String
|
||||||
|
public var action: () -> Void
|
||||||
|
|
||||||
|
public init(type: RevealOptionType, title: String, action: @escaping () -> Void) {
|
||||||
|
self.type = type
|
||||||
|
self.title = title
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let presentationData: ItemListPresentationData
|
let presentationData: ItemListPresentationData
|
||||||
let dateTimeFormat: PresentationDateTimeFormat
|
let dateTimeFormat: PresentationDateTimeFormat
|
||||||
let nameDisplayOrder: PresentationPersonNameOrder
|
let nameDisplayOrder: PresentationPersonNameOrder
|
||||||
@ -47,10 +66,13 @@ public final class VoiceChatParticipantItem: ListViewItem {
|
|||||||
let icon: Icon
|
let icon: Icon
|
||||||
let enabled: Bool
|
let enabled: Bool
|
||||||
let audioLevel: Signal<Float, NoError>?
|
let audioLevel: Signal<Float, NoError>?
|
||||||
|
let revealOptions: [RevealOption]
|
||||||
|
let revealed: Bool?
|
||||||
|
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
|
||||||
let action: (() -> Void)?
|
let action: (() -> Void)?
|
||||||
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
|
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
|
||||||
|
|
||||||
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, presence: PeerPresence?, text: ParticipantText, icon: Icon, enabled: Bool, audioLevel: Signal<Float, NoError>?, action: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) {
|
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, presence: PeerPresence?, text: ParticipantText, icon: Icon, enabled: Bool, audioLevel: Signal<Float, NoError>?, revealOptions: [RevealOption], revealed: Bool?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, action: (() -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) {
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.dateTimeFormat = dateTimeFormat
|
self.dateTimeFormat = dateTimeFormat
|
||||||
self.nameDisplayOrder = nameDisplayOrder
|
self.nameDisplayOrder = nameDisplayOrder
|
||||||
@ -61,6 +83,9 @@ public final class VoiceChatParticipantItem: ListViewItem {
|
|||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.enabled = enabled
|
self.enabled = enabled
|
||||||
self.audioLevel = audioLevel
|
self.audioLevel = audioLevel
|
||||||
|
self.revealOptions = revealOptions
|
||||||
|
self.revealed = revealed
|
||||||
|
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
||||||
self.action = action
|
self.action = action
|
||||||
self.contextAction = contextAction
|
self.contextAction = contextAction
|
||||||
}
|
}
|
||||||
@ -113,7 +138,7 @@ public final class VoiceChatParticipantItem: ListViewItem {
|
|||||||
|
|
||||||
private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0))
|
private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0))
|
||||||
|
|
||||||
public class VoiceChatParticipantItemNode: ListViewItemNode {
|
public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||||
private let backgroundNode: ASDisplayNode
|
private let backgroundNode: ASDisplayNode
|
||||||
private let topStripeNode: ASDisplayNode
|
private let topStripeNode: ASDisplayNode
|
||||||
private let bottomStripeNode: ASDisplayNode
|
private let bottomStripeNode: ASDisplayNode
|
||||||
@ -139,6 +164,7 @@ public class VoiceChatParticipantItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
private var audioLevelView: VoiceBlobView?
|
private var audioLevelView: VoiceBlobView?
|
||||||
private let audioLevelDisposable = MetaDisposable()
|
private let audioLevelDisposable = MetaDisposable()
|
||||||
|
private var didSetupAudioLevel = false
|
||||||
|
|
||||||
private var absoluteLocation: (CGRect, CGSize)?
|
private var absoluteLocation: (CGRect, CGSize)?
|
||||||
|
|
||||||
@ -170,6 +196,7 @@ public class VoiceChatParticipantItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
self.avatarNode = AvatarNode(font: avatarFont)
|
self.avatarNode = AvatarNode(font: avatarFont)
|
||||||
self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
|
self.avatarNode.isLayerBacked = !smartInvertColorsEnabled()
|
||||||
|
self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 40.0))
|
||||||
|
|
||||||
self.titleNode = TextNode()
|
self.titleNode = TextNode()
|
||||||
self.titleNode.isUserInteractionEnabled = false
|
self.titleNode.isUserInteractionEnabled = false
|
||||||
@ -386,6 +413,31 @@ public class VoiceChatParticipantItemNode: ListViewItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let peerRevealOptions: [ItemListRevealOption]
|
||||||
|
var mappedOptions: [ItemListRevealOption] = []
|
||||||
|
var index: Int32 = 0
|
||||||
|
for option in item.revealOptions {
|
||||||
|
let color: UIColor
|
||||||
|
let textColor: UIColor
|
||||||
|
switch option.type {
|
||||||
|
case .neutral:
|
||||||
|
color = item.presentationData.theme.list.itemDisclosureActions.constructive.fillColor
|
||||||
|
textColor = item.presentationData.theme.list.itemDisclosureActions.constructive.foregroundColor
|
||||||
|
case .warning:
|
||||||
|
color = item.presentationData.theme.list.itemDisclosureActions.warning.fillColor
|
||||||
|
textColor = item.presentationData.theme.list.itemDisclosureActions.warning.foregroundColor
|
||||||
|
case .destructive:
|
||||||
|
color = item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor
|
||||||
|
textColor = item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor
|
||||||
|
case .accent:
|
||||||
|
color = item.presentationData.theme.list.itemDisclosureActions.accent.fillColor
|
||||||
|
textColor = item.presentationData.theme.list.itemDisclosureActions.accent.foregroundColor
|
||||||
|
}
|
||||||
|
mappedOptions.append(ItemListRevealOption(key: index, title: option.title, icon: .none, color: color, textColor: textColor))
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
peerRevealOptions = mappedOptions
|
||||||
|
|
||||||
return (layout, { [weak self] synchronousLoad, animated in
|
return (layout, { [weak self] synchronousLoad, animated in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.layoutParams = (item, params, first, last)
|
strongSelf.layoutParams = (item, params, first, last)
|
||||||
@ -488,11 +540,12 @@ public class VoiceChatParticipantItemNode: ListViewItemNode {
|
|||||||
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
|
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + titleSpacing), size: statusLayout.size))
|
||||||
|
|
||||||
let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
let avatarFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((layout.contentSize.height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||||
transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame)
|
transition.updateFrameAsPositionAndBounds(node: strongSelf.avatarNode, frame: avatarFrame)
|
||||||
|
|
||||||
let blobFrame = avatarFrame.insetBy(dx: -12.0, dy: -12.0)
|
let blobFrame = avatarFrame.insetBy(dx: -12.0, dy: -12.0)
|
||||||
if let audioLevel = item.audioLevel {
|
if let audioLevel = item.audioLevel, !strongSelf.didSetupAudioLevel || currentItem?.peer.id != item.peer.id {
|
||||||
strongSelf.audioLevelView?.frame = blobFrame
|
strongSelf.audioLevelView?.frame = blobFrame
|
||||||
|
strongSelf.didSetupAudioLevel = true
|
||||||
strongSelf.audioLevelDisposable.set((audioLevel
|
strongSelf.audioLevelDisposable.set((audioLevel
|
||||||
|> deliverOnMainQueue).start(next: { value in
|
|> deliverOnMainQueue).start(next: { value in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
@ -523,12 +576,20 @@ public class VoiceChatParticipantItemNode: ListViewItemNode {
|
|||||||
strongSelf.containerNode.view.insertSubview(audioLevelView, at: 0)
|
strongSelf.containerNode.view.insertSubview(audioLevelView, at: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let level = min(1.0, max(0.0, CGFloat(value)))
|
||||||
|
let avatarScale: CGFloat
|
||||||
|
|
||||||
strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0)
|
strongSelf.audioLevelView?.updateLevel(CGFloat(value) * 2.0)
|
||||||
if value > 0.0 {
|
if value > 0.0 {
|
||||||
strongSelf.audioLevelView?.startAnimating()
|
strongSelf.audioLevelView?.startAnimating()
|
||||||
|
avatarScale = 1.03 + level * 0.1
|
||||||
} else {
|
} else {
|
||||||
strongSelf.audioLevelView?.stopAnimating()
|
strongSelf.audioLevelView?.stopAnimating(duration: 0.5)
|
||||||
|
avatarScale = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .spring)
|
||||||
|
transition.updateTransformScale(node: strongSelf.avatarNode, scale: avatarScale, beginWithCurrentState: true)
|
||||||
}))
|
}))
|
||||||
} else if let audioLevelView = strongSelf.audioLevelView {
|
} else if let audioLevelView = strongSelf.audioLevelView {
|
||||||
strongSelf.audioLevelView = nil
|
strongSelf.audioLevelView = nil
|
||||||
@ -596,6 +657,9 @@ public class VoiceChatParticipantItemNode: ListViewItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.updateIsHighlighted(transition: transition)
|
strongSelf.updateIsHighlighted(transition: transition)
|
||||||
|
|
||||||
|
strongSelf.setRevealOptions((left: [], right: peerRevealOptions))
|
||||||
|
strongSelf.setRevealOptionsOpened(item.revealed ?? false, animated: animated)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -676,4 +740,47 @@ public class VoiceChatParticipantItemNode: ListViewItemNode {
|
|||||||
item.action?()
|
item.action?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
|
super.updateRevealOffset(offset: offset, transition: transition)
|
||||||
|
|
||||||
|
if let item = self.layoutParams?.0, let params = self.layoutParams?.1 {
|
||||||
|
var leftInset: CGFloat = 65.0 + params.leftInset
|
||||||
|
|
||||||
|
var avatarFrame = self.avatarNode.frame
|
||||||
|
avatarFrame.origin.x = offset + leftInset - 50.0
|
||||||
|
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
|
||||||
|
|
||||||
|
var titleFrame = self.titleNode.frame
|
||||||
|
titleFrame.origin.x = leftInset + offset
|
||||||
|
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||||
|
|
||||||
|
var statusFrame = self.statusNode.frame
|
||||||
|
let previousStatusFrame = statusFrame
|
||||||
|
statusFrame.origin.x = leftInset + offset
|
||||||
|
self.statusNode.frame = statusFrame
|
||||||
|
transition.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func revealOptionsInteractivelyOpened() {
|
||||||
|
if let item = self.layoutParams?.0 {
|
||||||
|
item.setPeerIdWithRevealedOptions(item.peer.id, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func revealOptionsInteractivelyClosed() {
|
||||||
|
if let item = self.layoutParams?.0 {
|
||||||
|
item.setPeerIdWithRevealedOptions(nil, item.peer.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
|
||||||
|
if let item = self.layoutParams?.0 {
|
||||||
|
item.revealOptions[Int(option.key)].action()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setRevealOptionsOpened(false, animated: true)
|
||||||
|
self.revealOptionsInteractivelyClosed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user