Invite Links Improvements

This commit is contained in:
Ilya Laktyushin 2021-01-21 16:28:51 +03:00
parent ebb613fb9d
commit d6777d68eb
30 changed files with 4595 additions and 4330 deletions

View File

@ -5821,6 +5821,7 @@ Sorry for the inconvenience.";
"InviteLink.Title" = "Invite Links"; "InviteLink.Title" = "Invite Links";
"InviteLink.PermanentLink" = "Permanent Link"; "InviteLink.PermanentLink" = "Permanent Link";
"InviteLink.PublicLink" = "Public Link";
"InviteLink.Share" = "Share Link"; "InviteLink.Share" = "Share Link";
"InviteLink.PeopleJoinedNone" = "no one joined yet"; "InviteLink.PeopleJoinedNone" = "no one joined yet";
"InviteLink.PeopleJoined_1" = "%@ people joined"; "InviteLink.PeopleJoined_1" = "%@ people joined";
@ -5891,9 +5892,18 @@ Sorry for the inconvenience.";
"InviteLink.ExpiresIn" = "expires in %@"; "InviteLink.ExpiresIn" = "expires in %@";
"InviteLink.InviteLinkCopiedText" = "Invite link copied to clipboard";
"Conversation.ChecksTooltip.Delivered" = "Delivered"; "Conversation.ChecksTooltip.Delivered" = "Delivered";
"Conversation.ChecksTooltip.Read" = "Read"; "Conversation.ChecksTooltip.Read" = "Read";
"DialogList.MultipleTypingPair" = "%@ and %@ are typing"; "DialogList.MultipleTypingPair" = "%@ and %@ are typing";
"Common.Save" = "Save"; "Common.Save" = "Save";
"UserInfo.FakeUserWarning" = "⚠️ Warning: Many users reported that this account impersonates a famous person or organization.";
"UserInfo.FakeBotWarning" = "⚠️ Warning: Many users reported that this account impersonates a famous person or organization.";
"GroupInfo.FakeGroupWarning" = "⚠️ Warning: Many users reported that this account impersonates a famous person or organization.";
"ChannelInfo.FakeChannelWarning" = "⚠️ Warning: Many users reported that this account impersonates a famous person or organization.";
"ReportPeer.ReasonFake" = "Fake Account";

View File

@ -383,11 +383,13 @@ public struct ContactListAdditionalOption: Equatable {
public let title: String public let title: String
public let icon: ContactListActionItemIcon public let icon: ContactListActionItemIcon
public let action: () -> Void public let action: () -> Void
public let clearHighlightAutomatically: Bool
public init(title: String, icon: ContactListActionItemIcon, action: @escaping () -> Void) { public init(title: String, icon: ContactListActionItemIcon, action: @escaping () -> Void, clearHighlightAutomatically: Bool = false) {
self.title = title self.title = title
self.icon = icon self.icon = icon
self.action = action self.action = action
self.clearHighlightAutomatically = clearHighlightAutomatically
} }
public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool { public static func ==(lhs: ContactListAdditionalOption, rhs: ContactListAdditionalOption) -> Bool {

View File

@ -1258,6 +1258,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if peer.isScam { if peer.isScam {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular) currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 2.0 credibilityIconOffset = 2.0
} else if peer.isFake {
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 2.0
} else if peer.isVerified { } else if peer.isVerified {
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
credibilityIconOffset = 3.0 credibilityIconOffset = 3.0
@ -1270,6 +1273,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if peer.isScam { if peer.isScam {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular) currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 2.0 credibilityIconOffset = 2.0
} else if peer.isFake {
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 2.0
} else if peer.isVerified { } else if peer.isVerified {
currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme)
credibilityIconOffset = 3.0 credibilityIconOffset = 3.0

View File

@ -175,7 +175,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
interaction.authorize() interaction.authorize()
}) })
case let .option(_, option, header, _, _): case let .option(_, option, header, _, _):
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: false, header: header, action: option.action) return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: option.clearHighlightAutomatically, header: header, action: option.action)
case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled): case let .peer(_, peer, presence, header, selection, _, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled):
var status: ContactsPeerItemStatus var status: ContactsPeerItemStatus
let itemPeer: ContactsPeerItemPeer let itemPeer: ContactsPeerItemPeer

View File

@ -56,7 +56,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
case usageHeader(PresentationTheme, String) case usageHeader(PresentationTheme, String)
case usagePicker(PresentationTheme, InviteLinkUsageLimit) case usagePicker(PresentationTheme, InviteLinkUsageLimit)
case usageCustomPicker(PresentationTheme, Int32?, Bool) case usageCustomPicker(PresentationTheme, Int32?, Bool, Bool)
case usageInfo(PresentationTheme, String) case usageInfo(PresentationTheme, String)
case revoke(PresentationTheme, String) case revoke(PresentationTheme, String)
@ -141,8 +141,8 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused): case let .usageCustomPicker(lhsTheme, lhsValue, lhsFocused, lhsCustomValue):
if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused { if case let .usageCustomPicker(rhsTheme, rhsValue, rhsFocused, rhsCustomValue) = rhs, lhsTheme === rhsTheme, lhsValue == rhsValue, lhsFocused == rhsFocused, lhsCustomValue == rhsCustomValue {
return true return true
} else { } else {
return false return false
@ -221,9 +221,14 @@ private enum InviteLinksEditEntry: ItemListNodeEntry {
return updatedState return updatedState
}) })
}) })
case let .usageCustomPicker(theme, value, focused): case let .usageCustomPicker(theme, value, focused, customValue):
let text = value.flatMap { String($0) } ?? (focused ? "" : presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsersUnlimited) let text: String
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, selectAllOnFocus: true, tag: nil, sectionId: self.section, textUpdated: { updatedText in if let value = value, value != 0 {
text = String(value)
} else {
text = focused ? "" : presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsersUnlimited
}
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: presentationData.strings.InviteLink_Create_UsersLimitNumberOfUsers, textColor: theme.list.itemPrimaryTextColor), text: text, placeholder: "", type: .number, alignment: .right, selectAllOnFocus: true, secondaryStyle: !customValue, tag: nil, sectionId: self.section, textUpdated: { updatedText in
guard !updatedText.isEmpty else { guard !updatedText.isEmpty else {
return return
} }
@ -284,7 +289,12 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state:
entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased())) entries.append(.usageHeader(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimit.uppercased()))
entries.append(.usagePicker(presentationData.theme, state.usage)) entries.append(.usagePicker(presentationData.theme, state.usage))
entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit))
var customValue = false
if case .custom = state.usage {
customValue = true
}
entries.append(.usageCustomPicker(presentationData.theme, state.usage.value, state.pickingUsageLimit, customValue))
entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo)) entries.append(.usageInfo(presentationData.theme, presentationData.strings.InviteLink_Create_UsersLimitInfo))
@ -401,7 +411,7 @@ public func inviteLinkEditController(context: AccountContext, peerId: PeerId, in
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
expireDate = currentTime + value expireDate = currentTime + value
} else { } else {
expireDate = nil expireDate = 0
} }
let usageLimit = state.usage.value let usageLimit = state.usage.value

View File

@ -20,6 +20,7 @@ import ShareController
import OverlayStatusController import OverlayStatusController
import PresentationDataUtils import PresentationDataUtils
import DirectionalPanGesture import DirectionalPanGesture
import UndoUI
class InviteLinkInviteInteraction { class InviteLinkInviteInteraction {
let context: AccountContext let context: AccountContext
@ -139,7 +140,7 @@ private enum InviteLinkInviteEntry: Comparable, Identifiable {
case let .header(theme, title, text): case let .header(theme, title, text):
return InviteLinkInviteHeaderItem(theme: theme, title: title, text: text) return InviteLinkInviteHeaderItem(theme: theme, title: title, text: text)
case let .mainLink(_, invite): case let .mainLink(_, invite):
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: { return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, count: 0, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: {
interaction.copyLink(invite) interaction.copyLink(invite)
}, shareAction: { }, shareAction: {
interaction.shareLink(invite) interaction.shareLink(invite)
@ -345,8 +346,9 @@ public final class InviteLinkInviteController: ViewController {
if let invite = invite { if let invite = invite {
UIPasteboard.general.string = invite.link UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root)) self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
} }
}))) })))
@ -390,8 +392,9 @@ public final class InviteLinkInviteController: ViewController {
self?.controller?.presentInGlobalOverlay(contextController) self?.controller?.presentInGlobalOverlay(contextController)
}, copyLink: { [weak self] invite in }, copyLink: { [weak self] invite in
UIPasteboard.general.string = invite.link UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root)) self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}, shareLink: { [weak self] invite in }, shareLink: { [weak self] invite in
let shareController = ShareController(context: context, subject: .url(invite.link)) let shareController = ShareController(context: context, subject: .url(invite.link))
self?.controller?.present(shareController, in: .window(.root)) self?.controller?.present(shareController, in: .window(.root))
@ -625,6 +628,10 @@ public final class InviteLinkInviteController: ViewController {
} }
private var panGestureArguments: CGFloat? private var panGestureArguments: CGFloat?
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer is DirectionalPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer
}
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) { @objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
let contentOffset = self.listNode.visibleContentOffset() let contentOffset = self.listNode.visibleContentOffset()
@ -633,10 +640,20 @@ public final class InviteLinkInviteController: ViewController {
self.panGestureArguments = 0.0 self.panGestureArguments = 0.0
case .changed: case .changed:
var translation = recognizer.translation(in: self.contentNode.view).y var translation = recognizer.translation(in: self.contentNode.view).y
if let currentPanOffset = self.panGestureArguments { if let currentOffset = self.panGestureArguments {
if case let .known(value) = contentOffset, value <= 0.5 { if case let .known(value) = contentOffset, value <= 0.5 {
if currentOffset > 0.0 {
let translation = self.listNode.scroller.panGestureRecognizer.translation(in: self.listNode.scroller)
if translation.y > 10.0 {
self.listNode.scroller.panGestureRecognizer.isEnabled = false
self.listNode.scroller.panGestureRecognizer.isEnabled = true
} else {
self.listNode.scroller.panGestureRecognizer.setTranslation(CGPoint(), in: self.listNode.scroller)
}
}
} else { } else {
translation = currentPanOffset translation = 0.0
recognizer.setTranslation(CGPoint(), in: self.contentNode.view)
} }
self.panGestureArguments = translation self.panGestureArguments = translation

View File

@ -19,6 +19,7 @@ import ContextUI
import TelegramStringFormatting import TelegramStringFormatting
import ItemListPeerActionItem import ItemListPeerActionItem
import ShareController import ShareController
import UndoUI
private final class InviteLinkListControllerArguments { private final class InviteLinkListControllerArguments {
let context: AccountContext let context: AccountContext
@ -55,7 +56,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
case header(PresentationTheme, String) case header(PresentationTheme, String)
case mainLinkHeader(PresentationTheme, String) case mainLinkHeader(PresentationTheme, String)
case mainLink(PresentationTheme, ExportedInvitation?, [Peer]) case mainLink(PresentationTheme, ExportedInvitation?, [Peer], Int32, Bool)
case linksHeader(PresentationTheme, String) case linksHeader(PresentationTheme, String)
case linksCreate(PresentationTheme, String) case linksCreate(PresentationTheme, String)
@ -117,8 +118,8 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .mainLink(lhsTheme, lhsInvite, lhsPeers): case let .mainLink(lhsTheme, lhsInvite, lhsPeers, lhsImportersCount, lhsIsPublic):
if case let .mainLink(rhsTheme, rhsInvite, rhsPeers) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite, arePeerArraysEqual(lhsPeers, rhsPeers) { if case let .mainLink(rhsTheme, rhsInvite, rhsPeers, rhsImportersCount, rhsIsPublic) = rhs, lhsTheme === rhsTheme, lhsInvite == rhsInvite, arePeerArraysEqual(lhsPeers, rhsPeers), lhsImportersCount == rhsImportersCount, lhsIsPublic == rhsIsPublic {
return true return true
} else { } else {
return false return false
@ -179,8 +180,8 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
return InviteLinkHeaderItem(theme: theme, text: text, sectionId: self.section) return InviteLinkHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .mainLinkHeader(_, text): case let .mainLinkHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .mainLink(_, invite, peers): case let .mainLink(_, invite, peers, importersCount, isPublic):
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: peers, displayButton: true, displayImporters: true, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: importersCount, peers: peers, displayButton: true, displayImporters: !isPublic, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
if let invite = invite { if let invite = invite {
arguments.copyLink(invite) arguments.copyLink(invite)
} }
@ -225,14 +226,18 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
} }
} }
private func inviteLinkListControllerEntries(presentationData: PresentationData, view: PeerView, invites: [ExportedInvitation]?, revokedInvites: [ExportedInvitation]?, mainPeers: [Peer]) -> [InviteLinksListEntry] { private func inviteLinkListControllerEntries(presentationData: PresentationData, view: PeerView, invites: [ExportedInvitation]?, revokedInvites: [ExportedInvitation]?, importers: PeerInvitationImportersState?) -> [InviteLinksListEntry] {
var entries: [InviteLinksListEntry] = [] var entries: [InviteLinksListEntry] = []
entries.append(.header(presentationData.theme, presentationData.strings.InviteLink_CreatePrivateLinkHelp)) entries.append(.header(presentationData.theme, presentationData.strings.InviteLink_CreatePrivateLinkHelp))
entries.append(.mainLinkHeader(presentationData.theme, presentationData.strings.InviteLink_PermanentLink.uppercased()))
let mainInvite: ExportedInvitation? let mainInvite: ExportedInvitation?
if let invites = invites, let invite = invites.first(where: { $0.isPermanent && !$0.isRevoked }) { var isPublic = false
if let peer = peerViewMainPeer(view), let address = peer.addressName, !address.isEmpty {
mainInvite = ExportedInvitation(link: "t.me/\(address)", isPermanent: true, isRevoked: false, adminId: PeerId(0), date: 0, startDate: nil, expireDate: nil, usageLimit: nil, count: nil)
isPublic = true
} else if let invites = invites, let invite = invites.first(where: { $0.isPermanent && !$0.isRevoked }) {
mainInvite = invite mainInvite = invite
} else if let invite = (view.cachedData as? CachedChannelData)?.exportedInvitation { } else if let invite = (view.cachedData as? CachedChannelData)?.exportedInvitation {
mainInvite = invite mainInvite = invite
@ -241,7 +246,19 @@ private func inviteLinkListControllerEntries(presentationData: PresentationData,
} else { } else {
mainInvite = nil mainInvite = nil
} }
entries.append(.mainLink(presentationData.theme, mainInvite, mainPeers))
entries.append(.mainLinkHeader(presentationData.theme, isPublic ? presentationData.strings.InviteLink_PublicLink.uppercased() : presentationData.strings.InviteLink_PermanentLink.uppercased()))
let importersCount: Int32
if let count = importers?.count {
importersCount = count
} else if let count = mainInvite?.count {
importersCount = count
} else {
importersCount = 0
}
entries.append(.mainLink(presentationData.theme, mainInvite, importers?.importers.prefix(3).compactMap { $0.peer.peer } ?? [], importersCount, isPublic))
entries.append(.linksHeader(presentationData.theme, presentationData.strings.InviteLink_AdditionalLinks.uppercased())) entries.append(.linksHeader(presentationData.theme, presentationData.strings.InviteLink_AdditionalLinks.uppercased()))
entries.append(.linksCreate(presentationData.theme, presentationData.strings.InviteLink_Create)) entries.append(.linksCreate(presentationData.theme, presentationData.strings.InviteLink_Create))
@ -320,10 +337,11 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
pushControllerImpl?(controller) pushControllerImpl?(controller)
}, copyLink: { invite in }, copyLink: { invite in
UIPasteboard.general.string = invite.link UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil) presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}, mainLinkContextAction: { invite, node, gesture in }, mainLinkContextAction: { invite, node, gesture in
guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?() else { guard let node = node as? ContextExtractedContentContainingNode, let controller = getControllerImpl?(), let invite = invite else {
return return
} }
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -334,11 +352,10 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
}, action: { _, f in }, action: { _, f in
f(.dismissWithoutContent) f(.dismissWithoutContent)
if let invite = invite { UIPasteboard.general.string = invite.link
UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil) presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}
}))) })))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
@ -346,56 +363,56 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
}, action: { _, f in }, action: { _, f in
f(.dismissWithoutContent) f(.dismissWithoutContent)
if let invite = invite { let controller = InviteLinkQRCodeController(context: context, invite: invite)
let controller = InviteLinkQRCodeController(context: context, invite: invite) presentControllerImpl?(controller, nil)
presentControllerImpl?(controller, nil)
}
}))) })))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in if invite.adminId.toInt64() != 0 {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in
}, action: { _, f in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor)
f(.dismissWithoutContent) }, action: { _, f in
f(.dismissWithoutContent)
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in let controller = ActionSheetController(presentationData: presentationData)
controller?.dismissAnimated() let dismissAction: () -> Void = { [weak controller] in
} controller?.dismissAnimated()
controller.setItemGroups([ }
ActionSheetItemGroup(items: [ controller.setItemGroups([
ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text),
dismissAction() ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: {
dismissAction()
var revoke = false
updateState { state in var revoke = false
if !state.revokingPrivateLink { updateState { state in
revoke = true if !state.revokingPrivateLink {
var updatedState = state revoke = true
updatedState.revokingPrivateLink = true
return updatedState
} else {
return state
}
}
if revoke {
revokeLinkDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
updateState { state in
var updatedState = state var updatedState = state
updatedState.revokingPrivateLink = false updatedState.revokingPrivateLink = true
return updatedState return updatedState
} else {
return state
} }
}
invitesContext.reload() if revoke {
revokedInvitesContext.reload() revokeLinkDisposable.set((revokePersistentPeerExportedInvitation(account: context.account, peerId: peerId) |> deliverOnMainQueue).start(completed: {
})) updateState { state in
} var updatedState = state
}) updatedState.revokingPrivateLink = false
]), return updatedState
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) }
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) invitesContext.reload()
}))) revokedInvitesContext.reload()
}))
}
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})))
}
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
presentInGlobalOverlayImpl?(contextController) presentInGlobalOverlayImpl?(contextController)
@ -426,11 +443,21 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
f(.dismissWithoutContent) f(.dismissWithoutContent)
UIPasteboard.general.string = invite.link UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), nil) presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}))) })))
if !invite.isRevoked { if !invite.isRevoked {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextShare, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.dismissWithoutContent)
let shareController = ShareController(context: context, subject: .url(invite.link))
presentControllerImpl?(shareController, nil)
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor)
}, action: { _, f in }, action: { _, f in
@ -564,11 +591,19 @@ public func inviteLinkListController(context: AccountContext, peerId: PeerId) ->
importersState.set(context.state |> map(Optional.init)) importersState.set(context.state |> map(Optional.init))
} }
let previousRevokedInvites = Atomic<PeerExportedInvitationsState?>(value: nil)
let signal = combineLatest(context.sharedContext.presentationData, peerView, importersContext, importersState.get(), invitesContext.state, revokedInvitesContext.state) let signal = combineLatest(context.sharedContext.presentationData, peerView, importersContext, importersState.get(), invitesContext.state, revokedInvitesContext.state)
|> deliverOnMainQueue |> deliverOnMainQueue
|> map { presentationData, view, importersContext, importers, invites, revokedInvites -> (ItemListControllerState, (ItemListNodeState, Any)) in |> map { presentationData, view, importersContext, importers, invites, revokedInvites -> (ItemListControllerState, (ItemListNodeState, Any)) in
let previousRevokedInvites = previousRevokedInvites.swap(invites)
var crossfade = false
if (previousRevokedInvites?.hasLoadedOnce ?? false) != (revokedInvites.hasLoadedOnce) {
crossfade = true
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.InviteLink_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.InviteLink_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkListControllerEntries(presentationData: presentationData, view: view, invites: invites.invitations, revokedInvites: revokedInvites.invitations, mainPeers: importers?.importers.compactMap { $0.peer.peer } ?? []), style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: inviteLinkListControllerEntries(presentationData: presentationData, view: view, invites: invites.invitations, revokedInvites: revokedInvites.invitations, importers: importers), style: .blocks, emptyStateItem: nil, crossfadeState: crossfade, animateChanges: false)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }

View File

@ -20,6 +20,7 @@ import ShareController
import OverlayStatusController import OverlayStatusController
import PresentationDataUtils import PresentationDataUtils
import DirectionalPanGesture import DirectionalPanGesture
import UndoUI
class InviteLinkViewInteraction { class InviteLinkViewInteraction {
let context: AccountContext let context: AccountContext
@ -171,7 +172,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case let .link(_, invite): case let .link(_, invite):
let buttonColor = color(for: invite) let buttonColor = color(for: invite)
let availability = invitationAvailability(invite) let availability = invitationAvailability(invite)
return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, peers: [], displayButton: !invite.isRevoked && !availability.isZero, displayImporters: false, buttonColor: buttonColor, sectionId: 0, style: .plain, copyAction: { return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, count: 0, peers: [], displayButton: !invite.isRevoked && !availability.isZero, displayImporters: false, buttonColor: buttonColor, sectionId: 0, style: .plain, copyAction: {
interaction.copyLink(invite) interaction.copyLink(invite)
}, shareAction: { }, shareAction: {
interaction.shareLink(invite) interaction.shareLink(invite)
@ -393,8 +394,9 @@ public final class InviteLinkViewController: ViewController {
} }
}, copyLink: { [weak self] invite in }, copyLink: { [weak self] invite in
UIPasteboard.general.string = invite.link UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root)) self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}, shareLink: { [weak self] invite in }, shareLink: { [weak self] invite in
let shareController = ShareController(context: context, subject: .url(invite.link)) let shareController = ShareController(context: context, subject: .url(invite.link))
self?.controller?.present(shareController, in: .window(.root)) self?.controller?.present(shareController, in: .window(.root))
@ -412,8 +414,9 @@ public final class InviteLinkViewController: ViewController {
f(.dismissWithoutContent) f(.dismissWithoutContent)
UIPasteboard.general.string = invite.link UIPasteboard.general.string = invite.link
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self?.controller?.present(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.Username_LinkCopied, false)), in: .window(.root)) self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root))
}))) })))
if invite.isRevoked { if invite.isRevoked {
@ -452,6 +455,8 @@ public final class InviteLinkViewController: ViewController {
self?.controller?.present(controller, in: .window(.root)) self?.controller?.present(controller, in: .window(.root))
}))) })))
} }
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture) let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node)), items: .single(items), reactionItems: [], gesture: gesture)
self?.controller?.presentInGlobalOverlay(contextController) self?.controller?.presentInGlobalOverlay(contextController)
@ -717,12 +722,20 @@ public final class InviteLinkViewController: ViewController {
self.panGestureArguments = 0.0 self.panGestureArguments = 0.0
case .changed: case .changed:
var translation = recognizer.translation(in: self.contentNode.view).y var translation = recognizer.translation(in: self.contentNode.view).y
if let currentPanOffset = self.panGestureArguments { if let currentOffset = self.panGestureArguments {
if case let .known(value) = contentOffset, value <= 0.5 { if case let .known(value) = contentOffset, value <= 0.5 {
if currentOffset > 0.0 {
let translation = self.listNode.scroller.panGestureRecognizer.translation(in: self.listNode.scroller)
if translation.y > 10.0 {
self.listNode.scroller.panGestureRecognizer.isEnabled = false
self.listNode.scroller.panGestureRecognizer.isEnabled = true
} else {
self.listNode.scroller.panGestureRecognizer.setTranslation(CGPoint(), in: self.listNode.scroller)
}
}
} else { } else {
translation = currentPanOffset translation = 0.0
recognizer.setTranslation(CGPoint(), in: self.contentNode.view)
} }
self.panGestureArguments = translation self.panGestureArguments = translation

View File

@ -96,7 +96,8 @@ private let shareIcon = generateImage(CGSize(width: 26.0, height: 26.0), context
private class ItemNode: ASDisplayNode { private class ItemNode: ASDisplayNode {
private let selectionNode: HighlightTrackingButtonNode private let selectionNode: HighlightTrackingButtonNode
private let wrapperNode: ASDisplayNode private let wrapperNode: ASDisplayNode
private let backgroundNode: ASImageNode private let backgroundNode: ASDisplayNode
private let backgroundGradientLayer: CAGradientLayer
private let iconNode: ASImageNode private let iconNode: ASImageNode
private var timerNode: TimerNode? private var timerNode: TimerNode?
@ -122,11 +123,19 @@ private class ItemNode: ASDisplayNode {
self.selectionNode = HighlightTrackingButtonNode() self.selectionNode = HighlightTrackingButtonNode()
self.wrapperNode = ASDisplayNode() self.wrapperNode = ASDisplayNode()
self.backgroundNode = ASImageNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.displaysAsynchronously = false self.backgroundNode.clipsToBounds = true
self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.cornerRadius = 15.0
if #available(iOS 13.0, *) {
self.backgroundNode.layer.cornerCurve = .continuous
}
self.backgroundNode.isUserInteractionEnabled = false self.backgroundNode.isUserInteractionEnabled = false
self.backgroundGradientLayer = CAGradientLayer()
self.backgroundGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
self.backgroundGradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
self.backgroundNode.layer.addSublayer(self.backgroundGradientLayer)
self.iconNode = ASImageNode() self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true self.iconNode.displayWithoutProcessing = true
@ -256,10 +265,10 @@ private class ItemNode: ASDisplayNode {
snapshotView?.removeFromSuperview() snapshotView?.removeFromSuperview()
}) })
} }
self.backgroundNode.image = generateBackgroundImage(colors: colors) self.backgroundGradientLayer.colors = colors as? [Any]
} }
} else { } else {
self.backgroundNode.image = generateBackgroundImage(colors: colors) self.backgroundGradientLayer.colors = colors as? [Any]
} }
let secondaryTextColor = color.colors.text let secondaryTextColor = color.colors.text
@ -329,7 +338,7 @@ private class ItemNode: ASDisplayNode {
self.timerNode = timerNode self.timerNode = timerNode
self.addSubnode(timerNode) self.addSubnode(timerNode)
} }
timerNode.update(color: UIColor.white, creationTimestamp: invite.date, deadlineTimestamp: expireDate) timerNode.update(color: UIColor.white, creationTimestamp: invite.startDate ?? invite.date, deadlineTimestamp: expireDate)
if share { if share {
subtitleText = presentationData.strings.InviteLink_TapToCopy subtitleText = presentationData.strings.InviteLink_TapToCopy
} }
@ -359,6 +368,7 @@ private class ItemNode: ASDisplayNode {
transition.updateFrame(node: self.wrapperNode, frame: backgroundFrame) transition.updateFrame(node: self.wrapperNode, frame: backgroundFrame)
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
transition.updateFrame(node: self.selectionNode, frame: backgroundFrame) transition.updateFrame(node: self.selectionNode, frame: backgroundFrame)
transition.updateFrame(layer: self.backgroundGradientLayer, frame: backgroundFrame)
let buttonSize = CGSize(width: 26.0, height: 26.0) let buttonSize = CGSize(width: 26.0, height: 26.0)
let buttonFrame = CGRect(origin: CGPoint(x: itemSize.width - buttonSize.width - 12.0, y: 12.0), size: buttonSize) let buttonFrame = CGRect(origin: CGPoint(x: itemSize.width - buttonSize.width - 12.0, y: 12.0), size: buttonSize)
@ -532,7 +542,7 @@ private final class TimerNode: ASDisplayNode {
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
var fraction = CGFloat(params.deadlineTimestamp - currentTimestamp) / CGFloat(params.deadlineTimestamp - params.creationTimestamp) var fraction = CGFloat(params.deadlineTimestamp - currentTimestamp) / CGFloat(params.deadlineTimestamp - params.creationTimestamp)
fraction = 1.0 - max(0.0, min(1.0, fraction)) fraction = max(0.0001, 1.0 - max(0.0, min(1.0, fraction)))
let image: UIImage? let image: UIImage?

View File

@ -32,7 +32,9 @@ enum InviteLinkUsageLimit: Equatable {
} }
init(value: Int32?) { init(value: Int32?) {
if let value = value { if value == 0 {
self = .unlimited
} else if let value = value {
if value == 1 { if value == 1 {
self = .low self = .low
} else if value == 10 { } else if value == 10 {
@ -56,7 +58,7 @@ enum InviteLinkUsageLimit: Equatable {
case .high: case .high:
return 100 return 100
case .unlimited: case .unlimited:
return nil return 0
case let .custom(value): case let .custom(value):
return value return value
} }

View File

@ -29,6 +29,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
let context: AccountContext let context: AccountContext
let presentationData: ItemListPresentationData let presentationData: ItemListPresentationData
let invite: ExportedInvitation? let invite: ExportedInvitation?
let count: Int32
let peers: [Peer] let peers: [Peer]
let displayButton: Bool let displayButton: Bool
let displayImporters: Bool let displayImporters: Bool
@ -45,6 +46,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
context: AccountContext, context: AccountContext,
presentationData: ItemListPresentationData, presentationData: ItemListPresentationData,
invite: ExportedInvitation?, invite: ExportedInvitation?,
count: Int32,
peers: [Peer], peers: [Peer],
displayButton: Bool, displayButton: Bool,
displayImporters: Bool, displayImporters: Bool,
@ -60,6 +62,7 @@ public class ItemListPermanentInviteLinkItem: ListViewItem, ItemListItem {
self.context = context self.context = context
self.presentationData = presentationData self.presentationData = presentationData
self.invite = invite self.invite = invite
self.count = count
self.peers = peers self.peers = peers
self.displayButton = displayButton self.displayButton = displayButton
self.displayImporters = displayImporters self.displayImporters = displayImporters
@ -290,18 +293,13 @@ public class ItemListPermanentInviteLinkItemNode: ListViewItemNode, ItemListItem
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let (addressLayout, addressApply) = makeAddressLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.invite.flatMap({ $0.link.replacingOccurrences(of: "https://", with: "") }) ?? "", font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - 20.0 - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (addressLayout, addressApply) = makeAddressLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.invite.flatMap({ $0.link.replacingOccurrences(of: "https://", with: "") }) ?? "", font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 90.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let subtitle: String let subtitle: String
let subtitleColor: UIColor let subtitleColor: UIColor
if let count = item.invite?.count { if item.count > 0 {
if count > 0 { subtitle = item.presentationData.strings.InviteLink_PeopleJoined(item.count)
subtitle = item.presentationData.strings.InviteLink_PeopleJoined(count) subtitleColor = item.presentationData.theme.list.itemAccentColor
subtitleColor = item.presentationData.theme.list.itemAccentColor
} else {
subtitle = item.presentationData.strings.InviteLink_PeopleJoinedNone
subtitleColor = item.presentationData.theme.list.itemSecondaryTextColor
}
} else { } else {
subtitle = item.presentationData.strings.InviteLink_PeopleJoinedNone subtitle = item.presentationData.strings.InviteLink_PeopleJoinedNone
subtitleColor = item.presentationData.theme.list.itemSecondaryTextColor subtitleColor = item.presentationData.theme.list.itemSecondaryTextColor

View File

@ -376,6 +376,9 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo
if peer.isScam { if peer.isScam {
credibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular) credibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 6.0 credibilityIconOffset = 6.0
} else if peer.isFake {
credibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, type: .regular)
credibilityIconOffset = 2.0
} else if peer.isVerified { } else if peer.isVerified {
credibilityIconImage = PresentationResourcesItemList.verifiedPeerIcon(item.presentationData.theme) credibilityIconImage = PresentationResourcesItemList.verifiedPeerIcon(item.presentationData.theme)
} }

View File

@ -47,6 +47,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let maxLength: Int let maxLength: Int
let enabled: Bool let enabled: Bool
let selectAllOnFocus: Bool let selectAllOnFocus: Bool
let secondaryStyle: Bool
public let sectionId: ItemListSectionId public let sectionId: ItemListSectionId
let action: () -> Void let action: () -> Void
let textUpdated: (String) -> Void let textUpdated: (String) -> Void
@ -56,7 +57,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
let cleared: (() -> Void)? let cleared: (() -> Void)?
public let tag: ItemListItemTag? public let tag: ItemListItemTag?
public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) { public init(presentationData: ItemListPresentationData, title: NSAttributedString, text: String, placeholder: String, type: ItemListSingleLineInputItemType = .regular(capitalization: true, autocorrection: true), returnKeyType: UIReturnKeyType = .`default`, alignment: ItemListSingleLineInputAlignment = .default, spacing: CGFloat = 0.0, clearType: ItemListSingleLineInputClearType = .none, maxLength: Int = 0, enabled: Bool = true, selectAllOnFocus: Bool = false, secondaryStyle: Bool = false, tag: ItemListItemTag? = nil, sectionId: ItemListSectionId, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> String)? = nil, updatedFocus: ((Bool) -> Void)? = nil, action: @escaping () -> Void, cleared: (() -> Void)? = nil) {
self.presentationData = presentationData self.presentationData = presentationData
self.title = title self.title = title
self.text = text self.text = text
@ -69,6 +70,7 @@ public class ItemListSingleLineInputItem: ListViewItem, ItemListItem {
self.maxLength = maxLength self.maxLength = maxLength
self.enabled = enabled self.enabled = enabled
self.selectAllOnFocus = selectAllOnFocus self.selectAllOnFocus = selectAllOnFocus
self.secondaryStyle = secondaryStyle
self.tag = tag self.tag = tag
self.sectionId = sectionId self.sectionId = sectionId
self.textUpdated = textUpdated self.textUpdated = textUpdated
@ -183,7 +185,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
self.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)] self.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)]
self.textNode.textField.font = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) self.textNode.textField.font = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
self.textNode.textField.textColor = item.presentationData.theme.list.itemPrimaryTextColor self.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor
self.textNode.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance self.textNode.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
self.textNode.textField.tintColor = item.presentationData.theme.list.itemAccentColor self.textNode.textField.tintColor = item.presentationData.theme.list.itemAccentColor
self.textNode.textField.accessibilityHint = item.placeholder self.textNode.textField.accessibilityHint = item.placeholder
@ -218,6 +220,11 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
fontUpdated = true fontUpdated = true
} }
var styleUpdated = false
if currentItem?.secondaryStyle != item.secondaryStyle {
styleUpdated = true
}
let leftInset: CGFloat = 16.0 + params.leftInset let leftInset: CGFloat = 16.0 + params.leftInset
var rightInset: CGFloat = 16.0 + params.rightInset var rightInset: CGFloat = 16.0 + params.rightInset
@ -252,13 +259,17 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.textNode.textField.textColor = item.presentationData.theme.list.itemPrimaryTextColor strongSelf.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor
strongSelf.textNode.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance strongSelf.textNode.textField.keyboardAppearance = item.presentationData.theme.rootController.keyboardColor.keyboardAppearance
strongSelf.textNode.textField.tintColor = item.presentationData.theme.list.itemAccentColor strongSelf.textNode.textField.tintColor = item.presentationData.theme.list.itemAccentColor
} }
if fontUpdated { if fontUpdated {
strongSelf.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)] strongSelf.textNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(item.presentationData.fontSize.itemListBaseFontSize)]
}
if styleUpdated {
strongSelf.textNode.textField.textColor = item.secondaryStyle ? item.presentationData.theme.list.itemSecondaryTextColor : item.presentationData.theme.list.itemPrimaryTextColor
} }
let _ = titleApply() let _ = titleApply()

View File

@ -293,7 +293,7 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
case let .privateLinkHeader(_, title): case let .privateLinkHeader(_, title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .privateLink(_, invite, displayImporters): case let .privateLink(_, invite, displayImporters):
return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, peers: [], displayButton: true, displayImporters: displayImporters, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: { return ItemListPermanentInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: invite, count: 0, peers: [], displayButton: true, displayImporters: displayImporters, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
if let invite = invite { if let invite = invite {
arguments.copyLink(invite) arguments.copyLink(invite)
} }

View File

@ -20,6 +20,7 @@ public enum PeerReportSubject {
public enum PeerReportOption { public enum PeerReportOption {
case spam case spam
case fake
case violence case violence
case copyright case copyright
case pornography case pornography
@ -37,6 +38,8 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro
switch option { switch option {
case .spam: case .spam:
title = presentationData.strings.ReportPeer_ReasonSpam title = presentationData.strings.ReportPeer_ReasonSpam
case .fake:
title = presentationData.strings.ReportPeer_ReasonFake
case .violence: case .violence:
title = presentationData.strings.ReportPeer_ReasonViolence title = presentationData.strings.ReportPeer_ReasonViolence
case .pornography: case .pornography:
@ -57,6 +60,8 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro
switch option { switch option {
case .spam: case .spam:
reportReason = .spam reportReason = .spam
case .fake:
reportReason = .fake
case .violence: case .violence:
reportReason = .violence reportReason = .violence
case .pornography: case .pornography:
@ -112,6 +117,8 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe
switch option { switch option {
case .spam: case .spam:
title = presentationData.strings.ReportPeer_ReasonSpam title = presentationData.strings.ReportPeer_ReasonSpam
case .fake:
title = presentationData.strings.ReportPeer_ReasonFake
case .violence: case .violence:
title = presentationData.strings.ReportPeer_ReasonViolence title = presentationData.strings.ReportPeer_ReasonViolence
case .pornography: case .pornography:
@ -128,6 +135,8 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe
switch option { switch option {
case .spam: case .spam:
reportReason = .spam reportReason = .spam
case .fake:
reportReason = .fake
case .violence: case .violence:
reportReason = .violence reportReason = .violence
case .pornography: case .pornography:

View File

@ -652,7 +652,15 @@ private func userInfoEntries(account: Account, presentationData: PresentationDat
} else { } else {
aboutTitle = presentationData.strings.Profile_About aboutTitle = presentationData.strings.Profile_About
} }
if user.isScam { if user.isFake {
let aboutValue: String
if let _ = user.botInfo {
aboutValue = presentationData.strings.UserInfo_FakeBotWarning
} else {
aboutValue = presentationData.strings.UserInfo_FakeUserWarning
}
entries.append(UserInfoEntry.about(presentationData.theme, peer, aboutTitle, aboutValue))
} else if user.isScam {
let aboutValue: String let aboutValue: String
if let _ = user.botInfo { if let _ = user.botInfo {
aboutValue = presentationData.strings.UserInfo_ScamBotWarning aboutValue = presentationData.strings.UserInfo_ScamBotWarning

View File

@ -143,6 +143,7 @@ public struct TelegramChannelFlags: OptionSet {
public static let hasGeo = TelegramChannelFlags(rawValue: 1 << 3) public static let hasGeo = TelegramChannelFlags(rawValue: 1 << 3)
public static let hasVoiceChat = TelegramChannelFlags(rawValue: 1 << 4) public static let hasVoiceChat = TelegramChannelFlags(rawValue: 1 << 4)
public static let hasActiveVoiceChat = TelegramChannelFlags(rawValue: 1 << 5) public static let hasActiveVoiceChat = TelegramChannelFlags(rawValue: 1 << 5)
public static let isFake = TelegramChannelFlags(rawValue: 1 << 6)
} }
public final class TelegramChannel: Peer { public final class TelegramChannel: Peer {

View File

@ -14,6 +14,7 @@ public struct UserInfoFlags: OptionSet {
public static let isVerified = UserInfoFlags(rawValue: (1 << 0)) public static let isVerified = UserInfoFlags(rawValue: (1 << 0))
public static let isSupport = UserInfoFlags(rawValue: (1 << 1)) public static let isSupport = UserInfoFlags(rawValue: (1 << 1))
public static let isScam = UserInfoFlags(rawValue: (1 << 2)) public static let isScam = UserInfoFlags(rawValue: (1 << 2))
public static let isFake = UserInfoFlags(rawValue: (1 << 3))
} }
public struct BotUserInfoFlags: OptionSet { public struct BotUserInfoFlags: OptionSet {

View File

@ -103,6 +103,9 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? {
if (flags & Int32(1 << 24)) != 0 { if (flags & Int32(1 << 24)) != 0 {
channelFlags.insert(.hasActiveVoiceChat) channelFlags.insert(.hasActiveVoiceChat)
} }
if (flags & Int32(1 << 25)) != 0 {
channelFlags.insert(.isFake)
}
let restrictionInfo: PeerAccessRestrictionInfo? let restrictionInfo: PeerAccessRestrictionInfo?
if let restrictionReason = restrictionReason { if let restrictionReason = restrictionReason {

View File

@ -75,6 +75,21 @@ public extension Message {
return false return false
} }
var isFake: Bool {
if let author = self.author, author.isFake {
return true
}
if let forwardAuthor = self.forwardInfo?.author, forwardAuthor.isFake {
return true
}
for attribute in self.attributes {
if let attribute = attribute as? InlineBotMessageAttribute, let peerId = attribute.peerId, let bot = self.peers[peerId] as? TelegramUser, bot.isFake {
return true
}
}
return false
}
var sourceReference: SourceReferenceMessageAttribute? { var sourceReference: SourceReferenceMessageAttribute? {
for attribute in self.attributes { for attribute in self.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute { if let attribute = attribute as? SourceReferenceMessageAttribute {

View File

@ -122,6 +122,17 @@ public extension Peer {
} }
} }
var isFake: Bool {
switch self {
case let user as TelegramUser:
return user.flags.contains(.isFake)
case let channel as TelegramChannel:
return channel.flags.contains(.isFake)
default:
return false
}
}
var isVerified: Bool { var isVerified: Bool {
switch self { switch self {
case let user as TelegramUser: case let user as TelegramUser:

View File

@ -78,6 +78,7 @@ public func reportPeer(account: Account, peerId: PeerId) -> Signal<Void, NoError
public enum ReportReason: Equatable { public enum ReportReason: Equatable {
case spam case spam
case fake
case violence case violence
case porno case porno
case childAbuse case childAbuse
@ -91,6 +92,8 @@ private extension ReportReason {
switch self { switch self {
case .spam: case .spam:
return .inputReportReasonSpam return .inputReportReasonSpam
case .fake:
return .inputReportReasonOther(text: "fake")
case .violence: case .violence:
return .inputReportReasonViolence return .inputReportReasonViolence
case .porno: case .porno:

View File

@ -53,6 +53,9 @@ extension TelegramUser {
if (flags & (1 << 24)) != 0 { if (flags & (1 << 24)) != 0 {
userFlags.insert(.isScam) userFlags.insert(.isScam)
} }
if (flags & Int32(1 << 26)) != 0 {
userFlags.insert(.isFake)
}
var botInfo: BotUserInfo? var botInfo: BotUserInfo?
if (flags & (1 << 14)) != 0 { if (flags & (1 << 14)) != 0 {
@ -96,6 +99,9 @@ extension TelegramUser {
if (flags & (1 << 24)) != 0 { if (flags & (1 << 24)) != 0 {
userFlags.insert(.isScam) userFlags.insert(.isScam)
} }
if (flags & Int32(1 << 26)) != 0 {
userFlags.insert(.isFake)
}
var botInfo: BotUserInfo? var botInfo: BotUserInfo?
if (flags & (1 << 14)) != 0 { if (flags & (1 << 14)) != 0 {
@ -156,6 +162,9 @@ extension TelegramUser {
if rhs.flags.contains(.isScam) { if rhs.flags.contains(.isScam) {
userFlags.insert(.isScam) userFlags.insert(.isScam)
} }
if rhs.flags.contains(.isFake) {
userFlags.insert(.isFake)
}
let botInfo: BotUserInfo? = rhs.botInfo let botInfo: BotUserInfo? = rhs.botInfo

View File

@ -77,6 +77,9 @@ public enum PresentationResourceKey: Int32 {
case chatListScamRegularIcon case chatListScamRegularIcon
case chatListScamOutgoingIcon case chatListScamOutgoingIcon
case chatListScamServiceIcon case chatListScamServiceIcon
case chatListFakeRegularIcon
case chatListFakeOutgoingIcon
case chatListFakeServiceIcon
case chatListSecretIcon case chatListSecretIcon
case chatListRecentStatusOnlineIcon case chatListRecentStatusOnlineIcon
case chatListRecentStatusOnlineHighlightedIcon case chatListRecentStatusOnlineHighlightedIcon

View File

@ -248,6 +248,42 @@ public struct PresentationResourcesChatList {
}) })
} }
public static func fakeIcon(_ theme: PresentationTheme, type: ScamIconType) -> UIImage? {
let key: PresentationResourceKey
let color: UIColor
switch type {
case .regular:
key = PresentationResourceKey.chatListFakeRegularIcon
color = theme.chat.message.incoming.scamColor
case .outgoing:
key = PresentationResourceKey.chatListFakeOutgoingIcon
color = theme.chat.message.outgoing.scamColor
case .service:
key = PresentationResourceKey.chatListFakeServiceIcon
color = theme.chat.serviceMessage.components.withDefaultWallpaper.scam
}
return theme.image(key.rawValue, { theme in
return generateImage(CGSize(width: 37.0, height: 16.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(color.cgColor)
context.setStrokeColor(color.cgColor)
context.setLineWidth(1.0)
context.addPath(UIBezierPath(roundedRect: bounds.insetBy(dx: 0.5, dy: 0.5), cornerRadius: 2.0).cgPath)
context.strokePath()
let titlePath = CGMutablePath()
titlePath.addRect(bounds.offsetBy(dx: 0.0, dy: -2.0 + UIScreenPixel))
let titleString = NSAttributedString(string: "FAKE", font: Font.bold(10.0), textColor: color, paragraphAlignment: .center)
let titleFramesetter = CTFramesetterCreateWithAttributedString(titleString as CFAttributedString)
let titleFrame = CTFramesetterCreateFrame(titleFramesetter, CFRangeMake(0, titleString.length), titlePath, nil)
CTFrameDraw(titleFrame, context)
})
})
}
public static func secretIcon(_ theme: PresentationTheme) -> UIImage? { public static func secretIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatListSecretIcon.rawValue, { theme in return theme.image(PresentationResourceKey.chatListSecretIcon.rawValue, { theme in
return generateImage(CGSize(width: 9.0, height: 12.0), rotatedContext: { size, context in return generateImage(CGSize(width: 9.0, height: 12.0), rotatedContext: { size, context in

View File

@ -1426,9 +1426,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
var isScam = effectiveAuthor.isScam var isScam = effectiveAuthor.isScam
if case let .peer(peerId) = item.chatLocation, let authorPeerId = item.message.author?.id, authorPeerId == peerId { if case let .peer(peerId) = item.chatLocation, let authorPeerId = item.message.author?.id, authorPeerId == peerId {
isScam = false
} else if effectiveAuthor.isScam {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme.theme, type: incoming ? .regular : .outgoing)
} else if effectiveAuthor.isFake {
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme.theme, type: incoming ? .regular : .outgoing)
} }
currentCredibilityIconImage = isScam ? PresentationResourcesChatList.scamIcon(item.presentationData.theme.theme, type: incoming ? .regular : .outgoing) : nil
} }
if let rawAuthorNameColor = authorNameColor { if let rawAuthorNameColor = authorNameColor {
var dimColors = false var dimColors = false

View File

@ -193,7 +193,14 @@ class ChatMessageForwardInfoNode: ASDisplayNode {
} }
} }
if peer.isScam { if peer.isFake {
switch type {
case let .bubble(incoming):
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, type: incoming ? .regular : .outgoing)
case .standalone:
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, type: .service)
}
} else if peer.isScam {
switch type { switch type {
case let .bubble(incoming): case let .bubble(incoming):
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, type: incoming ? .regular : .outgoing) currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, type: incoming ? .regular : .outgoing)

View File

@ -937,7 +937,11 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
})) }))
} }
if let cachedData = data.cachedData as? CachedUserData { if let cachedData = data.cachedData as? CachedUserData {
if user.isScam { if user.isFake {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_FakeBotWarning : presentationData.strings.UserInfo_FakeUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: {
interaction.requestLayout()
}))
} else if user.isScam {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: { items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: {
interaction.requestLayout() interaction.requestLayout()
})) }))
@ -1025,12 +1029,27 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
})) }))
} }
if let cachedData = data.cachedData as? CachedChannelData { if let cachedData = data.cachedData as? CachedChannelData {
if channel.isScam { let aboutText: String?
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPublicBioEntities), action: nil, requestLayout: { if channel.isFake {
interaction.requestLayout() if case .broadcast = channel.info {
})) aboutText = presentationData.strings.ChannelInfo_FakeChannelWarning
} else {
aboutText = presentationData.strings.GroupInfo_FakeGroupWarning
}
} else if channel.isScam {
if case .broadcast = channel.info {
aboutText = presentationData.strings.ChannelInfo_ScamChannelWarning
} else {
aboutText = presentationData.strings.GroupInfo_ScamGroupWarning
}
} else if let about = cachedData.about, !about.isEmpty { } else if let about = cachedData.about, !about.isEmpty {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPublicBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { aboutText = about
} else {
aboutText = nil
}
if let aboutText = aboutText {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_AboutItem, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPublicBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
interaction.requestLayout() interaction.requestLayout()
})) }))
} }
@ -1061,12 +1080,19 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
} }
} else if let group = data.peer as? TelegramGroup { } else if let group = data.peer as? TelegramGroup {
if let cachedData = data.cachedData as? CachedGroupData { if let cachedData = data.cachedData as? CachedGroupData {
if group.isScam { let aboutText: String?
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.PeerInfo_GroupAboutItem, text: presentationData.strings.GroupInfo_ScamGroupWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPublicBioEntities), action: nil, requestLayout: { if group.isFake {
interaction.requestLayout() aboutText = presentationData.strings.GroupInfo_FakeGroupWarning
})) } else if group.isScam {
aboutText = presentationData.strings.GroupInfo_ScamGroupWarning
} else if let about = cachedData.about, !about.isEmpty { } else if let about = cachedData.about, !about.isEmpty {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.PeerInfo_GroupAboutItem, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPublicBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: { aboutText = about
} else {
aboutText = nil
}
if let aboutText = aboutText {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.PeerInfo_GroupAboutItem, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPublicBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
interaction.requestLayout() interaction.requestLayout()
})) }))
} }
@ -1266,6 +1292,9 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
interaction.editingOpenPublicLinkSetup() interaction.editingOpenPublicLinkSetup()
})) }))
}
if channel.hasPermission(.inviteMembers) {
let invitesText: String let invitesText: String
if let count = data.invitations?.count, count > 0 { if let count = data.invitations?.count, count > 0 {
invitesText = "\(count)" invitesText = "\(count)"
@ -1276,7 +1305,9 @@ private func editingItems(data: PeerInfoScreenData?, context: AccountContext, pr
items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, action: { items[.peerPublicSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemInviteLinks, label: .text(invitesText), text: presentationData.strings.GroupInfo_InviteLinks, action: {
interaction.editingOpenInviteLinksSetup() interaction.editingOpenInviteLinksSetup()
})) }))
}
if cachedData.flags.contains(.canChangeUsername) {
if let linkedDiscussionPeer = data.linkedDiscussionPeer { if let linkedDiscussionPeer = data.linkedDiscussionPeer {
let peerTitle: String let peerTitle: String
if let addressName = linkedDiscussionPeer.addressName, !addressName.isEmpty { if let addressName = linkedDiscussionPeer.addressName, !addressName.isEmpty {
@ -4425,7 +4456,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
if canCreateInviteLink { if canCreateInviteLink {
options.append(ContactListAdditionalOption(title: presentationData.strings.GroupInfo_InviteByLink, icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: { options.append(ContactListAdditionalOption(title: presentationData.strings.GroupInfo_InviteByLink, icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: {
createInviteLinkImpl?() createInviteLinkImpl?()
})) }, clearHighlightAutomatically: true))
} }
let contactsController: ViewController let contactsController: ViewController