This commit is contained in:
Isaac 2025-11-07 22:54:56 +08:00
parent 62e92134e0
commit 4853623813
10 changed files with 83 additions and 44 deletions

View file

@ -895,6 +895,7 @@ public final class PresentationCallImpl: PresentationCall {
invite: nil,
joinAsPeerId: nil,
isStream: false,
streamPeerId: nil,
keyPair: keyPair,
conferenceSourceId: self.internalId,
isConference: true,
@ -1081,6 +1082,7 @@ public final class PresentationCallImpl: PresentationCall {
invite: nil,
joinAsPeerId: nil,
isStream: false,
streamPeerId: nil,
keyPair: keyPair,
conferenceSourceId: self.internalId,
isConference: true,

View file

@ -893,6 +893,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
invite: nil,
joinAsPeerId: nil,
isStream: false,
streamPeerId: nil,
keyPair: nil,
conferenceSourceId: nil,
isConference: false,
@ -1116,6 +1117,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
invite: invite,
joinAsPeerId: joinAsPeerId,
isStream: initialCall.isStream ?? false,
streamPeerId: nil,
keyPair: nil,
conferenceSourceId: nil,
isConference: false,
@ -1157,6 +1159,7 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
invite: nil,
joinAsPeerId: nil,
isStream: false,
streamPeerId: nil,
keyPair: keyPair,
conferenceSourceId: nil,
isConference: true,

View file

@ -807,6 +807,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var screencastStateDisposable: Disposable?
public let isStream: Bool
private let streamPeerId: EnginePeer.Id?
private let sharedAudioContext: SharedCallAudioContext?
public let isConference: Bool
@ -854,6 +855,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
invite: String?,
joinAsPeerId: EnginePeer.Id?,
isStream: Bool,
streamPeerId: EnginePeer.Id?,
keyPair: TelegramKeyPair?,
conferenceSourceId: CallSessionInternalId?,
isConference: Bool,
@ -888,6 +890,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.hasVideo = false
self.hasScreencast = false
self.isStream = isStream
self.streamPeerId = streamPeerId
self.conferenceSourceId = conferenceSourceId
self.isConference = isConference
self.beginWithVideo = beginWithVideo
@ -2051,6 +2054,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
callId: callInfo.id,
reference: reference,
isStream: self.isStream,
streamPeerId: self.streamPeerId,
preferMuted: isEffectivelyMuted,
joinPayload: joinPayload,
peerAdminIds: peerAdminIds,
@ -2306,7 +2310,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
let accountContext = self.accountContext
let peerId = self.peerId
let rawAdminIds: Signal<Set<PeerId>, NoError>
if let peerId {
if let peerId = peerId ?? self.streamPeerId {
if peerId.namespace == Namespaces.Peer.CloudChannel {
rawAdminIds = Signal { subscriber in
let (disposable, _) = accountContext.peerChannelMemberCategoriesContextsManager.admins(engine: accountContext.engine, postbox: accountContext.account.postbox, network: accountContext.account.network, accountPeerId: accountContext.account.peerId, peerId: peerId, updated: { list in
@ -2346,7 +2350,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
let peer: Signal<EnginePeer?, NoError>
if let peerId {
if let peerId = peerId ?? self.streamPeerId {
peer = accountContext.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
} else {
peer = .single(nil)

View file

@ -644,7 +644,7 @@ public class JoinGroupCallE2E {
}
}
func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?, callId: Int64, reference: InternalGroupCallReference, isStream: Bool, preferMuted: Bool, joinPayload: String, peerAdminIds: Signal<[PeerId], NoError>, inviteHash: String? = nil, generateE2E: ((Data?) -> JoinGroupCallE2E?)?) -> Signal<JoinGroupCallResult, JoinGroupCallError> {
func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?, callId: Int64, reference: InternalGroupCallReference, isStream: Bool, streamPeerId: PeerId?, preferMuted: Bool, joinPayload: String, peerAdminIds: Signal<[PeerId], NoError>, inviteHash: String? = nil, generateE2E: ((Data?) -> JoinGroupCallE2E?)?) -> Signal<JoinGroupCallResult, JoinGroupCallError> {
enum InternalJoinError {
case error(JoinGroupCallError)
case restart
@ -750,7 +750,7 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?,
return joinRequest
|> mapToSignal { updates -> Signal<JoinGroupCallResult, InternalJoinError> in
let peer = account.postbox.transaction { transaction -> Peer? in
return peerId.flatMap(transaction.getPeer)
return (peerId ?? streamPeerId).flatMap(transaction.getPeer)
}
|> castError(InternalJoinError.self)
@ -820,6 +820,9 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?,
state.sortAscending = parsedCall.sortAscending
state.adminIds = Set(peerAdminIds)
if isStream, let channel = peer as? TelegramChannel, channel.hasPermission(.manageCalls) {
state.adminIds.insert(account.peerId)
}
let connectionMode: JoinGroupCallResult.ConnectionMode
if let clientParamsData = parsedClientParams.data(using: .utf8), let dict = (try? JSONSerialization.jsonObject(with: clientParamsData, options: [])) as? [String: Any] {

View file

@ -58,8 +58,8 @@ public extension TelegramEngine {
return _internal_getGroupCallParticipants(account: self.account, reference: reference, offset: offset, ssrcs: ssrcs, limit: limit, sortAscending: sortAscending, isStream: false)
}
public func joinGroupCall(peerId: PeerId?, joinAs: PeerId?, callId: Int64, reference: InternalGroupCallReference, isStream: Bool, preferMuted: Bool, joinPayload: String, peerAdminIds: Signal<[PeerId], NoError>, inviteHash: String? = nil, generateE2E: ((Data?) -> JoinGroupCallE2E?)?) -> Signal<JoinGroupCallResult, JoinGroupCallError> {
return _internal_joinGroupCall(account: self.account, peerId: peerId, joinAs: joinAs, callId: callId, reference: reference, isStream: isStream, preferMuted: preferMuted, joinPayload: joinPayload, peerAdminIds: peerAdminIds, inviteHash: inviteHash, generateE2E: generateE2E)
public func joinGroupCall(peerId: PeerId?, joinAs: PeerId?, callId: Int64, reference: InternalGroupCallReference, isStream: Bool, streamPeerId: PeerId?, preferMuted: Bool, joinPayload: String, peerAdminIds: Signal<[PeerId], NoError>, inviteHash: String? = nil, generateE2E: ((Data?) -> JoinGroupCallE2E?)?) -> Signal<JoinGroupCallResult, JoinGroupCallError> {
return _internal_joinGroupCall(account: self.account, peerId: peerId, joinAs: joinAs, callId: callId, reference: reference, isStream: isStream, streamPeerId: streamPeerId, preferMuted: preferMuted, joinPayload: joinPayload, peerAdminIds: peerAdminIds, inviteHash: inviteHash, generateE2E: generateE2E)
}
public func joinGroupCallAsScreencast(callId: Int64, accessHash: Int64, joinPayload: String) -> Signal<JoinGroupCallAsScreencastResult, JoinGroupCallError> {

View file

@ -945,25 +945,13 @@ public final class MessageInputPanelComponent: Component {
var inlineActions: [ChatTextInputPanelComponent.InlineAction] = []
if component.liveChatState?.isEnabled == true {
if component.paidMessageAction != nil && self.textInputPanelExternalState.textInputState.inputText.length == 0 {
inlineActions.append(ChatTextInputPanelComponent.InlineAction(
kind: .paidMessage,
action: { [weak self] in
guard let self else {
return
}
self.component?.paidMessageAction?()
}
))
} else if let inputMode {
if let inputMode {
let mappedInputMode: ChatTextInputPanelComponent.InputMode
switch inputMode {
case .text:
mappedInputMode = .text
case .emoji:
case .emoji, .stickers:
mappedInputMode = .emoji
case .stickers:
mappedInputMode = .stickers
}
inlineActions.append(ChatTextInputPanelComponent.InlineAction(
kind: .inputMode(mappedInputMode),
@ -975,6 +963,17 @@ public final class MessageInputPanelComponent: Component {
}
))
}
if component.paidMessageAction != nil && self.textInputPanelExternalState.textInputState.inputText.length == 0 {
inlineActions.append(ChatTextInputPanelComponent.InlineAction(
kind: .paidMessage,
action: { [weak self] in
guard let self else {
return
}
self.component?.paidMessageAction?()
}
))
}
}
let placeholder: String
@ -1106,15 +1105,17 @@ public final class MessageInputPanelComponent: Component {
containerSize: availableSize
)
let inputPanelFrame = CGRect(origin: CGPoint(), size: inputPanelSize)
if let inputPanelView = inputPanel.view {
var inputPanelViewIsActive = false
if let inputPanelView = inputPanel.view as? ChatTextInputPanelComponent.View {
if inputPanelView.superview == nil {
inputPanel.parentState = state
self.addSubview(inputPanelView)
}
transition.setFrame(view: inputPanelView, frame: inputPanelFrame)
inputPanelViewIsActive = inputPanelView.isActive
}
component.externalState.isEditing = self.textInputPanelExternalState.isEditing
component.externalState.isEditing = self.textInputPanelExternalState.isEditing || inputPanelViewIsActive
component.externalState.hasText = self.textInputPanelExternalState.textInputState.inputText.length != 0
component.externalState.isKeyboardHidden = component.hideKeyboard
component.externalState.insertText = { [weak self] text in

View file

@ -124,7 +124,18 @@ public final class PeerNameTextComponent: Component {
containerSize: availableSize
)
var titleX: CGFloat = 0.0
let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
var size = CGSize(width: titleFrame.maxX, height: titleSize.height)
if let iconContent {
let icon: ComponentView<Empty>
if let current = self.icon {
@ -148,30 +159,21 @@ public final class PeerNameTextComponent: Component {
environment: {},
containerSize: containerSize
)
let iconFrame = CGRect(origin: CGPoint(x: titleX, y: floorToScreenPixels((titleSize.height - iconSize.height) * 0.5)), size: iconSize)
titleX += iconSize.width + 4.0
size.width += 3.0
let iconFrame = CGRect(origin: CGPoint(x: size.width, y: floorToScreenPixels((titleSize.height - iconSize.height) * 0.5)), size: iconSize)
if let iconView = icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
}
size.width += iconSize.width
} else if let icon = self.icon {
self.icon = nil
icon.view?.removeFromSuperview()
}
let titleFrame = CGRect(origin: CGPoint(x: titleX, y: 0.0), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
}
return CGSize(width: titleFrame.maxX, height: titleSize.height)
return size
}
}

View file

@ -36,6 +36,7 @@ final class StoryContentLiveChatComponent: Component {
let storyPeerId: EnginePeer.Id
let insets: UIEdgeInsets
let isEmbeddedInCamera: Bool
let minPaidStars: Int?
let controller: () -> ViewController?
init(
@ -47,6 +48,7 @@ final class StoryContentLiveChatComponent: Component {
storyPeerId: EnginePeer.Id,
insets: UIEdgeInsets,
isEmbeddedInCamera: Bool,
minPaidStars: Int?,
controller: @escaping () -> ViewController?
) {
self.external = external
@ -57,6 +59,7 @@ final class StoryContentLiveChatComponent: Component {
self.storyPeerId = storyPeerId
self.insets = insets
self.isEmbeddedInCamera = isEmbeddedInCamera
self.minPaidStars = minPaidStars
self.controller = controller
}
@ -85,6 +88,9 @@ final class StoryContentLiveChatComponent: Component {
if lhs.isEmbeddedInCamera != rhs.isEmbeddedInCamera {
return false
}
if lhs.minPaidStars != rhs.minPaidStars {
return false
}
return true
}
@ -436,6 +442,12 @@ final class StoryContentLiveChatComponent: Component {
isMyMessage = true
canDelete = true
}
var isMessageFromAdmin = false
if message.isFromAdmin {
isMessageFromAdmin = true
} else if message.author?.id == component.storyPeerId {
isMessageFromAdmin = true
}
//TODO:localize
if !isMyMessage, let author = message.author {
@ -467,13 +479,6 @@ final class StoryContentLiveChatComponent: Component {
})))
}
#if DEBUG
if "".isEmpty {
isAdmin = true
canDelete = true
}
#endif
if canDelete {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in
guard let self else {
@ -484,7 +489,7 @@ final class StoryContentLiveChatComponent: Component {
guard let self else {
return
}
if isAdmin && !isMyMessage {
if isAdmin && !isMyMessage && !isMessageFromAdmin {
self.displayDeleteMessageAndBan(id: id)
} else {
self.displayDeleteMessageConfirmation(id: id)
@ -616,6 +621,12 @@ final class StoryContentLiveChatComponent: Component {
for message in messagesState.pinnedMessages.reversed() {
if let author = message.author, let paidStars = message.paidStars {
if let minPaidStars = component.minPaidStars {
if Int(paidStars) < minPaidStars {
continue
}
}
if let current = topMessageByPeerId[author.id] {
if let currentPaidStars = current.paidStars, currentPaidStars < paidStars {
topMessageByPeerId[author.id] = message

View file

@ -296,6 +296,9 @@ final class StoryItemContentComponent: Component {
if let _ = self.overlaysView.hitTest(self.convert(self.convert(point, to: self.overlaysView), to: self.overlaysView), with: nil) {
return false
}
if self.liveChat != nil {
return false
}
return true
}
@ -452,6 +455,7 @@ final class StoryItemContentComponent: Component {
invite: nil,
joinAsPeerId: nil,
isStream: !(component.isEmbeddedInCamera && liveStream.kind == .rtc),
streamPeerId: component.peer.id,
keyPair: nil,
conferenceSourceId: nil,
isConference: false,
@ -938,6 +942,11 @@ final class StoryItemContentComponent: Component {
self.liveChat = liveChat
}
var minPaidStars: Int?
if let mediaStreamCallState = self.mediaStreamCallState {
minPaidStars = mediaStreamCallState.minMessagePrice.flatMap(Int.init)
}
let _ = liveChat.update(
transition: mediaStreamTransition,
component: AnyComponent(StoryContentLiveChatComponent(
@ -949,6 +958,7 @@ final class StoryItemContentComponent: Component {
storyPeerId: component.peer.id,
insets: environment.containerInsets,
isEmbeddedInCamera: component.isEmbeddedInCamera,
minPaidStars: minPaidStars,
controller: { [weak self] in
guard let self, let component = self.component else {
return nil

View file

@ -949,6 +949,9 @@ public final class StoryItemSetContainerComponent: Component {
self.sendMessageContext.currentInputMode = .text
if hasFirstResponder(self) {
view.deactivateInput()
if self.sendMessageContext.inputMediaNode != nil {
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged(isFocused: false))))
}
} else {
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(view: nil, kind: .textFocusChanged(isFocused: false))))
}