Avatar story indicator counters

This commit is contained in:
Ali 2023-06-24 15:25:11 +03:00
parent 94e6f28efe
commit a0817a831b
6 changed files with 105 additions and 31 deletions

View File

@ -2795,7 +2795,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
hasUnseen: displayStoryIndicator, hasUnseen: displayStoryIndicator,
isDarkTheme: item.presentationData.theme.overallDarkAppearance, isDarkTheme: item.presentationData.theme.overallDarkAppearance,
activeLineWidth: 2.0, activeLineWidth: 2.0,
inactiveLineWidth: 1.0 + UIScreenPixel inactiveLineWidth: 1.0 + UIScreenPixel,
counters: nil
)), )),
environment: {}, environment: {},
containerSize: indicatorFrame.size containerSize: indicatorFrame.size

View File

@ -201,8 +201,10 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
})] })]
} }
var hasUnseenStories: Bool? var storyStats: (total: Int, unseen: Int)?
if let storyData = storyData { if let storyData = storyData {
storyStats = (storyData.count, storyData.unseenCount)
let text: String let text: String
//TODO:localize //TODO:localize
if storyData.unseenCount != 0 { if storyData.unseenCount != 0 {
@ -219,12 +221,11 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
} }
} }
status = .custom(string: text, multiline: false, isActive: false, icon: nil) status = .custom(string: text, multiline: false, isActive: false, icon: nil)
hasUnseenStories = storyData.unseenCount != 0
} }
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, selectionPosition: .left, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in
interaction.openPeer(peer, .generic) interaction.openPeer(peer, .generic)
}, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, hasUnseenStories: hasUnseenStories, openStories: { peer, sourceNode in }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: storyStats, openStories: { peer, sourceNode in
if case let .peer(peerValue, _) = peer, let peerValue { if case let .peer(peerValue, _) = peer, let peerValue {
interaction.openStories(peerValue, sourceNode) interaction.openStories(peerValue, sourceNode)
} }

View File

@ -180,7 +180,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
let arrowAction: (() -> Void)? let arrowAction: (() -> Void)?
let animationCache: AnimationCache? let animationCache: AnimationCache?
let animationRenderer: MultiAnimationRenderer? let animationRenderer: MultiAnimationRenderer?
let hasUnseenStories: Bool? let storyStats: (total: Int, unseen: Int)?
let openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? let openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)?
public let selectable: Bool public let selectable: Bool
@ -217,7 +217,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, arrowAction: (() -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, arrowAction: (() -> Void)? = nil,
animationCache: AnimationCache? = nil, animationCache: AnimationCache? = nil,
animationRenderer: MultiAnimationRenderer? = nil, animationRenderer: MultiAnimationRenderer? = nil,
hasUnseenStories: Bool? = nil, storyStats: (total: Int, unseen: Int)? = nil,
openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? = nil openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? = nil
) { ) {
self.presentationData = presentationData self.presentationData = presentationData
@ -248,7 +248,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
self.arrowAction = arrowAction self.arrowAction = arrowAction
self.animationCache = animationCache self.animationCache = animationCache
self.animationRenderer = animationRenderer self.animationRenderer = animationRenderer
self.hasUnseenStories = hasUnseenStories self.storyStats = storyStats
self.openStories = openStories self.openStories = openStories
if let index = index { if let index = index {
@ -1088,7 +1088,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
var avatarScale: CGFloat = 1.0 var avatarScale: CGFloat = 1.0
if item.hasUnseenStories != nil { if item.storyStats != nil {
avatarScale *= (avatarFrame.width - 2.0 * 2.0) / avatarFrame.width avatarScale *= (avatarFrame.width - 2.0 * 2.0) / avatarFrame.width
} }
@ -1096,7 +1096,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let storyIndicatorScale: CGFloat = 1.0 let storyIndicatorScale: CGFloat = 1.0
if let displayStoryIndicator = item.hasUnseenStories { if let storyStats = item.storyStats {
var indicatorTransition = Transition(transition) var indicatorTransition = Transition(transition)
let avatarStoryIndicator: ComponentView<Empty> let avatarStoryIndicator: ComponentView<Empty>
if let current = strongSelf.avatarStoryIndicator { if let current = strongSelf.avatarStoryIndicator {
@ -1113,10 +1113,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let _ = avatarStoryIndicator.update( let _ = avatarStoryIndicator.update(
transition: indicatorTransition, transition: indicatorTransition,
component: AnyComponent(AvatarStoryIndicatorComponent( component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: displayStoryIndicator, hasUnseen: storyStats.unseen != 0,
isDarkTheme: item.presentationData.theme.overallDarkAppearance, isDarkTheme: item.presentationData.theme.overallDarkAppearance,
activeLineWidth: 1.0 + UIScreenPixel, activeLineWidth: 1.0 + UIScreenPixel,
inactiveLineWidth: 1.0 + UIScreenPixel inactiveLineWidth: 1.0 + UIScreenPixel,
counters: AvatarStoryIndicatorComponent.Counters(totalCount: storyStats.total, unseenCount: storyStats.unseen)
)), )),
environment: {}, environment: {},
containerSize: indicatorFrame.size containerSize: indicatorFrame.size

View File

@ -215,7 +215,8 @@ public final class ChatAvatarNavigationNode: ASDisplayNode {
hasUnseen: hasUnseenStories, hasUnseen: hasUnseenStories,
isDarkTheme: theme.overallDarkAppearance, isDarkTheme: theme.overallDarkAppearance,
activeLineWidth: 1.0, activeLineWidth: 1.0,
inactiveLineWidth: 1.0 inactiveLineWidth: 1.0,
counters: nil
)), )),
environment: {}, environment: {},
containerSize: self.avatarNode.bounds.insetBy(dx: 2.0, dy: 2.0).size containerSize: self.avatarNode.bounds.insetBy(dx: 2.0, dy: 2.0).size

View File

@ -5,21 +5,34 @@ import ComponentFlow
import TelegramPresentationData import TelegramPresentationData
public final class AvatarStoryIndicatorComponent: Component { public final class AvatarStoryIndicatorComponent: Component {
public struct Counters: Equatable {
public var totalCount: Int
public var unseenCount: Int
public init(totalCount: Int, unseenCount: Int) {
self.totalCount = totalCount
self.unseenCount = unseenCount
}
}
public let hasUnseen: Bool public let hasUnseen: Bool
public let isDarkTheme: Bool public let isDarkTheme: Bool
public let activeLineWidth: CGFloat public let activeLineWidth: CGFloat
public let inactiveLineWidth: CGFloat public let inactiveLineWidth: CGFloat
public let counters: Counters?
public init( public init(
hasUnseen: Bool, hasUnseen: Bool,
isDarkTheme: Bool, isDarkTheme: Bool,
activeLineWidth: CGFloat, activeLineWidth: CGFloat,
inactiveLineWidth: CGFloat inactiveLineWidth: CGFloat,
counters: Counters?
) { ) {
self.hasUnseen = hasUnseen self.hasUnseen = hasUnseen
self.isDarkTheme = isDarkTheme self.isDarkTheme = isDarkTheme
self.activeLineWidth = activeLineWidth self.activeLineWidth = activeLineWidth
self.inactiveLineWidth = inactiveLineWidth self.inactiveLineWidth = inactiveLineWidth
self.counters = counters
} }
public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool { public static func ==(lhs: AvatarStoryIndicatorComponent, rhs: AvatarStoryIndicatorComponent) -> Bool {
@ -35,6 +48,9 @@ public final class AvatarStoryIndicatorComponent: Component {
if lhs.inactiveLineWidth != rhs.inactiveLineWidth { if lhs.inactiveLineWidth != rhs.inactiveLineWidth {
return false return false
} }
if lhs.counters != rhs.counters {
return false
}
return true return true
} }
@ -76,7 +92,59 @@ public final class AvatarStoryIndicatorComponent: Component {
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setLineWidth(lineWidth) context.setLineWidth(lineWidth)
if let counters = component.counters, counters.totalCount > 1 {
let center = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
let radius = (diameter - lineWidth) * 0.5
let spacing: CGFloat = 2.0
let angularSpacing: CGFloat = spacing / radius
let circleLength = CGFloat.pi * 2.0 * radius
let segmentLength = (circleLength - spacing * CGFloat(counters.totalCount)) / CGFloat(counters.totalCount)
let segmentAngle = segmentLength / radius
for pass in 0 ..< 2 {
context.resetClip()
let startIndex: Int
let endIndex: Int
if pass == 0 {
startIndex = 0
endIndex = counters.totalCount - counters.unseenCount
} else {
startIndex = counters.totalCount - counters.unseenCount
endIndex = counters.totalCount
}
if startIndex < endIndex {
for i in startIndex ..< endIndex {
let startAngle = CGFloat(i) * (angularSpacing + segmentAngle) - CGFloat.pi * 0.5 + angularSpacing * 0.5
context.move(to: CGPoint(x: center.x + cos(startAngle) * radius, y: center.y + sin(startAngle) * radius))
context.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: startAngle + segmentAngle, clockwise: false)
}
context.replacePathWithStrokedPath()
context.clip()
var locations: [CGFloat] = [1.0, 0.0]
let colors: [CGColor]
if pass == 1 {
colors = [UIColor(rgb: 0x34C76F).cgColor, UIColor(rgb: 0x3DA1FD).cgColor]
} else {
if component.isDarkTheme {
colors = [UIColor(rgb: 0x48484A).cgColor, UIColor(rgb: 0x48484A).cgColor]
} else {
colors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
}
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
}
}
} else {
context.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) context.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5))
context.replacePathWithStrokedPath() context.replacePathWithStrokedPath()
context.clip() context.clip()
@ -96,6 +164,7 @@ public final class AvatarStoryIndicatorComponent: Component {
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
}
}) })
transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(x: (availableSize.width - imageDiameter) * 0.5, y: (availableSize.height - imageDiameter) * 0.5), size: CGSize(width: imageDiameter, height: imageDiameter))) transition.setFrame(view: self.indicatorView, frame: CGRect(origin: CGPoint(x: (availableSize.width - imageDiameter) * 0.5, y: (availableSize.height - imageDiameter) * 0.5), size: CGSize(width: imageDiameter, height: imageDiameter)))

View File

@ -470,7 +470,8 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
hasUnseen: hasUnseenStories, hasUnseen: hasUnseenStories,
isDarkTheme: theme.overallDarkAppearance, isDarkTheme: theme.overallDarkAppearance,
activeLineWidth: 3.0, activeLineWidth: 3.0,
inactiveLineWidth: 2.0 inactiveLineWidth: 2.0,
counters: nil
)), )),
environment: {}, environment: {},
containerSize: self.avatarNode.bounds.size containerSize: self.avatarNode.bounds.size