mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Various improvements
This commit is contained in:
parent
efb828e61b
commit
e4e5142a96
21 changed files with 176 additions and 332 deletions
19
.vscode/launch.json
vendored
19
.vscode/launch.json
vendored
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Telegram",
|
||||
"type": "lldb-dap",
|
||||
"request": "attach",
|
||||
"preLaunchTask": "_launch_telegram",
|
||||
"debuggerRoot": "${workspaceFolder}",
|
||||
"attachCommands": [
|
||||
"command script import '${workspaceFolder}/scripts/lldb_attach.py'"
|
||||
],
|
||||
"terminateCommands": [
|
||||
"command script import '${workspaceFolder}/scripts/lldb_kill_app.py'"
|
||||
],
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"timeout": 9999
|
||||
}
|
||||
]
|
||||
}
|
||||
110
.vscode/tasks.json
vendored
110
.vscode/tasks.json
vendored
|
|
@ -1,110 +0,0 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Select Simulator for Apple Development",
|
||||
"type": "shell",
|
||||
"command": "./scripts/select_simulator.sh",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"focus": true,
|
||||
"panel": "dedicated"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Build Telegram",
|
||||
"type": "shell",
|
||||
"command": "./scripts/lldb_build.sh",
|
||||
"options": {
|
||||
"env": {
|
||||
"BAZEL_LABEL_TO_RUN": "//Telegram:Telegram",
|
||||
"BAZEL_EXTRA_BUILD_FLAGS": ""
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"kind": "build"
|
||||
},
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "bazel",
|
||||
"source": "bazel",
|
||||
"fileLocation": [
|
||||
"relative",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"pattern": {
|
||||
"regexp": "^(.+?):(\\d+):(\\d+):\\s+(error|warning|note):\\s+(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "_launch_telegram",
|
||||
"type": "shell",
|
||||
"command": "./scripts/lldb_launch_and_debug.sh",
|
||||
"options": {
|
||||
"env": {
|
||||
"BAZEL_LABEL_TO_RUN": "//Telegram:Telegram",
|
||||
"BAZEL_EXTRA_BUILD_FLAGS": "",
|
||||
"BAZEL_LAUNCH_ARGS": ""
|
||||
}
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"hide": true,
|
||||
"isBackground": true,
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "bazel",
|
||||
"source": "bazel",
|
||||
"fileLocation": [
|
||||
"relative",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"pattern": {
|
||||
"regexp": "launcher_error in (.*): (.*)",
|
||||
"kind": "file",
|
||||
"file": 1,
|
||||
"message": 2
|
||||
},
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "^Starting launch task\\.\\.\\.$",
|
||||
"endsPattern": "^ph.telegra.Telegraph: .*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"owner": "bazel",
|
||||
"source": "bazel",
|
||||
"fileLocation": [
|
||||
"relative",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"pattern": {
|
||||
"regexp": "^(.+?):(\\d+):(\\d+):\\s+(error|warning|note):\\s+(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5
|
||||
},
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "^Starting launch task\\.\\.\\.$",
|
||||
"endsPattern": "^ph.telegra.Telegraph: .*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -106,6 +106,16 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||
if !message.text.isEmpty {
|
||||
messageText = message.text
|
||||
messageEntities = message._asMessage().textEntitiesAttribute?.entities ?? []
|
||||
for entity in messageEntities {
|
||||
if case let .CustomEmoji(_, fileId) = entity.type {
|
||||
if customEmojiRanges == nil {
|
||||
customEmojiRanges = []
|
||||
}
|
||||
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile)
|
||||
customEmojiRanges?.append((range, attribute))
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1215,7 +1215,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
playbackControlsIsSeekable = seekable
|
||||
playbackControlsIsPlaying = !paused
|
||||
}
|
||||
if !self.areControlsVisible || hideControls {
|
||||
if !self.controlsVisibility() || hideControls {
|
||||
playbackControlsIsVisible = false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1295,6 +1295,10 @@ public final class MessageHistoryView: PostboxView {
|
|||
}
|
||||
|
||||
init(_ mutableView: MutableMessageHistoryView) {
|
||||
if case .external = mutableView.peerIds, mutableView.tag == nil, case .not = mutableView.namespaces {
|
||||
assert(true)
|
||||
}
|
||||
|
||||
self.tag = mutableView.tag
|
||||
self.namespaces = mutableView.namespaces
|
||||
self.isAddedToChatList = mutableView.isAddedToChatList
|
||||
|
|
|
|||
|
|
@ -180,7 +180,8 @@ public final class ChatFloatingTopicsPanel: Component {
|
|||
}
|
||||
}
|
||||
|
||||
if case .top = component.location {
|
||||
switch component.location {
|
||||
case .top, .bottom:
|
||||
let topPanel: ComponentView<ChatSidePanelEnvironment>
|
||||
var topPanelTransition = transition
|
||||
if let current = self.topPanel {
|
||||
|
|
@ -196,7 +197,7 @@ public final class ChatFloatingTopicsPanel: Component {
|
|||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
location: .top,
|
||||
location: component.location == .top ? .top : .bottom,
|
||||
peerId: component.peerId,
|
||||
kind: component.kind,
|
||||
topicId: component.topicId,
|
||||
|
|
@ -232,13 +233,15 @@ public final class ChatFloatingTopicsPanel: Component {
|
|||
}
|
||||
transition.setFrame(view: topPanelView, frame: topPanelFrame)
|
||||
}
|
||||
} else if let topPanel = self.topPanel {
|
||||
self.topPanel = nil
|
||||
if let topPanelView = topPanel.view as? ChatSideTopicsPanel.View {
|
||||
topPanelView.clipsToBounds = true
|
||||
transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: 16.0 + 72.0, height: topPanelView.bounds.height)), completion: { [weak topPanelView] _ in
|
||||
topPanelView?.removeFromSuperview()
|
||||
})
|
||||
default:
|
||||
if let topPanel = self.topPanel {
|
||||
self.topPanel = nil
|
||||
if let topPanelView = topPanel.view as? ChatSideTopicsPanel.View {
|
||||
topPanelView.clipsToBounds = true
|
||||
transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 8.0, y: 0.0), size: CGSize(width: 16.0 + 72.0, height: topPanelView.bounds.height)), completion: { [weak topPanelView] _ in
|
||||
topPanelView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -291,11 +294,17 @@ public final class ChatFloatingTopicsPanel: Component {
|
|||
}
|
||||
|
||||
public final class ChatTopicsHeaderPanelComponent: Component {
|
||||
public enum Location {
|
||||
case top
|
||||
case bottom
|
||||
}
|
||||
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let peerId: EnginePeer.Id
|
||||
public let kind: ChatSideTopicsPanel.Kind
|
||||
public let location: Location
|
||||
public let topicId: Int64?
|
||||
public let controller: () -> ViewController?
|
||||
public let togglePanel: () -> Void
|
||||
|
|
@ -308,6 +317,7 @@ public final class ChatTopicsHeaderPanelComponent: Component {
|
|||
strings: PresentationStrings,
|
||||
peerId: EnginePeer.Id,
|
||||
kind: ChatSideTopicsPanel.Kind,
|
||||
location: Location,
|
||||
topicId: Int64?,
|
||||
controller: @escaping () -> ViewController?,
|
||||
togglePanel: @escaping () -> Void,
|
||||
|
|
@ -319,6 +329,7 @@ public final class ChatTopicsHeaderPanelComponent: Component {
|
|||
self.strings = strings
|
||||
self.peerId = peerId
|
||||
self.kind = kind
|
||||
self.location = location
|
||||
self.topicId = topicId
|
||||
self.controller = controller
|
||||
self.togglePanel = togglePanel
|
||||
|
|
@ -342,6 +353,9 @@ public final class ChatTopicsHeaderPanelComponent: Component {
|
|||
if lhs.kind != rhs.kind {
|
||||
return false
|
||||
}
|
||||
if lhs.location != rhs.location {
|
||||
return false
|
||||
}
|
||||
if lhs.topicId != rhs.topicId {
|
||||
return false
|
||||
}
|
||||
|
|
@ -371,7 +385,7 @@ public final class ChatTopicsHeaderPanelComponent: Component {
|
|||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
location: .top,
|
||||
location: component.location == .top ? .top : .bottom,
|
||||
peerId: component.peerId,
|
||||
kind: component.kind,
|
||||
topicId: component.topicId,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
public enum Location {
|
||||
case side
|
||||
case top
|
||||
case bottom
|
||||
}
|
||||
|
||||
public enum Kind: Equatable {
|
||||
|
|
@ -1105,11 +1106,20 @@ public final class ChatSideTopicsPanel: Component {
|
|||
}
|
||||
}
|
||||
|
||||
let normalIconName: String
|
||||
switch location {
|
||||
case .top:
|
||||
normalIconName = "Chat/Title Panels/SidebarIcon"
|
||||
case .side:
|
||||
normalIconName = "Chat/Title Panels/SidebarBottomIcon"
|
||||
case .bottom:
|
||||
normalIconName = "Chat/Title Panels/SidebarTopIcon"
|
||||
}
|
||||
let iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: isReordering ? "Media Editor/Done" : "Chat/Title Panels/SidebarIcon",
|
||||
tintColor: location == .side ? theme.rootController.navigationBar.accentTextColor : theme.chat.inputPanel.panelControlColor,
|
||||
name: isReordering ? "Media Editor/Done" : normalIconName,
|
||||
tintColor: theme.chat.inputPanel.panelControlColor,
|
||||
maxSize: CGSize(width: 24.0, height: 24.0),
|
||||
scaleFactor: 1.0
|
||||
)),
|
||||
|
|
@ -1519,7 +1529,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
switch component.location {
|
||||
case .side:
|
||||
transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(-globalOffset, 0.0, 0.0))
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
transition.setTransform(view: tabItemView, transform: CATransform3DMakeTranslation(0.0, -globalOffset, 0.0))
|
||||
}
|
||||
}
|
||||
|
|
@ -1560,7 +1570,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
switch component.location {
|
||||
case .side:
|
||||
selectedItemFrame = item.frame
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
selectedItemFrame = CGRect(origin: CGPoint(x: item.frame.minX + 5.0, y: item.frame.minY), size: CGSize(width: item.frame.width - 4.0 - 11.0, height: item.frame.height))
|
||||
}
|
||||
}
|
||||
|
|
@ -1568,7 +1578,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
switch component.location {
|
||||
case .side:
|
||||
beforePinnedItemsPosition = item.frame.maxY
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
beforePinnedItemsPosition = item.frame.maxX
|
||||
}
|
||||
}
|
||||
|
|
@ -1597,7 +1607,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
switch component.location {
|
||||
case .side:
|
||||
beforePinnedItemsPosition = item.frame.maxY
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
beforePinnedItemsPosition = item.frame.maxX
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1605,7 +1615,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
switch component.location {
|
||||
case .side:
|
||||
afterPinnedItemsPosition = item.frame.minY
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
afterPinnedItemsPosition = item.frame.minX
|
||||
}
|
||||
}
|
||||
|
|
@ -1622,7 +1632,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
switch component.location {
|
||||
case .side:
|
||||
afterPinnedItemsPosition = listView.bounds.height + 500.0
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
afterPinnedItemsPosition = listView.bounds.width + 500.0
|
||||
}
|
||||
}
|
||||
|
|
@ -1638,7 +1648,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
switch component.location {
|
||||
case .side:
|
||||
selectedLineFrame = CGRect(origin: CGPoint(x: 0.0, y: selectedItemFrame.minY), size: CGSize(width: 4.0, height: selectedItemFrame.height))
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
selectedLineFrame = CGRect(origin: CGPoint(x: selectedItemFrame.minX, y: listView.frame.maxY - 3.0), size: CGSize(width: selectedItemFrame.width, height: 3.0))
|
||||
}
|
||||
|
||||
|
|
@ -1658,7 +1668,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
switch component.location {
|
||||
case .side:
|
||||
pinnedItemsBackgroundFrame = CGRect(origin: CGPoint(x: 5.0, y: beforePinnedItemsPosition), size: CGSize(width: listView.bounds.width - 5.0 - 4.0, height: afterPinnedItemsPosition - beforePinnedItemsPosition))
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
pinnedItemsBackgroundFrame = CGRect(origin: CGPoint(x: beforePinnedItemsPosition, y: 4.0), size: CGSize(width: afterPinnedItemsPosition - beforePinnedItemsPosition, height: listView.bounds.height - 5.0 - 4.0))
|
||||
}
|
||||
self.pinnedBackgroundContainer.updatePosition(position: pinnedItemsBackgroundFrame.origin, transition: pinnedItemsTransition)
|
||||
|
|
@ -1754,7 +1764,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
image.draw(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: image.size.width)), blendMode: .destinationOut, alpha: 1.0)
|
||||
}
|
||||
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 1, topCapHeight: Int(cornerRadius) + 1)
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
self.scrollViewMask.image = generateImage(CGSize(width: 8.0 + 1.0, height: 1.0), rotatedContext: { size, context in
|
||||
UIGraphicsPushContext(context)
|
||||
defer {
|
||||
|
|
@ -1785,7 +1795,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
context.setFillColor(component.theme.rootController.navigationBar.accentTextColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - size.height, y: 0.0), size: CGSize(width: size.height, height: size.height)))
|
||||
})?.stretchableImage(withLeftCapWidth: 1, topCapHeight: 4)
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
self.selectedLineView.image = generateImage(CGSize(width: 4.0, height: 3.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(component.theme.rootController.navigationBar.accentTextColor.cgColor)
|
||||
|
|
@ -1817,7 +1827,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
switch component.location {
|
||||
case .side:
|
||||
directionContainerInset = containerInsets.top
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
directionContainerInset = containerInsets.left
|
||||
}
|
||||
|
||||
|
|
@ -1876,7 +1886,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
case .side:
|
||||
itemFrame = CGRect(origin: CGPoint(x: 8.0 + 4.0, y: directionContainerInset + 6.0), size: itemSize)
|
||||
directionContainerInset += itemSize.height
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
itemFrame = CGRect(origin: CGPoint(x: 12.0, y: 6.0), size: itemSize)
|
||||
directionContainerInset += itemSize.width - 20.0
|
||||
}
|
||||
|
|
@ -1900,7 +1910,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
scrollSize = CGSize(width: availableSize.width, height: availableSize.height - directionContainerInset - environment.insets.top - containerInsets.bottom - additionalInsets.top - additionalInsets.bottom)
|
||||
scrollFrame = CGRect(origin: CGPoint(x: additionalInsets.left, y: directionContainerInset + environment.insets.top + additionalInsets.top), size: scrollSize)
|
||||
listContentInsets = UIEdgeInsets(top: 8.0, left: 0.0, bottom: 8.0, right: 0.0)
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
additionalInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 0.0, right: 8.0)
|
||||
scrollSize = CGSize(width: availableSize.width - additionalInsets.left - additionalInsets.right - directionContainerInset, height: availableSize.height - additionalInsets.top)
|
||||
scrollFrame = CGRect(origin: CGPoint(x: additionalInsets.left + directionContainerInset, y: additionalInsets.top), size: scrollSize)
|
||||
|
|
@ -1940,7 +1950,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
}
|
||||
)))
|
||||
)
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
listItems.append(AnyComponentWithIdentity(
|
||||
id: ScrollId.all,
|
||||
component: AnyComponent(HorizontalAllItemComponent(
|
||||
|
|
@ -2205,7 +2215,7 @@ public final class ChatSideTopicsPanel: Component {
|
|||
contextGesture: itemContextGesture
|
||||
)))
|
||||
)
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
listItems.append(AnyComponentWithIdentity(
|
||||
id: scrollId,
|
||||
component: AnyComponent(HorizontalItemComponent(
|
||||
|
|
|
|||
|
|
@ -756,7 +756,7 @@ public final class ContextControllerActionsListStackItem: ContextControllerActio
|
|||
|
||||
private var tip: ContextController.Tip?
|
||||
private var tipDisposable: Disposable?
|
||||
private var tipNode: InnerTextSelectionTipContainerNode?
|
||||
private(set) var tipNode: InnerTextSelectionTipContainerNode?
|
||||
private var tipSeparatorNode: ContextControllerActionsListSeparatorItemNode?
|
||||
|
||||
private var hapticFeedback: HapticFeedback?
|
||||
|
|
@ -2378,6 +2378,9 @@ public final class ContextControllerActionsStackNodeImpl: ASDisplayNode, Context
|
|||
if let tipNode = itemContainer.tipNode {
|
||||
tipNode.animateIn()
|
||||
}
|
||||
if let topNode = itemContainer.node as? ContextControllerActionsListStackItem.Node {
|
||||
topNode.tipNode?.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -179,31 +179,29 @@ final class LegacyGlassView: UIView {
|
|||
let displacementMagnitudeV = displacementMagnitudePoints / size.height
|
||||
let outerEdgeDistance = 2.0
|
||||
|
||||
if let displacementMap = generateDisplacementMap(size: size, cornerRadius: cornerRadius, edgeDistance: min(12.0, cornerRadius), scale: 1.0) {
|
||||
let meshTransform = generateGlassMeshFromDisplacementMap(
|
||||
size: size,
|
||||
cornerRadius: cornerRadius,
|
||||
displacementMap: displacementMap,
|
||||
displacementMagnitudeU: displacementMagnitudeU,
|
||||
displacementMagnitudeV: displacementMagnitudeV,
|
||||
cornerResolution: 12,
|
||||
outerEdgeDistance: outerEdgeDistance,
|
||||
bezier: DisplacementBezier(
|
||||
x1: 0.816137566137566,
|
||||
y1: 0.20502645502645533,
|
||||
x2: 0.5806878306878306,
|
||||
y2: 0.873015873015873
|
||||
)
|
||||
).mesh.makeValue()
|
||||
|
||||
if let meshTransform {
|
||||
if !transition.animation.isImmediate, let previousTransform = backdropLayer.value(forKey: "meshTransform") as? NSObject {
|
||||
backdropLayer.removeAnimation(forKey: "meshTransform")
|
||||
backdropLayer.setValue(meshTransform, forKey: "meshTransform")
|
||||
transition.animateMeshTransform(layer: backdropLayer, from: previousTransform, to: meshTransform)
|
||||
} else {
|
||||
backdropLayer.setValue(meshTransform, forKey: "meshTransform")
|
||||
}
|
||||
let meshTransform = generateGlassMesh(
|
||||
size: size,
|
||||
cornerRadius: cornerRadius,
|
||||
edgeDistance: min(12.0, cornerRadius),
|
||||
displacementMagnitudeU: displacementMagnitudeU,
|
||||
displacementMagnitudeV: displacementMagnitudeV,
|
||||
cornerResolution: 12,
|
||||
outerEdgeDistance: outerEdgeDistance,
|
||||
bezier: DisplacementBezier(
|
||||
x1: 0.816137566137566,
|
||||
y1: 0.20502645502645533,
|
||||
x2: 0.5806878306878306,
|
||||
y2: 0.873015873015873
|
||||
)
|
||||
).mesh.makeValue()
|
||||
|
||||
if let meshTransform {
|
||||
if !transition.animation.isImmediate, let previousTransform = backdropLayer.value(forKey: "meshTransform") as? NSObject {
|
||||
backdropLayer.removeAnimation(forKey: "meshTransform")
|
||||
backdropLayer.setValue(meshTransform, forKey: "meshTransform")
|
||||
transition.animateMeshTransform(layer: backdropLayer, from: previousTransform, to: meshTransform)
|
||||
} else {
|
||||
backdropLayer.setValue(meshTransform, forKey: "meshTransform")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ public final class HeaderPanelContainerComponent: Component {
|
|||
}
|
||||
panelCount += component.panels.count
|
||||
var cornerRadius: CGFloat = 0.0
|
||||
if panelCount == 1 {
|
||||
if panelCount == 1 && backgroundFrame.height <= 50.0 * 2.0 {
|
||||
cornerRadius = backgroundFrame.height * 0.5
|
||||
} else {
|
||||
cornerRadius = 20.0
|
||||
|
|
|
|||
|
|
@ -132,123 +132,39 @@ public func roundedRectGradient(x: CGFloat, y: CGFloat, width: CGFloat, height:
|
|||
return (nx, ny)
|
||||
}
|
||||
|
||||
/// Generates a displacement map image as a signed distance field from rounded rect edges.
|
||||
/// - edgeDistance: The distance (in points) over which displacement is applied
|
||||
/// - R channel: X displacement (127 = neutral, 0 = max left, 255 = max right)
|
||||
/// - G channel: Y displacement (127 = neutral, 0 = max up, 255 = max down)
|
||||
/// - B channel: Unused (always 0)
|
||||
/// Displacement is maximum at the edge and fades linearly to zero at edgeDistance.
|
||||
/// Actual displacement magnitude is applied when sampling the map.
|
||||
public func generateDisplacementMap(size: CGSize, cornerRadius: CGFloat, edgeDistance: CGFloat, scale: CGFloat) -> CGImage? {
|
||||
let width = Int(size.width * scale)
|
||||
let height = Int(size.height * scale)
|
||||
|
||||
// Clamp corner radius
|
||||
let maxCornerRadius = min(size.width, size.height) / 2.0
|
||||
let clampedRadius = min(cornerRadius, maxCornerRadius)
|
||||
|
||||
// Create bitmap context
|
||||
var pixelData = [UInt8](repeating: 0, count: width * height * 4)
|
||||
|
||||
for py in 0 ..< height {
|
||||
for px in 0 ..< width {
|
||||
// Convert pixel to point coordinates
|
||||
let x = CGFloat(px) / scale
|
||||
let y = CGFloat(py) / scale
|
||||
|
||||
// Get signed distance (negative inside, positive outside)
|
||||
let sdf = roundedRectSDF(x: x, y: y, width: size.width, height: size.height, cornerRadius: clampedRadius)
|
||||
|
||||
// Get gradient (outward normal direction)
|
||||
let (nx, ny) = roundedRectGradient(x: x, y: y, width: size.width, height: size.height, cornerRadius: clampedRadius)
|
||||
|
||||
// Inward normal (content moves away from edge, toward center)
|
||||
let inwardX = -nx
|
||||
let inwardY = -ny
|
||||
|
||||
// Distance from edge (positive inside the shape)
|
||||
let distFromEdge = -sdf
|
||||
|
||||
// Weight: 1 at edge, 0 at edgeDistance (linear falloff)
|
||||
let weight = max(0, min(1, 1.0 - distFromEdge / edgeDistance))
|
||||
|
||||
// Displacement modulated by distance from edge
|
||||
let displacementX = inwardX * weight
|
||||
let displacementY = inwardY * weight
|
||||
|
||||
// Encode in R/G: 127 = neutral, map -1..1 to 0..254
|
||||
let r = UInt8(max(0, min(255, Int(127 + displacementX * 127))))
|
||||
let g = UInt8(max(0, min(255, Int(127 + displacementY * 127))))
|
||||
|
||||
let idx = (py * width + px) * 4
|
||||
pixelData[idx + 0] = r // X displacement
|
||||
pixelData[idx + 1] = g // Y displacement
|
||||
pixelData[idx + 2] = 0 // Unused
|
||||
pixelData[idx + 3] = 255 // A
|
||||
}
|
||||
}
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
guard let context = CGContext(
|
||||
data: &pixelData,
|
||||
width: width,
|
||||
height: height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: width * 4,
|
||||
space: colorSpace,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return context.makeImage()
|
||||
}
|
||||
|
||||
/// Samples displacement from a displacement map with bilinear interpolation and bezier easing
|
||||
/// Computes displacement at a point analytically using the rounded rect SDF.
|
||||
/// - Parameters:
|
||||
/// - x, y: Coordinates in the displacement map's pixel space
|
||||
/// - pixels: Pointer to displacement map pixel data
|
||||
/// - width, height: Displacement map dimensions
|
||||
/// - bytesPerRow, bytesPerPixel: Displacement map layout
|
||||
/// - bezier: Bezier control points for easing curve
|
||||
/// - Returns: Displacement (dx, dy) in range -1..1 with bezier easing applied
|
||||
public func sampleDisplacement(
|
||||
/// - x, y: Point coordinates in the shape's coordinate space
|
||||
/// - width, height: Dimensions of the rounded rectangle
|
||||
/// - cornerRadius: Already-clamped corner radius
|
||||
/// - edgeDistance: Distance (in points) over which displacement fades from edge inward
|
||||
/// - bezier: Bezier control points for easing the displacement magnitude
|
||||
/// - Returns: (dx, dy) displacement in range -1..1 with bezier easing, plus the raw SDF value
|
||||
public func computeDisplacement(
|
||||
x: CGFloat,
|
||||
y: CGFloat,
|
||||
pixels: UnsafePointer<UInt8>,
|
||||
width: Int,
|
||||
height: Int,
|
||||
bytesPerRow: Int,
|
||||
bytesPerPixel: Int,
|
||||
width: CGFloat,
|
||||
height: CGFloat,
|
||||
cornerRadius: CGFloat,
|
||||
edgeDistance: CGFloat,
|
||||
bezier: DisplacementBezier
|
||||
) -> (dx: CGFloat, dy: CGFloat) {
|
||||
let clampedX = max(0, min(CGFloat(width - 1), x))
|
||||
let clampedY = max(0, min(CGFloat(height - 1), y))
|
||||
) -> (dx: CGFloat, dy: CGFloat, sdf: CGFloat) {
|
||||
let sdf = roundedRectSDF(x: x, y: y, width: width, height: height, cornerRadius: cornerRadius)
|
||||
let (nx, ny) = roundedRectGradient(x: x, y: y, width: width, height: height, cornerRadius: cornerRadius)
|
||||
|
||||
let x0 = Int(clampedX)
|
||||
let y0 = Int(clampedY)
|
||||
let x1 = min(x0 + 1, width - 1)
|
||||
let y1 = min(y0 + 1, height - 1)
|
||||
// Inward normal (content moves away from edge, toward center)
|
||||
let inwardX = -nx
|
||||
let inwardY = -ny
|
||||
|
||||
let fx = clampedX - CGFloat(x0)
|
||||
let fy = clampedY - CGFloat(y0)
|
||||
// Distance from edge (positive inside the shape)
|
||||
let distFromEdge = -sdf
|
||||
|
||||
func sample(_ sx: Int, _ sy: Int) -> (r: CGFloat, g: CGFloat) {
|
||||
let offset = sy * bytesPerRow + sx * bytesPerPixel
|
||||
return (CGFloat(pixels[offset + 0]), CGFloat(pixels[offset + 1]))
|
||||
}
|
||||
// Weight: 1 at edge, 0 at edgeDistance inward (linear falloff)
|
||||
let weight = max(0, min(1, 1.0 - distFromEdge / edgeDistance))
|
||||
|
||||
let c00 = sample(x0, y0)
|
||||
let c10 = sample(x1, y0)
|
||||
let c01 = sample(x0, y1)
|
||||
let c11 = sample(x1, y1)
|
||||
|
||||
let r = (c00.r * (1 - fx) + c10.r * fx) * (1 - fy) + (c01.r * (1 - fx) + c11.r * fx) * fy
|
||||
let g = (c00.g * (1 - fx) + c10.g * fx) * (1 - fy) + (c01.g * (1 - fx) + c11.g * fx) * fy
|
||||
|
||||
// Decode: 127 = neutral, map 0..254 to -1..1
|
||||
var dx = (r - 127.0) / 127.0
|
||||
var dy = (g - 127.0) / 127.0
|
||||
// Displacement direction modulated by distance
|
||||
var dx = inwardX * weight
|
||||
var dy = inwardY * weight
|
||||
|
||||
// Apply bezier easing to vector magnitude, preserving direction
|
||||
let mag = hypot(dx, dy)
|
||||
|
|
@ -259,7 +175,7 @@ public func sampleDisplacement(
|
|||
dy *= scale
|
||||
}
|
||||
|
||||
return (dx, dy)
|
||||
return (dx, dy, sdf)
|
||||
}
|
||||
|
||||
/// Generates a glass mesh with corner-aware topology.
|
||||
|
|
@ -268,10 +184,10 @@ public func sampleDisplacement(
|
|||
/// - 1 center patch
|
||||
/// Corner/edge seams share the same coordinates (but do not reuse vertices) so
|
||||
/// the neighbouring faces fit perfectly without T-junctions.
|
||||
public func generateGlassMeshFromDisplacementMap(
|
||||
public func generateGlassMesh(
|
||||
size: CGSize,
|
||||
cornerRadius: CGFloat,
|
||||
displacementMap: CGImage,
|
||||
edgeDistance: CGFloat,
|
||||
displacementMagnitudeU: CGFloat,
|
||||
displacementMagnitudeV: CGFloat,
|
||||
cornerResolution: Int,
|
||||
|
|
@ -279,17 +195,6 @@ public func generateGlassMeshFromDisplacementMap(
|
|||
bezier: DisplacementBezier,
|
||||
generateWireframe: Bool = false
|
||||
) -> (mesh: MeshTransform, wireframe: CGPath?) {
|
||||
guard let dispDataProvider = displacementMap.dataProvider,
|
||||
let dispData = dispDataProvider.data,
|
||||
let dispPixels = CFDataGetBytePtr(dispData) else {
|
||||
return (mesh: MeshTransform(), wireframe: nil)
|
||||
}
|
||||
|
||||
let dispWidth = displacementMap.width
|
||||
let dispHeight = displacementMap.height
|
||||
let dispBytesPerRow = displacementMap.bytesPerRow
|
||||
let dispBytesPerPixel = displacementMap.bitsPerPixel / 8
|
||||
|
||||
let clampedRadius = min(cornerRadius, min(size.width, size.height) / 2)
|
||||
|
||||
let transform = MeshTransform()
|
||||
|
|
@ -311,7 +216,7 @@ public func generateGlassMeshFromDisplacementMap(
|
|||
let usableUNorm = usableWidth / size.width
|
||||
let usableVNorm = usableHeight / size.height
|
||||
|
||||
// Helper to sample displacement and create vertex
|
||||
// Helper to compute displacement analytically and create vertex
|
||||
func makeVertex(u: CGFloat, v: CGFloat, depth: CGFloat = 0) -> (vertex: MeshTransform.Vertex, point: CGPoint) {
|
||||
let mappedU = insetUOffset + u * usableUNorm
|
||||
let mappedV = insetVOffset + v * usableVNorm
|
||||
|
|
@ -322,28 +227,27 @@ public func generateGlassMeshFromDisplacementMap(
|
|||
fromX = mappedU
|
||||
fromY = mappedV
|
||||
} else {
|
||||
let (dispX, dispY) = sampleDisplacement(
|
||||
x: mappedU * CGFloat(dispWidth - 1),
|
||||
y: mappedV * CGFloat(dispHeight - 1),
|
||||
pixels: dispPixels,
|
||||
width: dispWidth,
|
||||
height: dispHeight,
|
||||
bytesPerRow: dispBytesPerRow,
|
||||
bytesPerPixel: dispBytesPerPixel,
|
||||
let worldX = insetPoints + u * usableWidth
|
||||
let worldY = insetPoints + v * usableHeight
|
||||
|
||||
let (dispX, dispY, sdf) = computeDisplacement(
|
||||
x: worldX,
|
||||
y: worldY,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
cornerRadius: clampedRadius,
|
||||
edgeDistance: edgeDistance,
|
||||
bezier: bezier
|
||||
)
|
||||
|
||||
// Slight boost near the edge to emphasize the outer strip (rounded-corner aware)
|
||||
let worldX = insetPoints + u * usableWidth
|
||||
let worldY = insetPoints + v * usableHeight
|
||||
let sdf = roundedRectSDF(x: worldX, y: worldY, width: size.width, height: size.height, cornerRadius: clampedRadius)
|
||||
let distToEdge = max(0.0, -sdf) // distance inside the rounded rect to the edge
|
||||
// Edge boost: slight displacement boost near the silhouette
|
||||
let distToEdge = max(0.0, -sdf)
|
||||
let edgeBand = max(0.0, outerEdgeDistance)
|
||||
let edgeBoostGain: CGFloat = 0.5 // up to +50% displacement at the edge, fades inside
|
||||
let edgeBoostGain: CGFloat = 0.5
|
||||
let edgeBoost: CGFloat
|
||||
if edgeBand > 0 {
|
||||
let t = max(0.0, min(1.0, (edgeBand - distToEdge) / edgeBand))
|
||||
let eased = t * t * (3 - 2 * t) // smoothstep
|
||||
let eased = t * t * (3 - 2 * t)
|
||||
edgeBoost = 1.0 + eased * edgeBoostGain
|
||||
} else {
|
||||
edgeBoost = 1.0
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SidebarBottomIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SidebarBottomIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "tabsdown.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SidebarBottomIcon.imageset/tabsdown.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SidebarBottomIcon.imageset/tabsdown.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "list_24.pdf",
|
||||
"filename" : "tabsleft.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
Binary file not shown.
BIN
submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SidebarIcon.imageset/tabsleft.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SidebarIcon.imageset/tabsleft.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SidebarTopIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SidebarTopIcon.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "tabsup.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SidebarTopIcon.imageset/tabsup.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Title Panels/SidebarTopIcon.imageset/tabsup.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -1351,7 +1351,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
var headerPanels: [HeaderPanelContainerComponent.Panel] = []
|
||||
var footerPanels: [HeaderPanelContainerComponent.Panel] = []
|
||||
|
||||
if let headerTopicsPanel = headerTopicsPanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction, force: false) {
|
||||
if self.chatPresentationInterfaceState.search == nil, let headerTopicsPanel = headerTopicsPanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, context: self.context, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction, force: false) {
|
||||
let panel = HeaderPanelContainerComponent.Panel(
|
||||
key: "topics",
|
||||
orderIndex: 0,
|
||||
|
|
@ -1363,7 +1363,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
footerPanels.append(panel)
|
||||
}
|
||||
}
|
||||
if let mediaPlayback = self.controller?.globalControlPanelsContextState?.mediaPlayback {
|
||||
if self.chatPresentationInterfaceState.search == nil, let mediaPlayback = self.controller?.globalControlPanelsContextState?.mediaPlayback {
|
||||
headerPanels.append(HeaderPanelContainerComponent.Panel(
|
||||
key: "media",
|
||||
orderIndex: 1,
|
||||
|
|
@ -1378,7 +1378,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
)))
|
||||
)
|
||||
}
|
||||
if let liveLocation = self.controller?.globalControlPanelsContextState?.liveLocation {
|
||||
if self.chatPresentationInterfaceState.search == nil, let liveLocation = self.controller?.globalControlPanelsContextState?.liveLocation {
|
||||
headerPanels.append(HeaderPanelContainerComponent.Panel(
|
||||
key: "liveLocation",
|
||||
orderIndex: 2,
|
||||
|
|
@ -1393,7 +1393,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
)))
|
||||
)
|
||||
}
|
||||
if let groupCall = self.controller?.globalControlPanelsContextState?.groupCall {
|
||||
if self.chatPresentationInterfaceState.search == nil, let groupCall = self.controller?.globalControlPanelsContextState?.groupCall {
|
||||
headerPanels.append(HeaderPanelContainerComponent.Panel(
|
||||
key: "groupCall",
|
||||
orderIndex: 3,
|
||||
|
|
@ -1651,7 +1651,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
switch floatingTopicsPanelComponent.location {
|
||||
case .side:
|
||||
floatingTopicsPanelInsets.left += 72.0 + 10.0 + 10.0
|
||||
case .top:
|
||||
case .top, .bottom:
|
||||
floatingTopicsPanelInsets.top += 40.0 + 10.0
|
||||
}
|
||||
} else if let floatingTopicsPanel = self.floatingTopicsPanel {
|
||||
|
|
|
|||
|
|
@ -461,6 +461,9 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea
|
|||
case .Loading:
|
||||
return nil
|
||||
case let .HistoryView(view, _, _, _, _, _, _):
|
||||
if view.isLoading {
|
||||
return nil
|
||||
}
|
||||
return view.entries.isEmpty
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,6 +260,7 @@ func headerTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterf
|
|||
strings: chatPresentationInterfaceState.strings,
|
||||
peerId: peerId,
|
||||
kind: .monoforum,
|
||||
location: chatPresentationInterfaceState.persistentData.topicListPanelLocation == .top ? .top : .bottom,
|
||||
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
||||
controller: { [weak interfaceInteraction] in
|
||||
return interfaceInteraction?.chatController()
|
||||
|
|
@ -290,6 +291,7 @@ func headerTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterf
|
|||
strings: chatPresentationInterfaceState.strings,
|
||||
peerId: peerId,
|
||||
kind: .forum,
|
||||
location: chatPresentationInterfaceState.persistentData.topicListPanelLocation == .top ? .top : .bottom,
|
||||
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
||||
controller: { [weak interfaceInteraction] in
|
||||
return interfaceInteraction?.chatController()
|
||||
|
|
@ -322,6 +324,7 @@ func headerTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInterf
|
|||
strings: chatPresentationInterfaceState.strings,
|
||||
peerId: peerId,
|
||||
kind: .botForum(forumManagedByUser: botInfo.flags.contains(.forumManagedByUser)),
|
||||
location: chatPresentationInterfaceState.persistentData.topicListPanelLocation == .top ? .top : .bottom,
|
||||
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
||||
controller: { [weak interfaceInteraction] in
|
||||
return interfaceInteraction?.chatController()
|
||||
|
|
@ -374,7 +377,7 @@ func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInte
|
|||
theme: chatPresentationInterfaceState.theme,
|
||||
preferClearGlass: chatPresentationInterfaceState.preferredGlassType == .clear,
|
||||
strings: chatPresentationInterfaceState.strings,
|
||||
location: topicListDisplayModeOnTheSide ? .side : .top,
|
||||
location: .side,
|
||||
peerId: peerId,
|
||||
kind: .monoforum,
|
||||
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
||||
|
|
@ -406,7 +409,7 @@ func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInte
|
|||
theme: chatPresentationInterfaceState.theme,
|
||||
preferClearGlass: chatPresentationInterfaceState.preferredGlassType == .clear,
|
||||
strings: chatPresentationInterfaceState.strings,
|
||||
location: topicListDisplayModeOnTheSide ? .side : .top,
|
||||
location: .side,
|
||||
peerId: peerId,
|
||||
kind: .forum,
|
||||
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
||||
|
|
@ -440,7 +443,7 @@ func floatingTopicsPanelForChatPresentationInterfaceState(_ chatPresentationInte
|
|||
theme: chatPresentationInterfaceState.theme,
|
||||
preferClearGlass: chatPresentationInterfaceState.preferredGlassType == .clear,
|
||||
strings: chatPresentationInterfaceState.strings,
|
||||
location: topicListDisplayModeOnTheSide ? .side : .top,
|
||||
location: .side,
|
||||
peerId: peerId,
|
||||
kind: .botForum(forumManagedByUser: botInfo.flags.contains(.forumManagedByUser)),
|
||||
topicId: chatPresentationInterfaceState.chatLocation.threadId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue