Various improvements

This commit is contained in:
Ilya Laktyushin 2026-02-23 21:25:25 +04:00
parent 4203d70414
commit 5d50aa34af
19 changed files with 2010 additions and 1905 deletions

View file

@ -15828,3 +15828,7 @@ Error: %8$@";
"Channel.AdminLog.EditRankOwn" = "Edit Member Tag";
"Channel.AdminLog.EditRank" = "Edit Member Tags";
"Channel.AdminLog.MessageRankNameNew" = "changed member tag for %1$@:\n%2$@";
"Channel.AdminLog.MessageRankUsernameNew" = "changed member tag for %1$@ (%2$@):\n%3$@";
"Channel.AdminLog.MessageRankNew" = "changed member tag:\n%1$@";

View file

@ -1528,7 +1528,7 @@ public protocol SharedAccountContext: AnyObject {
func makeLoginEmailSetupController(context: AccountContext, blocking: Bool, emailPattern: String?, canAutoDismissIfNeeded: Bool, navigationController: NavigationController?, completion: @escaping () -> Void, dismiss: @escaping () -> Void) -> ViewController
func makePasskeySetupController(context: AccountContext, displaySkip: Bool, navigationController: NavigationController?, completion: @escaping () -> Void, dismiss: @escaping () -> Void) -> ViewController
func makeChatCustomRankSetupScreen(context: AccountContext, peerId: EnginePeer.Id, participantId: EnginePeer.Id, rank: String?) -> ViewController
func makeChatCustomRankSetupScreen(context: AccountContext, peerId: EnginePeer.Id, participantId: EnginePeer.Id, rank: String?, role: ChatRankInfoScreenRole) -> ViewController
func makePeerCopyProtectionInfoScreen(context: AccountContext, completion: @escaping () -> Void) -> ViewController

View file

@ -5263,7 +5263,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let context = self.context
let controller = UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: "You joined the group", cancel: "Add Tag", destructive: false), elevatedLayout: true, action: { [weak self] action in
if let self, case .undo = action {
let tagController = context.sharedContext.makeChatCustomRankSetupScreen(context: context, peerId: peerId, participantId: context.account.peerId, rank: nil)
let tagController = context.sharedContext.makeChatCustomRankSetupScreen(context: context, peerId: peerId, participantId: context.account.peerId, rank: nil, role: .member)
self.push(tagController)
}
return true

View file

@ -152,6 +152,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1515256996] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionExportedInviteDelete($0) }
dict[-384910503] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionExportedInviteEdit($0) }
dict[1091179342] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionExportedInviteRevoke($0) }
dict[1476834540] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantEditRank($0) }
dict[-484690728] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantInvite($0) }
dict[405815507] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoin($0) }
dict[-23084712] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByInvite($0) }

View file

@ -787,6 +787,19 @@ public extension Api {
return ("channelAdminLogEventActionExportedInviteRevoke", [("invite", self.invite as Any)])
}
}
public class Cons_channelAdminLogEventActionParticipantEditRank: TypeConstructorDescription {
public var userId: Int64
public var prevRank: String
public var newRank: String
public init(userId: Int64, prevRank: String, newRank: String) {
self.userId = userId
self.prevRank = prevRank
self.newRank = newRank
}
public func descriptionFields() -> (String, [(String, Any)]) {
return ("channelAdminLogEventActionParticipantEditRank", [("userId", self.userId as Any), ("prevRank", self.prevRank as Any), ("newRank", self.newRank as Any)])
}
}
public class Cons_channelAdminLogEventActionParticipantInvite: TypeConstructorDescription {
public var participant: Api.ChannelParticipant
public init(participant: Api.ChannelParticipant) {
@ -1044,6 +1057,7 @@ public extension Api {
case channelAdminLogEventActionExportedInviteDelete(Cons_channelAdminLogEventActionExportedInviteDelete)
case channelAdminLogEventActionExportedInviteEdit(Cons_channelAdminLogEventActionExportedInviteEdit)
case channelAdminLogEventActionExportedInviteRevoke(Cons_channelAdminLogEventActionExportedInviteRevoke)
case channelAdminLogEventActionParticipantEditRank(Cons_channelAdminLogEventActionParticipantEditRank)
case channelAdminLogEventActionParticipantInvite(Cons_channelAdminLogEventActionParticipantInvite)
case channelAdminLogEventActionParticipantJoin
case channelAdminLogEventActionParticipantJoinByInvite(Cons_channelAdminLogEventActionParticipantJoinByInvite)
@ -1250,6 +1264,14 @@ public extension Api {
}
_data.invite.serialize(buffer, true)
break
case .channelAdminLogEventActionParticipantEditRank(let _data):
if boxed {
buffer.appendInt32(1476834540)
}
serializeInt64(_data.userId, buffer: buffer, boxed: false)
serializeString(_data.prevRank, buffer: buffer, boxed: false)
serializeString(_data.newRank, buffer: buffer, boxed: false)
break
case .channelAdminLogEventActionParticipantInvite(let _data):
if boxed {
buffer.appendInt32(-484690728)
@ -1471,6 +1493,8 @@ public extension Api {
return ("channelAdminLogEventActionExportedInviteEdit", [("prevInvite", _data.prevInvite as Any), ("newInvite", _data.newInvite as Any)])
case .channelAdminLogEventActionExportedInviteRevoke(let _data):
return ("channelAdminLogEventActionExportedInviteRevoke", [("invite", _data.invite as Any)])
case .channelAdminLogEventActionParticipantEditRank(let _data):
return ("channelAdminLogEventActionParticipantEditRank", [("userId", _data.userId as Any), ("prevRank", _data.prevRank as Any), ("newRank", _data.newRank as Any)])
case .channelAdminLogEventActionParticipantInvite(let _data):
return ("channelAdminLogEventActionParticipantInvite", [("participant", _data.participant as Any)])
case .channelAdminLogEventActionParticipantJoin:
@ -1926,6 +1950,23 @@ public extension Api {
return nil
}
}
public static func parse_channelAdminLogEventActionParticipantEditRank(_ reader: BufferReader) -> ChannelAdminLogEventAction? {
var _1: Int64?
_1 = reader.readInt64()
var _2: String?
_2 = parseString(reader)
var _3: String?
_3 = parseString(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantEditRank(Cons_channelAdminLogEventActionParticipantEditRank(userId: _1!, prevRank: _2!, newRank: _3!))
}
else {
return nil
}
}
public static func parse_channelAdminLogEventActionParticipantInvite(_ reader: BufferReader) -> ChannelAdminLogEventAction? {
var _1: Api.ChannelParticipant?
if let signature = reader.readInt32() {

View file

@ -3157,14 +3157,17 @@ public extension Api.functions.channels {
}
}
public extension Api.functions.channels {
static func editAdmin(channel: Api.InputChannel, userId: Api.InputUser, adminRights: Api.ChatAdminRights, rank: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
static func editAdmin(flags: Int32, channel: Api.InputChannel, userId: Api.InputUser, adminRights: Api.ChatAdminRights, rank: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(-751007486)
buffer.appendInt32(-1701270168)
serializeInt32(flags, buffer: buffer, boxed: false)
channel.serialize(buffer, true)
userId.serialize(buffer, true)
adminRights.serialize(buffer, true)
serializeString(rank, buffer: buffer, boxed: false)
return (FunctionDescription(name: "channels.editAdmin", parameters: [("channel", String(describing: channel)), ("userId", String(describing: userId)), ("adminRights", String(describing: adminRights)), ("rank", String(describing: rank))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
if Int(flags) & Int(1 << 0) != 0 {
serializeString(rank!, buffer: buffer, boxed: false)
}
return (FunctionDescription(name: "channels.editAdmin", parameters: [("flags", String(describing: flags)), ("channel", String(describing: channel)), ("userId", String(describing: userId)), ("adminRights", String(describing: adminRights)), ("rank", String(describing: rank))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {

View file

@ -180,6 +180,7 @@ public final class ChannelAdminEventLogContext {
clipIndex = i - 1
}
}
clipIndex = max(0, clipIndex)
if clipIndex < events.count {
events.removeSubrange(clipIndex ..< events.count)
}

View file

@ -95,6 +95,7 @@ public enum AdminLogEventAction {
case changeEmojiPack(prev: StickerPackReference?, new: StickerPackReference?)
case participantSubscriptionExtended(prev: RenderedChannelParticipant, new: RenderedChannelParticipant)
case toggleAutoTranslation(Bool)
case editRank(peerId: EnginePeer.Id, prev: String, new: String)
}
public enum ChannelAdminLogEventError {
@ -129,12 +130,13 @@ public struct AdminLogEventsFlags: OptionSet {
public static let invites = AdminLogEventsFlags(rawValue: 1 << 15)
public static let sendMessages = AdminLogEventsFlags(rawValue: 1 << 16)
public static let forums = AdminLogEventsFlags(rawValue: 1 << 17)
public static let ranks = AdminLogEventsFlags(rawValue: 1 << 19)
public static var all: AdminLogEventsFlags {
return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .sendMessages, .pinnedMessages, .editMessages, .deleteMessages, .calls, .invites, .forums]
return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .sendMessages, .pinnedMessages, .editMessages, .deleteMessages, .calls, .invites, .forums, .ranks]
}
public static var flags: AdminLogEventsFlags {
return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .sendMessages, .pinnedMessages, .editMessages, .deleteMessages, .calls, .invites, .forums]
return [.join, .leave, .invite, .ban, .unban, .kick, .unkick, .promote, .demote, .info, .settings, .sendMessages, .pinnedMessages, .editMessages, .deleteMessages, .calls, .invites, .forums, .ranks]
}
}
@ -483,6 +485,8 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net
case let .channelAdminLogEventActionToggleAutotranslation(channelAdminLogEventActionToggleAutotranslationData):
let newValue = channelAdminLogEventActionToggleAutotranslationData.newValue
action = .toggleAutoTranslation(boolFromApiValue(newValue))
case let .channelAdminLogEventActionParticipantEditRank(channelAdminLogEventActionParticipantEditRankData):
action = .editRank(peerId: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(channelAdminLogEventActionParticipantEditRankData.userId)), prev: channelAdminLogEventActionParticipantEditRankData.prevRank, new: channelAdminLogEventActionParticipantEditRankData.newRank)
}
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
if let action = action {

View file

@ -184,7 +184,12 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI
}
updatedParticipant = .member(id: adminId, invitedAt: Int32(Date().timeIntervalSince1970), adminInfo: adminInfo, banInfo: nil, rank: rank, subscriptionUntilDate: nil)
}
return account.network.request(Api.functions.channels.editAdmin(channel: inputChannel, userId: inputUser, adminRights: rights?.apiAdminRights ?? .chatAdminRights(Api.ChatAdminRights.Cons_chatAdminRights(flags: 0)), rank: rank ?? ""))
var flags: Int32 = 0
if let _ = rank {
flags = (1 << 0)
}
return account.network.request(Api.functions.channels.editAdmin(flags: flags, channel: inputChannel, userId: inputUser, adminRights: rights?.apiAdminRights ?? .chatAdminRights(Api.ChatAdminRights.Cons_chatAdminRights(flags: 0)), rank: rank ?? ""))
|> map { [$0] }
|> `catch` { error -> Signal<[Api.Updates], UpdateChannelAdminRightsError> in
if error.errorDescription == "USER_NOT_PARTICIPANT" {
@ -196,7 +201,7 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI
return .addMemberError(error)
}
|> then(
account.network.request(Api.functions.channels.editAdmin(channel: inputChannel, userId: inputUser, adminRights: rights?.apiAdminRights ?? .chatAdminRights(Api.ChatAdminRights.Cons_chatAdminRights(flags: 0)), rank: rank ?? ""))
account.network.request(Api.functions.channels.editAdmin(flags: flags, channel: inputChannel, userId: inputUser, adminRights: rights?.apiAdminRights ?? .chatAdminRights(Api.ChatAdminRights.Cons_chatAdminRights(flags: 0)), rank: rank ?? ""))
|> mapError { error -> UpdateChannelAdminRightsError in
return .generic
}

View file

@ -268,7 +268,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: "You joined the group", cancel: "Add Tag", destructive: false), elevatedLayout: true, action: { action in
if case .undo = action {
let tagController = context.sharedContext.makeChatCustomRankSetupScreen(context: context, peerId: peer.id, participantId: context.account.peerId, rank: nil)
let tagController = context.sharedContext.makeChatCustomRankSetupScreen(context: context, peerId: peer.id, participantId: context.account.peerId, rank: nil, role: .member)
self.interfaceInteraction?.getNavigationController()?.pushViewController(tagController)
}
return true

View file

@ -1054,7 +1054,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat
}
public func updatePeer(peer: Peer) {
if let previousPeer = self.peer, previousPeer.nameColor != peer.nameColor {
if let previousPeer = self.peer, previousPeer.nameColor != peer.nameColor || previousPeer.smallProfileImage != peer.smallProfileImage {
self.peer = peer
if peer.smallProfileImage != nil {
self.avatarNode.setPeerV2(context: self.context, theme: self.presentationData.theme.theme, peer: EnginePeer(peer), authorOfMessage: self.messageReference, overrideImage: nil, emptyColor: .black, synchronousLoad: false, displayDimensions: CGSize(width: avatarHeaderSize(), height: avatarHeaderSize()))

View file

@ -122,7 +122,7 @@ private final class ChatRankInfoSheetContent: CombinedComponent {
let textString: String
let linkColor: UIColor
var linkHasBackground = true
let additionalTextString: String? = "Only admins can assign member tags in this group."
let additionalTextString: String? = component.canChange ? nil : "Only admins can assign member tags in this group."
var adminPreviewTag = "Admin Tag"
var adminPreviewRole: ChatRankInfoScreenRole = .admin
@ -379,7 +379,7 @@ private final class ChatRankInfoSheetContent: CombinedComponent {
var buttonTitle: [AnyComponentWithIdentity<Empty>] = []
if canEdit {
buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent(
text: "Add My Tag",
text: "Set My Tag",
badge: 0,
textColor: theme.list.itemCheckColors.foregroundColor,
badgeBackground: theme.list.itemCheckColors.foregroundColor,

View file

@ -65,42 +65,23 @@ private final class ChatParticipantRightsContent: CombinedComponent {
weak var controller: ViewController?
var memberId: EnginePeer.Id {
let memberId: EnginePeer.Id
switch subject {
case let .admin(_, peerIdValue):
memberId = peerIdValue
case let .member(_, peerIdValue, _):
memberId = peerIdValue
case .rank:
memberId = self.context.account.peerId
}
return memberId
}
init(context: AccountContext, subject: ChatParticipantRightsScreen.Subject) {
self.context = context
self.subject = subject
super.init()
let peerId: EnginePeer.Id
let participantId: EnginePeer.Id
switch subject {
case let .admin(_, peerIdValue):
peerId = peerIdValue
case let .member(_, peerIdValue, rankValue):
peerId = peerIdValue
self.rank = rankValue
self.initialRank = rankValue
case let .rank(_, peerIdValue, rankValue):
peerId = peerIdValue
case let .rank(_, participantIdValue, rankValue, _):
participantId = participantIdValue
self.rank = rankValue
self.initialRank = rankValue
}
self.disposable = (context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Peer.Presence(id: peerId)
TelegramEngine.EngineData.Item.Peer.Peer(id: participantId),
TelegramEngine.EngineData.Item.Peer.Presence(id: participantId)
)
|> deliverOnMainQueue).start(next: { [weak self] peer, presence in
guard let self else {
@ -117,28 +98,10 @@ private final class ChatParticipantRightsContent: CombinedComponent {
}
func complete() {
var rank = self.rank
if rank?.isEmpty == true {
rank = nil
}
let _ = self.context.peerChannelMemberCategoriesContextsManager.updateMemberRank(engine: self.context.engine, peerId: self.subject.peerId, memberId: self.memberId, rank: rank).start()
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController {
Queue.mainQueue().after(0.5) {
//TODO:localize
var title: String?
var text: String
if let rank {
title = "Tag added"
text = rank
} else {
text = "Tag removed"
}
let toastController = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: title, text: text, cancel: nil, destructive: false), appearance: .init(isNarrow: true), action: { _ in return true})
(navigationController.topViewController as? ViewController)?.present(toastController, in: .current)
}
guard let controller = self.controller as? ChatParticipantRightsScreen else {
return
}
controller.complete(rank: self.rank)
}
}
@ -149,7 +112,6 @@ private final class ChatParticipantRightsContent: CombinedComponent {
static var body: Body {
let closeButton = Child(GlassBarButtonComponent.self)
let title = Child(MultilineTextComponent.self)
let peerSection = Child(ListSectionComponent.self)
let rankSection = Child(ListSectionComponent.self)
let doneButton = Child(ButtonComponent.self)
@ -192,10 +154,6 @@ private final class ChatParticipantRightsContent: CombinedComponent {
let titleString: String
switch component.subject {
case .member:
titleString = "Member"
case .admin:
titleString = "Admin"
case .rank:
titleString = "Member Tag"
}
@ -215,67 +173,35 @@ private final class ChatParticipantRightsContent: CombinedComponent {
contentHeight += 44.0
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let itemPresentationData = ItemListPresentationData(theme: theme, fontSize: presentationData.listsFontSize, strings: strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: environment.dateTimeFormat)
let listItemParams = ListViewItemLayoutParams(width: context.availableSize.width - sideInset * 2.0, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true)
switch component.subject {
case .admin, .member:
var peerSectionItems: [AnyComponentWithIdentity<Empty>] = []
peerSectionItems.append(
AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor(
itemGenerator: ItemListAvatarAndNameInfoItem(itemContext: .accountContext(component.context), presentationData: itemPresentationData, systemStyle: .glass, dateTimeFormat: environment.dateTimeFormat, mode: .generic, peer: state.peer, presence: state.presence, memberCount: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: 0, style: .blocks(withTopInset: false, withExtendedBottomInset: false), editingNameUpdated: { _ in
}, avatarTapped: {
}),
params: listItemParams
)))
)
let peerSection = peerSection.update(
component: ListSectionComponent(
theme: theme,
style: .glass,
header: nil,
footer: nil,
items: peerSectionItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: context.transition
)
context.add(peerSection
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + peerSection.size.height / 2.0))
)
contentHeight += peerSection.size.height
contentHeight += 38.0
default:
break
}
let peer: EnginePeer
if let current = state.peer {
peer = current
} else {
peer = EnginePeer.user(TelegramUser(id: EnginePeer.Id(0), accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
peer = EnginePeer.user(TelegramUser(id: component.subject.participantId, accessHash: nil, firstName: " ", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil))
}
var rankPreviewPlaceholder = ""
var rankFooterString = ""
var rankRole: ChatRankInfoScreenRole = .member
switch component.subject {
case .admin:
rankFooterString = "Add short tag next to \(peer.compactDisplayTitle)'s name."
rankPreviewPlaceholder = "admin"
rankRole = .admin
case .member:
rankFooterString = "Add short tag next to \(peer.compactDisplayTitle)'s name."
rankPreviewPlaceholder = "0"
case .rank:
case let .rank(_, _, _, role):
if peer.id == component.context.account.peerId {
rankFooterString = "Share your role, title, or how you're known in this group. Your tag is visible to all members."
} else {
rankFooterString = "Add short tag next to \(peer.compactDisplayTitle)'s name."
}
rankPreviewPlaceholder = "0"
switch role {
case .creator:
rankPreviewPlaceholder = strings.Conversation_Owner
case .admin:
rankPreviewPlaceholder = strings.Conversation_Admin
case .member:
rankPreviewPlaceholder = "0"
}
rankRole = role
}
let rankValue = state.rank ?? ""
@ -363,7 +289,7 @@ private final class ChatParticipantRightsContent: CombinedComponent {
let buttonTitle: String
switch component.subject {
case let .rank(_, _, initialRank):
case let .rank(_, _, initialRank, _):
if (initialRank ?? "").isEmpty && (state.rank ?? "").isEmpty {
buttonTitle = "Add Later"
} else if (initialRank ?? "").isEmpty && !(state.rank ?? "").isEmpty {
@ -373,8 +299,6 @@ private final class ChatParticipantRightsContent: CombinedComponent {
} else {
buttonTitle = "Edit Tag"
}
default:
buttonTitle = "Save Changes"
}
let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0)
@ -509,20 +433,21 @@ private final class ChatParticipantRightsComponent: CombinedComponent {
public class ChatParticipantRightsScreen: ViewControllerComponentContainer {
public enum Subject {
case member(peerId: EnginePeer.Id, participantId: EnginePeer.Id, rank: String?)
case admin(peerId: EnginePeer.Id, participantId: EnginePeer.Id)
case rank(peerId: EnginePeer.Id, participantId: EnginePeer.Id, rank: String?)
case rank(peerId: EnginePeer.Id, participantId: EnginePeer.Id, rank: String?, role: ChatRankInfoScreenRole)
var peerId: EnginePeer.Id {
switch self {
case let .member(peerId, _, _):
return peerId
case let .admin(peerId, _):
return peerId
case let .rank(peerId, _, _):
case let .rank(peerId, _, _, _):
return peerId
}
}
var participantId: EnginePeer.Id {
switch self {
case let .rank(_, participantId, _, _):
return participantId
}
}
}
private let context: AccountContext
@ -559,6 +484,42 @@ public class ChatParticipantRightsScreen: ViewControllerComponentContainer {
self.view.disablesInteractiveModalDismiss = true
}
fileprivate func complete(rank: String?) {
var rank = rank
if rank?.isEmpty == true {
rank = nil
}
if let rank, rank.count > rankMaxLength {
if let view = self.node.hostView.findTaggedView(tag: rankFieldTag) as? ListTextFieldItemComponent.View {
view.animateError()
HapticFeedback().error()
}
return
}
let _ = self.context.peerChannelMemberCategoriesContextsManager.updateMemberRank(engine: self.context.engine, peerId: self.subject.peerId, memberId: self.subject.participantId, rank: rank).start()
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
if let navigationController = self.navigationController as? NavigationController {
Queue.mainQueue().after(0.5) {
//TODO:localize
var title: String?
var text: String
if let rank {
title = "Tag added"
text = rank
} else {
text = "Tag removed"
}
let toastController = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: title, text: text, cancel: nil, destructive: false), appearance: .init(isNarrow: true), action: { _ in return true})
(navigationController.topViewController as? ViewController)?.present(toastController, in: .current)
}
}
self.dismissAnimated()
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

View file

@ -123,6 +123,7 @@ public final class ListTextFieldItemComponent: Component {
private let textField: TextField
private let placeholder = ComponentView<Empty>()
private let clearButton = ComponentView<Empty>()
private let counter = ComponentView<Empty>()
private var component: ListTextFieldItemComponent?
private weak var state: EmptyComponentState?
@ -178,6 +179,10 @@ public final class ListTextFieldItemComponent: Component {
return false
}
public func animateError() {
self.textField.layer.addShakeAnimation()
}
public func activateInput() {
self.textField.becomeFirstResponder()
}
@ -294,6 +299,31 @@ public final class ListTextFieldItemComponent: Component {
clearButtonView.isHidden = self.currentText.isEmpty || !self.textField.isFirstResponder
}
let text = self.textField.text ?? ""
if let characterLimit = component.characterLimit, text.count > 0 {
let counterSize = self.counter.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "\(characterLimit - text.count)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: text.count > characterLimit ? component.theme.list.itemDestructiveColor : component.theme.list.itemSecondaryTextColor)),
horizontalAlignment: .right
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 30.0 - component.contentInsets.left - component.contentInsets.right, height: 100.0)
)
let counterFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - component.contentInsets.right - clearButtonSize.width - counterSize.width + 17.0, y: floor((contentHeight - counterSize.height) * 0.5)), size: counterSize)
if let counterView = self.counter.view {
if counterView.superview == nil {
counterView.layer.anchorPoint = CGPoint()
counterView.isUserInteractionEnabled = false
self.addSubview(counterView)
}
transition.setPosition(view: counterView, position: counterFrame.origin)
counterView.bounds = CGRect(origin: CGPoint(), size: counterFrame.size)
}
} else {
self.counter.view?.removeFromSuperview()
}
self.separatorInset = 16.0 + component.contentInsets.left
return CGSize(width: availableSize.width, height: contentHeight)

View file

@ -4268,15 +4268,31 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
switch action {
case .editRank:
let rank: String?
let role: ChatRankInfoScreenRole
switch member {
case let .channelMember(participant, _):
rank = participant.participant.rank
case let .legacyGroupMember(_, _, _, _, _, rankValue):
switch participant.participant {
case .creator:
role = .creator
case let .member(_, _, adminInfo, _, _, _):
role = adminInfo != nil ? .admin : .member
}
case let .legacyGroupMember(_, roleValue, _, _, _, rankValue):
rank = rankValue
switch roleValue {
case .creator:
role = .creator
case .admin:
role = .admin
case .member:
role = .member
}
default:
rank = nil
role = .member
}
let controller = self.context.sharedContext.makeChatCustomRankSetupScreen(context: self.context, peerId: peer.id, participantId: member.id, rank: rank)
let controller = self.context.sharedContext.makeChatCustomRankSetupScreen(context: self.context, peerId: peer.id, participantId: member.id, rank: rank, role: role)
self.controller?.push(controller)
case .promote:
if case let .channelMember(channelMember, _) = member {

View file

@ -174,8 +174,6 @@ final class ChannelAppearanceScreenComponent: Component {
private let topOverscrollLayer = SimpleLayer()
private let scrollView: ScrollView
private let actionButton = ComponentView<Empty>()
private let bottomPanelBackgroundView: BlurredBackgroundView
private let bottomPanelSeparator: SimpleLayer
private let navigationTitle = ComponentView<Empty>()
@ -242,9 +240,6 @@ final class ChannelAppearanceScreenComponent: Component {
}
self.scrollView.alwaysBounceVertical = true
self.bottomPanelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
self.bottomPanelSeparator = SimpleLayer()
super.init(frame: frame)
self.scrollView.delegate = self
@ -253,9 +248,6 @@ final class ChannelAppearanceScreenComponent: Component {
self.scrollView.layer.addSublayer(self.topOverscrollLayer)
self.addSubview(self.edgeEffectView)
self.addSubview(self.bottomPanelBackgroundView)
self.layer.addSublayer(self.bottomPanelSeparator)
}
required init?(coder: NSCoder) {
@ -348,13 +340,7 @@ final class ChannelAppearanceScreenComponent: Component {
transition.setAlpha(view: navigationTitleView, alpha: navigationAlpha)
}
transition.setAlpha(view: self.edgeEffectView, alpha: navigationAlpha)
let bottomNavigationAlphaDistance: CGFloat = 16.0
let bottomNavigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentSize.height - self.scrollView.bounds.maxY) / bottomNavigationAlphaDistance))
transition.setAlpha(view: self.bottomPanelBackgroundView, alpha: bottomNavigationAlpha)
transition.setAlpha(layer: self.bottomPanelSeparator, alpha: bottomNavigationAlpha)
//transition.setAlpha(view: self.edgeEffectView, alpha: navigationAlpha)
}
private func resolveState() -> ResolvedState? {
@ -1007,11 +993,6 @@ final class ChannelAppearanceScreenComponent: Component {
}
self.requiredBoostSubject = requiredBoostSubject
let edgeEffectHeight: CGFloat = environment.navigationHeight + 8.0
let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: edgeEffectHeight))
transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame)
self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, blur: true, rect: edgeEffectFrame, edge: .top, edgeSize: min(30, edgeEffectFrame.height), transition: transition)
let headerColor: UIColor
if let profileColor {
let headerBackgroundColors = component.context.peerNameColors.getProfile(profileColor, dark: environment.theme.overallDarkAppearance, subject: .background)
@ -1843,13 +1824,10 @@ final class ChannelAppearanceScreenComponent: Component {
transition.setAlpha(view: buttonView, alpha: 1.0)
}
let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: buttonY - 8.0), size: CGSize(width: availableSize.width, height: availableSize.height - buttonY + 8.0))
transition.setFrame(view: self.bottomPanelBackgroundView, frame: bottomPanelFrame)
self.bottomPanelBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.bottomPanelBackgroundView.update(size: bottomPanelFrame.size, transition: transition.containedViewLayoutTransition)
self.bottomPanelSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
transition.setFrame(layer: self.bottomPanelSeparator, frame: CGRect(origin: CGPoint(x: bottomPanelFrame.minX, y: bottomPanelFrame.minY), size: CGSize(width: bottomPanelFrame.width, height: UIScreenPixel)))
let edgeEffectHeight: CGFloat = availableSize.height - buttonY + 36.0
let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - edgeEffectHeight), size: CGSize(width: availableSize.width, height: edgeEffectHeight))
transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame)
self.edgeEffectView.update(content: environment.theme.list.blocksBackgroundColor, alpha: 1.0, rect: edgeEffectFrame, edge: .bottom, edgeSize: edgeEffectFrame.height, transition: transition)
let previousBounds = self.scrollView.bounds
@ -1860,10 +1838,6 @@ final class ChannelAppearanceScreenComponent: Component {
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: availableSize.height - bottomPanelFrame.minY, right: 0.0)
if self.scrollView.verticalScrollIndicatorInsets != scrollInsets {
self.scrollView.verticalScrollIndicatorInsets = scrollInsets
}
if !previousBounds.isEmpty, !transition.animation.isImmediate {
let bounds = self.scrollView.bounds

View file

@ -5275,19 +5275,46 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let self, let chatPeer = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel else {
return
}
var canChange = false
let chatParticipant = Promise<ChannelParticipant?>()
if let defaultBannedRights = chatPeer.defaultBannedRights {
canChange = !defaultBannedRights.flags.contains(.banEditRank)
if canChange {
chatParticipant.set(self.context.engine.peers.fetchChannelParticipant(peerId: chatPeer.id, participantId: self.context.account.peerId))
}
}
let controller = self.context.sharedContext.makeChatRankInfoScreen(
context: self.context,
chatPeer: EnginePeer(chatPeer),
userPeer: peer,
role: role,
rank: rank,
canChange: false, //!(chatPeer.defaultBannedRights?.flags.contains(.editRank) ?? true),
canChange: canChange,
completion: { [weak self] in
guard let self else {
return
}
let controller = self.context.sharedContext.makeChatCustomRankSetupScreen(context: self.context, peerId: chatPeer.id, participantId: self.context.account.peerId, rank: nil)
self.push(controller)
let _ = (chatParticipant.get()
|> deliverOnMainQueue).start(next: { [weak self] participant in
guard let self else {
return
}
var rank: String? = nil
var role: ChatRankInfoScreenRole = .member
switch participant {
case let .creator(_, _, rankValue):
rank = rankValue
role = .creator
case let .member(_, _, adminInfo, _, rankValue, _):
rank = rankValue
role = adminInfo != nil ? .admin : .member
default:
break
}
let controller = self.context.sharedContext.makeChatCustomRankSetupScreen(context: self.context, peerId: chatPeer.id, participantId: self.context.account.peerId, rank: rank, role: role)
self.push(controller)
})
}
)
self.push(controller)

View file

@ -4317,8 +4317,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return PasskeysScreen(context: context, displaySkip: displaySkip, initialPasskeysData: nil, passkeysDataUpdated: { _ in }, completion: completion, cancel: dismiss)
}
public func makeChatCustomRankSetupScreen(context: AccountContext, peerId: EnginePeer.Id, participantId: EnginePeer.Id, rank: String?) -> ViewController {
return ChatParticipantRightsScreen(context: context, subject: .rank(peerId: peerId, participantId: participantId, rank: rank))
public func makeChatCustomRankSetupScreen(context: AccountContext, peerId: EnginePeer.Id, participantId: EnginePeer.Id, rank: String?, role: ChatRankInfoScreenRole) -> ViewController {
return ChatParticipantRightsScreen(context: context, subject: .rank(peerId: peerId, participantId: participantId, rank: rank, role: role))
}
public func makePeerCopyProtectionInfoScreen(context: AccountContext, completion: @escaping () -> Void) -> ViewController {