Various improvements

This commit is contained in:
Ilya Laktyushin 2025-09-27 01:27:56 +04:00
parent b8bf673c5f
commit 4657ac752d
27 changed files with 462 additions and 160 deletions

View file

@ -15098,11 +15098,12 @@ Error: %8$@";
"PeerInfo.NotesInfo" = "only visible to you";
"PeerInfo.AddNotesPlaceholder" = "Add Notes";
"PeerInfo.AddNotesInfo" = "Notes are only visible to you.";
"PeerInfo.NoteActionCopy" = "Copy Note";
"PeerInfo.NoteActionEdit" = "Edit Note";
"PeerInfo.ToastNoteCopied" = "Note copied to clipboard.";
"PeerInfo.ViewDiscussion" = "View Discussion";
"MESSAGE_SUGGEST_BIRTHDAY" = "%@ suggested you your birthday";
"Gift.UnavailableAction.Title" = "Action Locked";
@ -15111,3 +15112,22 @@ Error: %8$@";
"Gift.UnavailableAction.OpenFragment_URL" = "https://fragment.com";
"Gift.Unique.Telegram" = "Telegram";
"VoiceChat.ContextEnableMessages" = "Enable Messages";
"VoiceChat.ContextDisableMessages" = "Disable Messages";
"VoiceChat.ToastMessagesEnabled" = "Messages enabled";
"VoiceChat.ToastMessagesDisabled" = "Messages disabled";
"VoiceChat.Rotate" = "rotate";
"VoiceChat.Message" = "message";
"VoiceChat.ShareButton" = "share";
"VoiceChat.MessagePlaceholder" = "Message";
"VoiceChat.SubtitleParticipants_1" = " participant";
"VoiceChat.SubtitleParticipants_any" = " participants";
"VoiceChat.SubtitleInvited_1" = " invited";
"VoiceChat.SubtitleInvited_any" = " invited";
"ProfileLevelInfo.NegativeRating" = "Negative rating";
"Notification.StarsGift.Assigned" = "You started displaying %@ on your Telegram profile page.";

View file

@ -505,6 +505,7 @@ public protocol PresentationGroupCall: AnyObject {
func disableScreencast()
func switchVideoCamera()
func updateDefaultParticipantsAreMuted(isMuted: Bool)
func updateMessagesEnabled(isEnabled: Bool)
func setVolume(peerId: EnginePeer.Id, volume: Int32, sync: Bool)
func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo])
func setSuspendVideoChannelRequests(_ value: Bool)

View file

@ -237,7 +237,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode {
var glassBackgroundFrame = contentBounds.insetBy(dx: 10.0, dy: 10.0)
glassBackgroundFrame.size.height -= 8.0
transition.updateFrame(view: glassBackgroundView, frame: glassBackgroundFrame, beginWithCurrentState: true)
glassBackgroundView.update(size: glassBackgroundFrame.size, cornerRadius: 23.0, isDark: true, tintColor: .init(kind: .panel, color: UIColor(rgb: 0x4d4f5c, alpha: 0.6)), transition: ComponentTransition(transition))
glassBackgroundView.update(size: glassBackgroundFrame.size, cornerRadius: 23.0, isDark: true, tintColor: .init(kind: .custom, color: UIColor(rgb: 0x25272e, alpha: 0.72)), transition: ComponentTransition(transition))
transition.updateFrame(view: self.backgroundTintView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentBounds.width, height: contentBounds.height)).insetBy(dx: -10.0, dy: -10.0))
} else {

View file

@ -26,7 +26,6 @@ import GZip
import BalancedTextComponent
import Markdown
import PremiumStarComponent
import GlassBackgroundComponent
public final class ReactionItem {
public struct Reaction: Equatable {

View file

@ -13,10 +13,20 @@ import AccountContext
import TextFormat
import TelegramPresentationData
import ReactionSelectionNode
import BundleIconComponent
import Markdown
private let glassColor = UIColor(rgb: 0x25272e, alpha: 0.72)
final class MessageItemComponent: Component {
public enum Icon: Equatable {
case peer(EnginePeer)
case icon(String)
}
private let context: AccountContext
private let peer: EnginePeer
private let icon: Icon
private let isNotification: Bool
private let text: String
private let entities: [MessageTextEntity]
private let availableReactions: [ReactionItem]?
@ -24,14 +34,16 @@ final class MessageItemComponent: Component {
init(
context: AccountContext,
peer: EnginePeer,
icon: Icon,
isNotification: Bool,
text: String,
entities: [MessageTextEntity],
availableReactions: [ReactionItem]?,
avatarTapped: @escaping () -> Void = {}
) {
self.context = context
self.peer = peer
self.icon = icon
self.isNotification = isNotification
self.text = text
self.entities = entities
self.availableReactions = availableReactions
@ -42,7 +54,7 @@ final class MessageItemComponent: Component {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
if lhs.icon != rhs.icon {
return false
}
if lhs.text != rhs.text {
@ -61,6 +73,7 @@ final class MessageItemComponent: Component {
private let container: UIView
private let background: GlassBackgroundView
private let avatarNode: AvatarNode
private let icon: ComponentView<Empty>
private let text: ComponentView<Empty>
weak var standaloneReactionAnimation: StandaloneReactionAnimation?
@ -74,6 +87,7 @@ final class MessageItemComponent: Component {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0))
self.icon = ComponentView()
self.text = ComponentView()
super.init(frame: frame)
@ -107,8 +121,8 @@ final class MessageItemComponent: Component {
transition.setPosition(view: textSnapshotView, position: CGPoint(x: textSnapshotView.center.x + 71.0, y: textSnapshotView.center.y))
let initialSize = self.background.frame.size
self.background.update(size: globalFrame.size, cornerRadius: cornerRadius, isDark: true, tintColor: .init(kind: .panel, color: UIColor(rgb: 0x4d4f5c, alpha: 0.6)), transition: .immediate)
self.background.update(size: initialSize, cornerRadius: 18.0, isDark: true, tintColor: .init(kind: .panel, color: UIColor(rgb: 0x4d4f5c, alpha: 0.6)), transition: transition)
self.background.update(size: globalFrame.size, cornerRadius: cornerRadius, isDark: true, tintColor: .init(kind: .custom, color: glassColor), transition: .immediate)
self.background.update(size: initialSize, cornerRadius: 18.0, isDark: true, tintColor: .init(kind: .custom, color: glassColor), transition: transition)
let deltaX = (globalFrame.width - self.container.frame.width) / 2.0
let deltaY = (globalFrame.height - self.container.frame.height) / 2.0
@ -147,45 +161,51 @@ final class MessageItemComponent: Component {
let textColor: UIColor = .white
let linkColor: UIColor = UIColor(rgb: 0x59b6fa)
let minimalHeight: CGFloat = 36.0
let minimalHeight: CGFloat = component.isNotification ? 50.0 : 36.0
let cornerRadius = minimalHeight * 0.5
let avatarInset: CGFloat = 4.0
let avatarSize = CGSize(width: minimalHeight - avatarInset * 2.0, height: minimalHeight - avatarInset * 2.0)
let avatarInset: CGFloat = component.isNotification ? 10.0 : 4.0
let avatarSize = CGSize(width: component.isNotification ? 30.0 : 28.0, height: component.isNotification ? 30.0 : 28.0)
let avatarSpacing: CGFloat = 10.0
let iconSpacing: CGFloat = 10.0
let rightInset: CGFloat = 13.0
let avatarFrame = CGRect(origin: CGPoint(x: avatarInset, y: avatarInset), size: avatarSize)
if component.peer.smallProfileImage != nil {
self.avatarNode.setPeerV2(
context: component.context,
theme: theme,
peer: component.peer,
authorOfMessage: nil,
overrideImage: nil,
emptyColor: nil,
clipStyle: .round,
synchronousLoad: true,
displayDimensions: avatarSize
)
} else {
self.avatarNode.setPeer(
context: component.context,
theme: theme,
peer: component.peer,
clipStyle: .round,
synchronousLoad: true,
displayDimensions: avatarSize
)
if case let .peer(peer) = component.icon {
if peer.smallProfileImage != nil {
self.avatarNode.setPeerV2(
context: component.context,
theme: theme,
peer: peer,
authorOfMessage: nil,
overrideImage: nil,
emptyColor: nil,
clipStyle: .round,
synchronousLoad: true,
displayDimensions: avatarSize
)
} else {
self.avatarNode.setPeer(
context: component.context,
theme: theme,
peer: peer,
clipStyle: .round,
synchronousLoad: true,
displayDimensions: avatarSize
)
}
}
if self.avatarNode.bounds.isEmpty {
self.avatarNode.frame = avatarFrame
} else {
transition.setFrame(view: self.avatarNode.view, frame: avatarFrame)
}
var peerName = component.peer.compactDisplayTitle
if peerName.count > 40 {
peerName = "\(peerName.prefix(40))"
var peerName = ""
if !component.isNotification, case let .peer(peer) = component.icon {
peerName = peer.compactDisplayTitle
if peerName.count > 40 {
peerName = "\(peerName.prefix(40))"
}
}
let text = component.text
@ -208,8 +228,32 @@ final class MessageItemComponent: Component {
}
}
let attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: linkColor, baseFont: textFont, linkFont: textFont, boldFont: boldTextFont, italicFont: textFont, boldItalicFont: boldTextFont, fixedFont: textFont, blockQuoteFont: textFont, message: nil, entityFiles: self.entityFiles).mutableCopy() as! NSMutableAttributedString
attributedText.insert(NSAttributedString(string: peerName + " ", font: boldTextFont, textColor: textColor), at: 0)
let attributedText: NSAttributedString
if component.isNotification {
attributedText = parseMarkdownIntoAttributedString(
text,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
link: MarkdownAttributeSet(font: textFont, textColor: linkColor),
linkAttribute: { _ in return nil }
)
)
} else {
let textWithAppliedEntities = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: linkColor, baseFont: textFont, linkFont: textFont, boldFont: boldTextFont, italicFont: textFont, boldItalicFont: boldTextFont, fixedFont: textFont, blockQuoteFont: textFont, message: nil, entityFiles: self.entityFiles).mutableCopy() as! NSMutableAttributedString
if !peerName.isEmpty {
textWithAppliedEntities.insert(NSAttributedString(string: peerName + " ", font: boldTextFont, textColor: textColor), at: 0)
}
attributedText = textWithAppliedEntities
}
let spacing: CGFloat
switch component.icon {
case .peer:
spacing = avatarSpacing
case .icon:
spacing = iconSpacing
}
let textSize = self.text.update(
transition: transition,
@ -223,12 +267,28 @@ final class MessageItemComponent: Component {
lineSpacing: 0.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - avatarInset - avatarSize.width - avatarSpacing - rightInset, height: .greatestFiniteMagnitude)
containerSize: CGSize(width: availableSize.width - avatarInset - avatarSize.width - spacing - rightInset, height: .greatestFiniteMagnitude)
)
let size = CGSize(width: avatarInset + avatarSize.width + avatarSpacing + textSize.width + rightInset, height: max(minimalHeight, textSize.height + 15.0))
let size = CGSize(width: avatarInset + avatarSize.width + spacing + textSize.width + rightInset, height: max(minimalHeight, textSize.height + 15.0))
let textFrame = CGRect(origin: CGPoint(x: avatarInset + avatarSize.width + avatarSpacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
if case let .icon(iconName) = component.icon {
let iconSize = self.icon.update(
transition: transition,
component: AnyComponent(BundleIconComponent(name: iconName, tintColor: .white)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let iconFrame = CGRect(origin: CGPoint(x: avatarInset, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.container.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: iconFrame)
}
}
let textFrame = CGRect(origin: CGPoint(x: avatarInset + avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
self.container.addSubview(textView)
@ -238,7 +298,7 @@ final class MessageItemComponent: Component {
transition.setFrame(view: self.container, frame: CGRect(origin: CGPoint(), size: size))
self.background.update(size: size, cornerRadius: cornerRadius, isDark: true, tintColor: .init(kind: .panel, color: UIColor(rgb: 0x4d4f5c, alpha: 0.6)), transition: transition)
self.background.update(size: size, cornerRadius: cornerRadius, isDark: true, tintColor: .init(kind: .custom, color: glassColor), transition: transition)
transition.setFrame(view: self.background, frame: CGRect(origin: CGPoint(), size: size))
if isFirstTime, let availableReactions = component.availableReactions, let textView = self.text.view {

View file

@ -10,17 +10,20 @@ import ReactionSelectionNode
final class MessageListComponent: Component {
struct Item: Equatable {
let id: AnyHashable
let peer: EnginePeer
let icon: MessageItemComponent.Icon
let isNotification: Bool
let text: String
let entities: [MessageTextEntity]
}
class SendActionTransition {
public let randomId: Int64
public let textSnapshotView: UIView
public let globalFrame: CGRect
public let cornerRadius: CGFloat
init(textSnapshotView: UIView, globalFrame: CGRect, cornerRadius: CGFloat) {
init(randomId: Int64, textSnapshotView: UIView, globalFrame: CGRect, cornerRadius: CGFloat) {
self.randomId = randomId
self.textSnapshotView = textSnapshotView
self.globalFrame = globalFrame
self.cornerRadius = cornerRadius
@ -141,7 +144,7 @@ final class MessageListComponent: Component {
let previousContentHeight = self.scrollView.contentSize.height
let wasAtBottom = self.isAtBottom(tolerance: 1.0)
let maxWidth: CGFloat = 300.0
let maxWidth: CGFloat = 330.0
var measured: [(id: AnyHashable, size: CGSize, item: MessageListComponent.Item, itemTransition: ComponentTransition)] = []
measured.reserveCapacity(component.items.count)
@ -160,7 +163,8 @@ final class MessageListComponent: Component {
transition: transition,
component: AnyComponent(MessageItemComponent(
context: component.context,
peer: item.peer,
icon: item.icon,
isNotification: item.isNotification,
text: item.text,
entities: item.entities,
availableReactions: component.availableReactions
@ -182,7 +186,7 @@ final class MessageListComponent: Component {
validKeys.insert(entry.id)
if let itemView = self.itemViews[entry.id]?.view {
var customAnimation = false
if entry.item.peer.id == component.context.account.peerId, let _ = self.nextSendActionTransition {
if let nextSendActionTransition = self.nextSendActionTransition, entry.id == AnyHashable(nextSendActionTransition.randomId) {
customAnimation = true
}
let itemFrame = CGRect(

View file

@ -1104,7 +1104,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
}
case let .call(isTerminated, _, _, _, _, _, _):
case let .call(isTerminated, _, _, _, _, _, _, _):
if isTerminated {
self.markAsCanBeRemoved()
}
@ -2615,6 +2615,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if (state.isCreator || self.stateValue.adminIds.contains(self.accountContext.account.peerId)) && state.defaultParticipantsAreMuted.canChange {
self.stateValue.defaultParticipantMuteState = state.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted
}
self.stateValue.messagesAreEnabled = state.messagesAreEnabled.isEnabled
self.stateValue.recordingStartTimestamp = state.recordingStartTimestamp
self.stateValue.title = state.title
self.stateValue.scheduleTimestamp = state.scheduleTimestamp
@ -3907,6 +3908,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self.participantsContext?.updateDefaultParticipantsAreMuted(isMuted: isMuted)
}
public func updateMessagesEnabled(isEnabled: Bool) {
self.participantsContext?.updateMessagesEnabled(isEnabled: isEnabled)
}
func video(endpointId: String) -> Signal<OngoingGroupCallContext.VideoFrameData, NoError>? {
return Signal { [weak self] subscriber in
guard let self else {
@ -3997,9 +4002,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
})
}
public func sendMessage(text: String, entities: [MessageTextEntity]) {
public func sendMessage(randomId: Int64? = nil, text: String, entities: [MessageTextEntity]) {
if let messagesContext = self.messagesContext {
messagesContext.send(fromId: self.joinAsPeerId, text: text, entities: entities)
messagesContext.send(fromId: self.joinAsPeerId, randomId: randomId, text: text, entities: entities)
}
}
}

View file

@ -36,6 +36,7 @@ final class VideoChatActionButtonComponent: Component {
case video
case rotateCamera
case message
case share
case leave
}
@ -43,6 +44,7 @@ final class VideoChatActionButtonComponent: Component {
case video(isActive: Bool)
case rotateCamera
case message
case share
case leave
fileprivate var iconType: IconType {
@ -64,6 +66,8 @@ final class VideoChatActionButtonComponent: Component {
return .rotateCamera
case .message:
return .message
case .share:
return .share
case .leave:
return .leave
}
@ -142,12 +146,11 @@ final class VideoChatActionButtonComponent: Component {
let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2)
let genericBackgroundColor = UIColor(rgb: 0x2d2f38, alpha: 0.6)
let genericBackgroundColor = UIColor(rgb: 0x25272e, alpha: 0.72)
let blueColor = UIColor(rgb: 0x21477d)
let titleText: String
let backgroundColor: UIColor
var tintColorKind: GlassBackgroundView.TintColor.Kind = .custom
let iconDiameter: CGFloat
var isEnabled: Bool = true
switch component.content {
@ -183,9 +186,6 @@ final class VideoChatActionButtonComponent: Component {
backgroundColor = genericBackgroundColor
case .muted:
backgroundColor = !isActive ? genericBackgroundColor : blueColor
if isActive {
tintColorKind = .custom
}
case .unmuted:
backgroundColor = !isActive ? genericBackgroundColor : UIColor(rgb: 0x34C659)
case .raiseHand, .scheduled:
@ -193,8 +193,7 @@ final class VideoChatActionButtonComponent: Component {
}
iconDiameter = 58.0
case .rotateCamera:
//TODO:localize
titleText = "rotate"
titleText = component.strings.VoiceChat_Rotate
switch component.microphoneState {
case .connecting:
backgroundColor = genericBackgroundColor
@ -207,14 +206,16 @@ final class VideoChatActionButtonComponent: Component {
}
iconDiameter = 44.0
case .message:
//TODO:localize
titleText = "message"
titleText = component.strings.VoiceChat_Message
backgroundColor = genericBackgroundColor
iconDiameter = 56.0
case .share:
titleText = component.strings.VoiceChat_ShareButton
backgroundColor = genericBackgroundColor
iconDiameter = 56.0
case .leave:
titleText = component.strings.VoiceChat_Leave
backgroundColor = UIColor(rgb: 0x4f1613)
tintColorKind = .custom
backgroundColor = UIColor(rgb: 0x330d0b)
iconDiameter = 22.0
}
@ -246,6 +247,8 @@ final class VideoChatActionButtonComponent: Component {
self.contentImage = UIImage(bundleImageName: "Call/CallSwitchCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate)
case .message:
self.contentImage = UIImage(bundleImageName: "Call/CallMessageButton")?.precomposed().withRenderingMode(.alwaysTemplate)
case .share:
self.contentImage = UIImage(bundleImageName: "Call/CallShareButton")?.precomposed().withRenderingMode(.alwaysTemplate)
case .leave:
self.contentImage = generateImage(CGSize(width: 28.0, height: 28.0), opaque: false, rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
@ -291,7 +294,7 @@ final class VideoChatActionButtonComponent: Component {
}
}
self.background.update(size: size, cornerRadius: size.width * 0.5, isDark: true, tintColor: .init(kind: tintColorKind, color: backgroundColor), transition: tintTransition)
self.background.update(size: size, cornerRadius: size.width * 0.5, isDark: true, tintColor: .init(kind: .custom, color: backgroundColor), transition: tintTransition)
transition.setFrame(view: self.background, frame: CGRect(origin: CGPoint(), size: size))
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) * 0.5), y: size.height + 5.0), size: titleSize)

View file

@ -0,0 +1,23 @@
import Foundation
import SwiftSignalKit
import TelegramCore
enum VideoChatNotificationIcon {
case peer(EnginePeer)
case icon(String)
}
extension VideoChatScreenComponent.View {
func displayNotification(icon: VideoChatNotificationIcon, text: String, duration: Int32) {
let id = Int64.random(in: 0 ..< .max)
let expiresOn = Int32(CFAbsoluteTimeGetCurrent()) + duration
self.messageNotifications.append((id: id, icon: icon, text: text, expiresOn: expiresOn))
self.state?.updated(transition: .spring(duration: 0.4))
Queue.mainQueue().after(Double(duration)) {
self.messageNotifications.removeAll(where: { $0.id == id })
self.state?.updated(transition: .spring(duration: 0.4))
}
}
}

View file

@ -158,6 +158,15 @@ extension VideoChatCall {
conferenceSource.setCurrentAudioOutput(output)
}
}
func setMessagesEnabled(isEnabled: Bool) {
switch self {
case let .group(group):
group.updateMessagesEnabled(isEnabled: isEnabled)
case .conferenceSource:
break
}
}
}
final class VideoChatScreenComponent: Component {
@ -239,7 +248,7 @@ final class VideoChatScreenComponent: Component {
let videoControlButton = ComponentView<Empty>()
let leaveButton = ComponentView<Empty>()
let microphoneButton = ComponentView<Empty>()
let messageButton = ComponentView<Empty>()
var messageButton: ComponentView<Empty>?
let speakerButton = ComponentView<Empty>()
@ -265,6 +274,7 @@ final class VideoChatScreenComponent: Component {
weak var disappearingReactionContextNode: ReactionContextNode?
weak var willDismissReactionContextNode: ReactionContextNode?
var messageNotifications: [(id: Int64, icon: VideoChatNotificationIcon, text: String, expiresOn: Int32)] = []
let messagesList = ComponentView<Empty>()
var messagesState: GroupCallMessagesContext.State?
var messagesStateDisposable: Disposable?
@ -495,6 +505,9 @@ final class VideoChatScreenComponent: Component {
if let messageListView = self.messagesList.view, view.isDescendant(of: messageListView) {
return false
}
if let inputPanelView = self.inputPanel.view, view.isDescendant(of: inputPanelView) {
return false
}
}
return true
}
@ -1020,9 +1033,13 @@ final class VideoChatScreenComponent: Component {
}
private func onMessagePressed() {
self.inputPanelIsActive = true
self.state?.updated()
self.activateInput()
if let callState = self.callState, callState.messagesAreEnabled {
self.inputPanelIsActive = true
self.state?.updated()
self.activateInput()
} else if let inviteLinks = self.inviteLinks {
self.presentShare(inviteLinks)
}
}
private func onLeavePressed() {
@ -1371,7 +1388,7 @@ final class VideoChatScreenComponent: Component {
})
}
private func sendInput() {
private func sendInput(randomId: Int64? = nil) {
guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else {
return
}
@ -1385,11 +1402,11 @@ final class VideoChatScreenComponent: Component {
return
}
let entities = generateTextEntities(text.string, enabledTypes: [.mention, .hashtag, .allUrl], currentEntities: generateChatInputTextEntities(text))
call.sendMessage(text: text.string, entities: entities)
call.sendMessage(randomId: randomId, text: text.string, entities: entities)
}
inputPanelView.clearSendMessageInput(updateState: true)
// self.currentInputMode = .text
// if hasFirstResponder(self) {
// self.endEditing(true)
@ -1824,7 +1841,8 @@ final class VideoChatScreenComponent: Component {
} else {
text = environment.strings.VoiceChat_DisplayAsSuccess(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
}
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
self.displayNotification(icon: .peer(peer), text: text, duration: 3)
//self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
})
self.memberEventsDisposable?.dispose()
@ -1849,8 +1867,10 @@ final class VideoChatScreenComponent: Component {
}
if displayEvent {
let text = environment.strings.VoiceChat_PeerJoinedText(event.peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
let text = environment.strings.VoiceChat_PeerJoinedText("**\(event.peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder))**").string
self.displayNotification(icon: .peer(event.peer), text: text, duration: 3)
//self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: event.peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
}
}
} else {
@ -2294,10 +2314,7 @@ final class VideoChatScreenComponent: Component {
alphaTransition.setAlpha(view: navigationLeftButtonView, alpha: self.isAnimatedOutFromPrivateCall ? 0.0 : 1.0)
}
var navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - navigationButtonAreaWidth + floor((navigationButtonAreaWidth - navigationRightButtonSize.width) * 0.5) + navigationButtonInset, y: topInset + floor((navigationBarHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize)
if buttonsOnTheSide {
navigationRightButtonFrame.origin.x += 42.0
}
let navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - navigationButtonAreaWidth + floor((navigationButtonAreaWidth - navigationRightButtonSize.width) * 0.5) + navigationButtonInset, y: topInset + floor((navigationBarHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize)
if let navigationRightButtonView = self.navigationRightButton.view {
if navigationRightButtonView.superview == nil {
self.containerView.addSubview(navigationRightButtonView)
@ -2368,16 +2385,15 @@ final class VideoChatScreenComponent: Component {
var idleTitleStatusText: [AnimatedTextComponent.Item] = []
if let callState = self.callState {
if callState.networkState == .connected, let members = self.members {
//TODO:localize
let totalCount = max(1, members.totalCount)
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: false, content: .number(totalCount, minDigits: 0)))
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(1), isUnbreakable: false, content: .text(totalCount == 1 ? " participant" : " participants")))
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(1), isUnbreakable: false, content: .text(environment.strings.VoiceChat_SubtitleParticipants(Int32(totalCount)))))
if let lastTitleEvent = self.lastTitleEvent {
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(6), isUnbreakable: false, content: .text(", \(lastTitleEvent)")))
} else if !self.invitedPeers.isEmpty {
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(3), isUnbreakable: true, content: .text(", ")))
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(4), isUnbreakable: false, content: .number(self.invitedPeers.count, minDigits: 0)))
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(5), isUnbreakable: false, content: .text(" invited")))
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(5), isUnbreakable: false, content: .text(environment.strings.VoiceChat_SubtitleInvited(Int32(self.invitedPeers.count)))))
}
} else if callState.scheduleTimestamp != nil {
idleTitleStatusText.append(AnimatedTextComponent.Item(id: AnyHashable(0), isUnbreakable: false, content: .text(environment.strings.VoiceChat_Scheduled)))
@ -2557,6 +2573,7 @@ final class VideoChatScreenComponent: Component {
let videoButtonContent: VideoChatActionButtonComponent.Content?
let videoControlButtonContent: VideoChatActionButtonComponent.Content
let messageButtonContent: VideoChatActionButtonComponent.Content?
var buttonAudio: VideoChatActionButtonComponent.Content.Audio = .speaker
var buttonIsEnabled = false
@ -2599,12 +2616,27 @@ final class VideoChatScreenComponent: Component {
}
}
if let callState = self.callState, !callState.messagesAreEnabled {
if let _ = self.inviteLinks {
messageButtonContent = .share
} else {
messageButtonContent = nil
}
} else {
messageButtonContent = .message
}
let actionButtonDiameter: CGFloat = 56.0
let expandedMicrophoneButtonDiameter: CGFloat = actionButtonDiameter
let collapsedMicrophoneButtonDiameter: CGFloat = actionButtonDiameter // 116.0
let buttonsCount = videoButtonContent == nil ? 4 : 5
var buttonsCount = 3
if let _ = videoButtonContent {
buttonsCount += 1
}
if let _ = messageButtonContent {
buttonsCount += 1
}
let buttonsWidth: CGFloat = actionButtonDiameter * CGFloat(buttonsCount)
let remainingButtonsSpace: CGFloat
@ -2688,21 +2720,42 @@ final class VideoChatScreenComponent: Component {
var thirdActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: actionButtonSize)
if buttonsCount == 4 {
if buttonsOnTheSide {
firstActionButtonFrame.origin.x = microphoneButtonFrame.minX
secondActionButtonFrame.origin.x = microphoneButtonFrame.minX
thirdActionButtonFrame.origin.x = microphoneButtonFrame.minX
fourthActionButtonFrame.origin.x = microphoneButtonFrame.minX
microphoneButtonFrame.origin.y = availableSize.height * 0.5 - landscapeControlsSpacing * 0.5 - actionButtonDiameter
firstActionButtonFrame.origin.y = microphoneButtonFrame.minY - landscapeControlsSpacing - actionButtonDiameter
thirdActionButtonFrame.origin.y = microphoneButtonFrame.maxY + landscapeControlsSpacing
fourthActionButtonFrame.origin.y = microphoneButtonFrame.maxY + landscapeControlsSpacing + actionButtonDiameter + landscapeControlsSpacing
} else {
microphoneButtonFrame.origin.x = availableSize.width * 0.5 - actionMicrophoneButtonSpacing * 0.5 - actionButtonDiameter
firstActionButtonFrame.origin.x = microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter
thirdActionButtonFrame.origin.x = microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing
fourthActionButtonFrame.origin.x = microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing + actionButtonDiameter + actionMicrophoneButtonSpacing
if let _ = messageButtonContent {
if buttonsOnTheSide {
firstActionButtonFrame.origin.x = microphoneButtonFrame.minX
secondActionButtonFrame.origin.x = microphoneButtonFrame.minX
thirdActionButtonFrame.origin.x = microphoneButtonFrame.minX
fourthActionButtonFrame.origin.x = microphoneButtonFrame.minX
microphoneButtonFrame.origin.y = availableSize.height * 0.5 - landscapeControlsSpacing * 0.5 - actionButtonDiameter
firstActionButtonFrame.origin.y = microphoneButtonFrame.minY - landscapeControlsSpacing - actionButtonDiameter
thirdActionButtonFrame.origin.y = microphoneButtonFrame.maxY + landscapeControlsSpacing
fourthActionButtonFrame.origin.y = microphoneButtonFrame.maxY + landscapeControlsSpacing + actionButtonDiameter + landscapeControlsSpacing
} else {
microphoneButtonFrame.origin.x = availableSize.width * 0.5 - actionMicrophoneButtonSpacing * 0.5 - actionButtonDiameter
firstActionButtonFrame.origin.x = microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter
thirdActionButtonFrame.origin.x = microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing
fourthActionButtonFrame.origin.x = microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing + actionButtonDiameter + actionMicrophoneButtonSpacing
}
}
if let _ = videoButtonContent {
if buttonsOnTheSide {
firstActionButtonFrame.origin.x = microphoneButtonFrame.minX
secondActionButtonFrame.origin.x = microphoneButtonFrame.minX
thirdActionButtonFrame.origin.x = microphoneButtonFrame.minX
fourthActionButtonFrame.origin.x = microphoneButtonFrame.minX
microphoneButtonFrame.origin.y = availableSize.height * 0.5 - landscapeControlsSpacing * 0.5 - actionButtonDiameter
firstActionButtonFrame.origin.y = microphoneButtonFrame.minY - landscapeControlsSpacing - actionButtonDiameter
thirdActionButtonFrame.origin.y = microphoneButtonFrame.maxY + landscapeControlsSpacing
fourthActionButtonFrame.origin.y = microphoneButtonFrame.maxY + landscapeControlsSpacing + actionButtonDiameter + landscapeControlsSpacing
} else {
microphoneButtonFrame.origin.x = availableSize.width * 0.5 + actionMicrophoneButtonSpacing * 0.5
firstActionButtonFrame.origin.x = microphoneButtonFrame.minX - actionMicrophoneButtonSpacing * 2.0 - actionButtonDiameter * 2.0
secondActionButtonFrame.origin.x = microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter
//thirdActionButtonFrame.origin.x = microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing
fourthActionButtonFrame.origin.x = microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing
}
}
} else if buttonsOnTheSide {
firstActionButtonFrame.origin.x = microphoneButtonFrame.minX
@ -3127,7 +3180,6 @@ final class VideoChatScreenComponent: Component {
transition.setPosition(view: microphoneButtonView, position: microphoneButtonFrame.center)
transition.setBounds(view: microphoneButtonView, bounds: CGRect(origin: CGPoint(), size: microphoneButtonFrame.size))
}
let _ = self.speakerButton.update(
transition: transition,
@ -3202,8 +3254,12 @@ final class VideoChatScreenComponent: Component {
)
if let videoButtonView = videoButton.view {
if videoButtonView.superview == nil {
if let speakerButtonView = self.speakerButton.view {
self.containerView.insertSubview(videoButtonView, belowSubview: speakerButtonView)
if let microphoneButtonView = self.microphoneButton.view {
self.containerView.insertSubview(videoButtonView, aboveSubview: microphoneButtonView)
}
if !transition.animation.isImmediate {
transition.animateScale(view: videoButtonView, from: 0.01, to: 1.0)
transition.animateAlpha(view: videoButtonView, from: 0.0, to: 1.0)
}
}
videoButtonTransition.setPosition(view: videoButtonView, position: secondActionButtonFrame.center)
@ -3213,40 +3269,64 @@ final class VideoChatScreenComponent: Component {
self.videoButton = nil
if let videoButtonView = videoButton.view {
let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))
transition.animateScale(view: videoButtonView, from: 1.0, to: 0.01)
transition.animateAlpha(view: videoButtonView, from: 1.0, to: 0.0, completion: { _ in
transition.setScale(view: videoButtonView, scale: 0.01)
transition.setAlpha(view: videoButtonView, alpha: 0.0, completion: { _ in
videoButtonView.removeFromSuperview()
})
}
}
let _ = self.messageButton.update(
transition: transition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(VideoChatActionButtonComponent(
strings: environment.strings,
content: .message,
microphoneState: actionButtonMicrophoneState,
isCollapsed: areButtonsActuallyCollapsed || buttonsOnTheSide
)),
effectAlignment: .center,
action: { [weak self] in
guard let self else {
return
}
self.onMessagePressed()
},
animateAlpha: false
)),
environment: {},
containerSize: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)
)
if let messageButtonView = self.messageButton.view {
if messageButtonView.superview == nil {
self.containerView.addSubview(messageButtonView)
if let messageButtonContent {
var messageButtonTransition = transition
let messageButton: ComponentView<Empty>
if let current = self.messageButton {
messageButton = current
} else {
messageButtonTransition = .immediate
messageButton = ComponentView<Empty>()
self.messageButton = messageButton
}
let _ = messageButton.update(
transition: messageButtonTransition,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(VideoChatActionButtonComponent(
strings: environment.strings,
content: messageButtonContent,
microphoneState: actionButtonMicrophoneState,
isCollapsed: areButtonsActuallyCollapsed || buttonsOnTheSide
)),
effectAlignment: .center,
action: { [weak self] in
self?.onMessagePressed()
},
animateAlpha: false
)),
environment: {},
containerSize: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)
)
if let messageButtonView = messageButton.view {
if messageButtonView.superview == nil {
if let microphoneButtonView = self.microphoneButton.view {
self.containerView.insertSubview(messageButtonView, aboveSubview: microphoneButtonView)
}
if !transition.animation.isImmediate {
transition.animateScale(view: messageButtonView, from: 0.01, to: 1.0)
transition.animateAlpha(view: messageButtonView, from: 0.0, to: 1.0)
}
}
messageButtonTransition.setPosition(view: messageButtonView, position: thirdActionButtonFrame.center)
messageButtonTransition.setBounds(view: messageButtonView, bounds: CGRect(origin: CGPoint(), size: thirdActionButtonFrame.size))
}
} else if let messageButton = self.messageButton, messageButton.view?.superview != nil {
self.messageButton = nil
if let messageButtonView = messageButton.view {
let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))
transition.setScale(view: messageButtonView, scale: 0.01)
transition.setAlpha(view: messageButtonView, alpha: 0.0, completion: { _ in
messageButtonView.removeFromSuperview()
})
}
transition.setPosition(view: messageButtonView, position: thirdActionButtonFrame.center)
transition.setBounds(view: messageButtonView, bounds: CGRect(origin: CGPoint(), size: thirdActionButtonFrame.size))
}
let _ = self.leaveButton.update(
@ -3399,7 +3479,6 @@ final class VideoChatScreenComponent: Component {
characterLimit = Int(value)
}
//TODO:localize
self.inputPanel.parentState = state
inputPanelSize = self.inputPanel.update(
transition: transition,
@ -3409,7 +3488,7 @@ final class VideoChatScreenComponent: Component {
theme: environment.theme,
strings: environment.strings,
style: .glass,
placeholder: .plain("Message"),
placeholder: .plain(environment.strings.VoiceChat_MessagePlaceholder),
sendPaidMessageStars: nil,
maxLength: characterLimit,
queryTypes: [.mention, .hashtag],
@ -3419,10 +3498,8 @@ final class VideoChatScreenComponent: Component {
nextInputMode: { _ in return nextInputMode },
areVoiceMessagesAvailable: false,
presentController: { c in
//controller.present(c, in: .window(.root))
},
presentInGlobalOverlay: { c in
//controller.presentInGlobalOverlay(c)
},
sendMessageAction: { [weak self] transition in
guard let self else {
@ -3431,7 +3508,7 @@ final class VideoChatScreenComponent: Component {
if self.inputPanelExternalState.hasText {
self.nextSendMessageTransition = transition
self.sendInput()
self.sendInput(randomId: transition?.randomId)
} else {
self.inputPanelIsActive = false
self.deactivateInput()
@ -3783,7 +3860,10 @@ final class VideoChatScreenComponent: Component {
if let next = self.nextSendMessageTransition {
self.nextSendMessageTransition = nil
sendActionTransition = MessageListComponent.SendActionTransition(
textSnapshotView: next.textSnapshotView, globalFrame: next.globalFrame, cornerRadius: next.cornerRadius
randomId: next.randomId,
textSnapshotView: next.textSnapshotView,
globalFrame: next.globalFrame,
cornerRadius: next.cornerRadius
)
}
@ -3796,13 +3876,34 @@ final class VideoChatScreenComponent: Component {
messageItems.append(
MessageListComponent.Item(
id: message.id,
peer: author,
icon: .peer(author),
isNotification: false,
text: message.text,
entities: message.entities
)
)
}
}
for notification in self.messageNotifications {
let icon: MessageItemComponent.Icon
switch notification.icon {
case let .peer(peer):
icon = .peer(peer)
case let .icon(name):
icon = .icon(name)
}
messageItems.insert(
MessageListComponent.Item(
id: notification.id,
icon: icon,
isNotification: true,
text: notification.text,
entities: []
),
at: 0
)
}
let normalMessagesBottomInset: CGFloat = buttonsOnTheSide ? 16.0 : availableSize.height - microphoneButtonFrame.minY + 16.0
let messagesBottomInset: CGFloat = max(inputPanelBottomInset + inputPanelSize.height + 31.0, normalMessagesBottomInset) + reactionsInset

View file

@ -227,6 +227,29 @@ extension VideoChatScreenComponent.View {
}
self.openTitleEditing()
})))
items.append(.action(ContextMenuActionItem(text: callState.messagesAreEnabled ? environment.strings.VoiceChat_ContextDisableMessages : environment.strings.VoiceChat_ContextEnableMessages, icon: { theme -> UIImage? in
return generateTintedImage(image: UIImage(bundleImageName: callState.messagesAreEnabled ? "Call/MessagesDisable" : "Call/MessagesEnable"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
f(.default)
guard let self, let currentCall = self.currentCall else {
return
}
let isEnabled = !callState.messagesAreEnabled
currentCall.setMessagesEnabled(isEnabled: isEnabled)
let iconName: String
let text: String
if isEnabled {
iconName = "Call/ToastMessagesEnabled"
text = environment.strings.VoiceChat_ToastMessagesEnabled
} else {
iconName = "Call/ToastMessagesDisabled"
text = environment.strings.VoiceChat_ToastMessagesDisabled
}
self.displayNotification(icon: .icon(iconName), text: text, duration: 3)
})))
var hasPermissions = true
if let peer = self.peer, case let .channel(chatPeer) = peer {

View file

@ -4909,9 +4909,12 @@ func replayFinalState(
let canChange = (flags & (1 << 2)) != 0
let isVideoEnabled = (flags & (1 << 9)) != 0
let defaultParticipantsAreMuted = GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: isMuted, canChange: canChange)
let messagesEnabled = (flags & (1 << 17)) != 0
let canChangeMessagesEnabled = (flags & (1 << 18)) != 0
let messagesAreEnabled = GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: messagesEnabled, canChange: canChangeMessagesEnabled)
updatedGroupCallParticipants.append((
info.id,
.call(isTerminated: false, defaultParticipantsAreMuted: defaultParticipantsAreMuted, title: title, recordingStartTimestamp: recordStartDate, scheduleTimestamp: scheduleDate, isVideoEnabled: isVideoEnabled, participantCount: Int(participantsCount))
.call(isTerminated: false, defaultParticipantsAreMuted: defaultParticipantsAreMuted, messagesAreEnabled: messagesAreEnabled, title: title, recordingStartTimestamp: recordStartDate, scheduleTimestamp: scheduleDate, isVideoEnabled: isVideoEnabled, participantCount: Int(participantsCount))
))
default:
break
@ -4920,7 +4923,7 @@ func replayFinalState(
case let .groupCallDiscarded(callId, _, _):
updatedGroupCallParticipants.append((
callId,
.call(isTerminated: true, defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false), title: nil, recordingStartTimestamp: nil, scheduleTimestamp: nil, isVideoEnabled: false, participantCount: nil)
.call(isTerminated: true, defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false), messagesAreEnabled: GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: false, canChange: false), title: nil, recordingStartTimestamp: nil, scheduleTimestamp: nil, isVideoEnabled: false, participantCount: nil)
))
if let peerId {

View file

@ -766,7 +766,10 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId?, joinAs: PeerId?,
let isMuted = (flags & (1 << 1)) != 0
let canChange = (flags & (1 << 2)) != 0
let isVideoEnabled = (flags & (1 << 9)) != 0
let messagesEnabled = (flags & (1 << 17)) != 0
let canChangeMessagesEnabled = (flags & (1 << 18)) != 0
state.defaultParticipantsAreMuted = GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: isMuted, canChange: canChange)
state.messagesAreEnabled = GroupCallParticipantsContext.State.MessagesAreEnabled(isEnabled: messagesEnabled, canChange: canChangeMessagesEnabled)
state.title = title
state.recordingStartTimestamp = recordStartDate
state.scheduleTimestamp = scheduleDate
@ -1583,7 +1586,7 @@ public final class GroupCallParticipantsContext {
}
case state(update: StateUpdate)
case call(isTerminated: Bool, defaultParticipantsAreMuted: State.DefaultParticipantsAreMuted, title: String?, recordingStartTimestamp: Int32?, scheduleTimestamp: Int32?, isVideoEnabled: Bool, participantCount: Int?)
case call(isTerminated: Bool, defaultParticipantsAreMuted: State.DefaultParticipantsAreMuted, messagesAreEnabled: State.MessagesAreEnabled, title: String?, recordingStartTimestamp: Int32?, scheduleTimestamp: Int32?, isVideoEnabled: Bool, participantCount: Int?)
case conferenceChainBlocks(subChainId: Int, blocks: [Data], nextOffset: Int)
}
@ -1961,9 +1964,10 @@ public final class GroupCallParticipantsContext {
for update in updates {
if case let .state(update) = update {
stateUpdates.append(update)
} else if case let .call(_, defaultParticipantsAreMuted, title, recordingStartTimestamp, scheduleTimestamp, isVideoEnabled, participantsCount) = update {
} else if case let .call(_, defaultParticipantsAreMuted, messagesAreEnabled, title, recordingStartTimestamp, scheduleTimestamp, isVideoEnabled, participantsCount) = update {
var state = self.stateValue.state
state.defaultParticipantsAreMuted = defaultParticipantsAreMuted
state.messagesAreEnabled = messagesAreEnabled
state.recordingStartTimestamp = recordingStartTimestamp
state.title = title
state.scheduleTimestamp = scheduleTimestamp
@ -2336,6 +2340,7 @@ public final class GroupCallParticipantsContext {
state.adminIds = strongSelf.stateValue.state.adminIds
state.isCreator = strongSelf.stateValue.state.isCreator
state.defaultParticipantsAreMuted = strongSelf.stateValue.state.defaultParticipantsAreMuted
state.messagesAreEnabled = strongSelf.stateValue.state.messagesAreEnabled
state.title = strongSelf.stateValue.state.title
state.recordingStartTimestamp = strongSelf.stateValue.state.recordingStartTimestamp
state.scheduleTimestamp = strongSelf.stateValue.state.scheduleTimestamp
@ -2583,12 +2588,12 @@ public final class GroupCallParticipantsContext {
}
public func updateMessagesEnabled(isEnabled: Bool) {
if isEnabled == self.stateValue.state.messagesAreEnabled.isEnabled {
return
}
// if isEnabled == self.stateValue.state.messagesAreEnabled.isEnabled {
// return
// }
self.stateValue.state.messagesAreEnabled.isEnabled = isEnabled
self.updateDefaultMuteDisposable.set((self.account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 2, call: self.reference.apiInputGroupCall, joinMuted: nil, messagesEnabled: isEnabled ? .boolTrue : .boolFalse))
self.updateMessagesEnabledDisposable.set((self.account.network.request(Api.functions.phone.toggleGroupCallSettings(flags: 1 << 2, call: self.reference.apiInputGroupCall, joinMuted: nil, messagesEnabled: isEnabled ? .boolTrue : .boolFalse))
|> deliverOnMainQueue).start(next: { [weak self] updates in
guard let strongSelf = self else {
return
@ -3636,7 +3641,7 @@ public final class GroupCallMessagesContext {
}
}
func send(fromId: EnginePeer.Id, text: String, entities: [MessageTextEntity]) {
func send(fromId: EnginePeer.Id, randomId: Int64?, text: String, entities: [MessageTextEntity]) {
let _ = (self.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(fromId)
}
@ -3712,9 +3717,9 @@ public final class GroupCallMessagesContext {
})
}
public func send(fromId: EnginePeer.Id, text: String, entities: [MessageTextEntity]) {
public func send(fromId: EnginePeer.Id, randomId: Int64?, text: String, entities: [MessageTextEntity]) {
self.impl.with { impl in
impl.send(fromId: fromId, text: text, entities: entities)
impl.send(fromId: fromId, randomId: randomId, text: text, entities: entities)
}
}
}

View file

@ -1260,9 +1260,13 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes)
}
}
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, _):
case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned):
if case let .unique(gift) = gift {
if !forAdditionalServiceMessage && !"".isEmpty {
if assigned {
let attributes: [Int: MarkdownAttributeSet] = [0: boldAttributes]
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))"
attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Assigned(giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes)
} else if !forAdditionalServiceMessage && !"".isEmpty {
attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor)
} else if let messagePeer = message.peers[message.id.peerId] {
var peerName = EnginePeer(messagePeer).compactDisplayTitle

View file

@ -462,12 +462,11 @@ public final class ButtonComponent: Component {
if let self, let component = self.component, component.isEnabled {
switch component.background.style {
case .glass:
let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))
let transition = ComponentTransition(animation: .curve(duration: highlighted ? 0.25 : 0.35, curve: .spring))
if highlighted {
let highlightedColor = component.background.color.withMultiplied(hue: 1.0, saturation: 0.77, brightness: 1.01)
transition.setBackgroundColor(view: self.containerView, color: highlightedColor)
transition.setScale(view: self.containerView, scale: 1.05)
} else {
transition.setBackgroundColor(view: self.containerView, color: component.background.color)
transition.setScale(view: self.containerView, scale: 1.0)

View file

@ -529,14 +529,13 @@ public final class MessageInputActionButtonComponent: Component {
self.backgroundView = backgroundView
}
var tintColor = UIColor(rgb: 0x4d4f5c, alpha: 0.6)
var tintKind: GlassBackgroundView.TintColor.Kind = .panel
var tintColor = UIColor(rgb: 0x25272e, alpha: 0.72)
let tintKind: GlassBackgroundView.TintColor.Kind = .custom
if case .send = component.mode {
tintColor = UIColor(rgb: 0x029dff)
tintKind = .custom
}
let buttonSize = CGSize(width: 40.0, height: 40.0)
backgroundView.update(size: buttonSize, cornerRadius: buttonSize.height / 2.0, isDark: false, tintColor: .init(kind: tintKind, color: tintColor), transition: transition)
backgroundView.update(size: buttonSize, cornerRadius: buttonSize.height / 2.0, isDark: true, tintColor: .init(kind: tintKind, color: tintColor), transition: transition)
backgroundView.frame = CGRect(origin: .zero, size: buttonSize)
}

View file

@ -157,11 +157,13 @@ public final class MessageInputPanelComponent: Component {
}
public final class SendActionTransition {
public let randomId: Int64
public let textSnapshotView: UIView
public let globalFrame: CGRect
public let cornerRadius: CGFloat
init(textSnapshotView: UIView, globalFrame: CGRect, cornerRadius: CGFloat) {
init(randomId: Int64, textSnapshotView: UIView, globalFrame: CGRect, cornerRadius: CGFloat) {
self.randomId = randomId
self.textSnapshotView = textSnapshotView
self.globalFrame = globalFrame
self.cornerRadius = cornerRadius
@ -785,6 +787,7 @@ public final class MessageInputPanelComponent: Component {
var sendActionTransition: MessageInputPanelComponent.SendActionTransition?
if let snapshotView = self.textClippingView.snapshotView(afterScreenUpdates: false), let backgroundView = self.fieldGlassBackgroundView {
sendActionTransition = MessageInputPanelComponent.SendActionTransition(
randomId: Int64.random(in: .min ..< .max),
textSnapshotView: snapshotView,
globalFrame: backgroundView.convert(backgroundView.bounds, to: nil),
cornerRadius: baseFieldHeight * 0.5
@ -1101,7 +1104,7 @@ public final class MessageInputPanelComponent: Component {
self.fieldBackgroundTint.isHidden = true
}
if let fieldGlassBackgroundView = self.fieldGlassBackgroundView {
fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: true, tintColor: .init(kind: .panel, color: UIColor(rgb: 0x4d4f5c, alpha: 0.6)), transition: transition)
fieldGlassBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, isDark: true, tintColor: .init(kind: .custom, color: UIColor(rgb: 0x25272e, alpha: 0.72)), transition: transition)
transition.setFrame(view: fieldGlassBackgroundView, frame: fieldBackgroundFrame)
}
default:
@ -1348,7 +1351,10 @@ public final class MessageInputPanelComponent: Component {
environment: {},
containerSize: availableTextFieldSize
)
let counterFrame = CGRect(origin: CGPoint(x: availableSize.width - insets.right + floorToScreenPixels((insets.right - counterSize.width) * 0.5), y: size.height - insets.bottom - baseFieldHeight - counterSize.height - 5.0), size: counterSize)
var counterFrame = CGRect(origin: CGPoint(x: availableSize.width - insets.right + floorToScreenPixels((insets.right - counterSize.width) * 0.5), y: size.height - insets.bottom - baseFieldHeight - counterSize.height - 5.0), size: counterSize)
if case .glass = component.style {
counterFrame.origin.x -= 7.0
}
if let counterView = self.counter.view {
if counterView.superview == nil {
self.addSubview(counterView)

View file

@ -6680,7 +6680,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
if !headerButtons.contains(.discussion) && hasDiscussion {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "View Discussion", icon: { theme in
items.append(.action(ContextMenuActionItem(text: presentationData.strings.PeerInfo_ViewDiscussion, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.dismissWithoutContent)

View file

@ -480,14 +480,13 @@ private final class ProfileLevelInfoScreenComponent: Component {
levelFraction = max(0.0, levelFraction)
//TODO:localize
let levelInfoSize = self.levelInfo.update(
transition: isChangingPreview ? ComponentTransition.immediate.withUserData(ProfileLevelRatingBarComponent.TransitionHint(animate: true)) : .immediate,
component: AnyComponent(ProfileLevelRatingBarComponent(
theme: environment.theme,
value: levelFraction,
leftLabel: currentLevel < 0 ? "" : environment.strings.ProfileLevelInfo_LevelIndex(Int32(currentLevel)),
rightLabel: currentLevel < 0 ? "Negative rating" : nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "",
rightLabel: currentLevel < 0 ? environment.strings.ProfileLevelInfo_NegativeRating : nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "",
badgeValue: badgeText,
badgeTotal: badgeTextSuffix,
level: Int(currentLevel),

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "disablemessages_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "enablemessages_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "toastmessagesdisabled_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "toastmessagesenabled_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}