From 5145b9e605511ad719b6d7fd28abd49b0bcbdd05 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 31 Oct 2025 17:51:37 +0400 Subject: [PATCH] Stories --- .../Source/Base/Transition.swift | 10 +- submodules/Display/Source/UIKitUtils.swift | 4 +- .../MtProtoKit/Sources/MTTcpConnection.m | 712 +++++++++--------- .../Sources/PresentationGroupCall.swift | 7 + .../TelegramEngine/Calls/GroupCalls.swift | 143 +++- .../Messages/TelegramEngineMessages.swift | 4 +- .../Sources/AsyncListComponent.swift | 3 + .../Sources/ChatSendStarsScreen.swift | 168 ++++- .../Sources/ChatFloatingTopicsPanel.swift | 82 +- .../Sources/ChatTextInputPanelNode.swift | 29 +- .../Sources/StarReactionButtonComponent.swift | 104 ++- .../Sources/GiftAuctionScreen.swift | 5 +- .../Sources/GlassBackgroundComponent.swift | 87 ++- .../Sources/MessageInputPanelComponent.swift | 11 +- .../StoryLiveChatMessageComponent.swift | 176 ++++- .../Sources/LiveChatReactionStreamView.swift | 36 +- .../Sources/PinnedBarComponent.swift | 466 ++++++++++++ .../StoryContentLiveChatComponent.swift | 432 ++--------- .../StoryItemSetContainerComponent.swift | 4 +- ...StoryItemSetContainerViewSendMessage.swift | 21 +- .../LiveChatCrown.imageset/Contents.json | 12 + .../Stories/LiveChatCrown.imageset/crown.pdf | Bin 0 -> 4519 bytes .../Sources/ChatControllerNode.swift | 12 +- 23 files changed, 1515 insertions(+), 1013 deletions(-) create mode 100644 submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/PinnedBarComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/LiveChatCrown.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/LiveChatCrown.imageset/crown.pdf diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 15b6a429e2..48159bcd2c 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1305,15 +1305,19 @@ public struct ComponentTransition { ) } - public func animateBlur(layer: CALayer, fromRadius: CGFloat, toRadius: CGFloat, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { - if case .none = self.animation { + public func animateBlur(layer: CALayer, fromRadius: CGFloat, toRadius: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { + let duration: Double + switch self.animation { + case let .curve(durationValue, _): + duration = durationValue + case .none: return } if let blurFilter = CALayer.blur() { blurFilter.setValue(toRadius as NSNumber, forKey: "inputRadius") layer.filters = [blurFilter] - layer.animate(from: fromRadius as NSNumber, to: toRadius as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: removeOnCompletion, completion: { [weak layer] flag in + layer.animate(from: fromRadius as NSNumber, to: toRadius as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: duration, delay: delay, removeOnCompletion: removeOnCompletion, completion: { [weak layer] flag in if let layer { if toRadius <= 0.0 { layer.filters = nil diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 60de5fe558..130accaff1 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -45,9 +45,9 @@ public func dumpLayers(_ layer: CALayer) { } private func dumpLayers(_ layer: CALayer, indent: String = "") { - print("\(indent)\(layer)(frame: \(layer.frame), bounds: \(layer.bounds))") + print("\(indent)\(layer.debugDescription)(frame: \(layer.frame), bounds: \(layer.bounds))") if layer.sublayers != nil { - let nextIndent = indent + ".." + let nextIndent = indent + "—" if let sublayers = layer.sublayers { for sublayer in sublayers { dumpLayers(sublayer as CALayer, indent: nextIndent) diff --git a/submodules/MtProtoKit/Sources/MTTcpConnection.m b/submodules/MtProtoKit/Sources/MTTcpConnection.m index e8cf239881..997567a73f 100644 --- a/submodules/MtProtoKit/Sources/MTTcpConnection.m +++ b/submodules/MtProtoKit/Sources/MTTcpConnection.m @@ -10,6 +10,7 @@ #import #import #import +#import #import @@ -108,7 +109,26 @@ static void generate_public_key(unsigned char key[32], id pr } } -/*typedef enum { +static bool MTFillRandomBytes(uint8_t *buffer, size_t length) { + return SecRandomCopyBytes(kSecRandomDefault, length, buffer) == errSecSuccess; +} + +static bool MTGenerateGreaseValues(uint8_t grease[8]) { + if (!MTFillRandomBytes(grease, 8)) { + return false; + } + for (NSUInteger i = 0; i < 8; i++) { + grease[i] = (uint8_t)((grease[i] & 0xf0) | 0x0a); + } + for (NSUInteger i = 0; i < 8; i += 2) { + if (grease[i] == grease[i + 1]) { + grease[i + 1] ^= 0x10; + } + } + return true; +} + +typedef enum { HelloGenerationCommandInvalid = 0, HelloGenerationCommandString = 1, HelloGenerationCommandZero = 2, @@ -121,77 +141,75 @@ static void generate_public_key(unsigned char key[32], id pr } HelloGenerationCommand; typedef struct { - int position; + NSUInteger position; } HelloParseState; static HelloGenerationCommand parseCommand(NSString *string, HelloParseState *state) { - if (state->position + 1 >= string.length) { - return HelloGenerationCommandInvalid; - } - unichar c = [string characterAtIndex:state->position]; - state->position += 1; - - if (c == 'S') { - return HelloGenerationCommandString; - } else if (c == 'Z') { - return HelloGenerationCommandZero; - } else if (c == 'R') { - return HelloGenerationCommandRandom; - } else if (c == 'D') { - return HelloGenerationCommandDomain; - } else if (c == 'G') { - return HelloGenerationCommandGrease; - } else if (c == 'K') { - return HelloGenerationCommandKey; - } else if (c == '[') { - return HelloGenerationCommandPushLengthPosition; - } else if (c == ']') { - return HelloGenerationCommandPopLengthPosition; - } else { - return HelloGenerationCommandInvalid; + while (state->position < string.length) { + unichar c = [string characterAtIndex:state->position]; + state->position += 1; + if (c == '\n' || c == '\r') { + continue; + } else if (c == 'S') { + return HelloGenerationCommandString; + } else if (c == 'Z') { + return HelloGenerationCommandZero; + } else if (c == 'R') { + return HelloGenerationCommandRandom; + } else if (c == 'D') { + return HelloGenerationCommandDomain; + } else if (c == 'G') { + return HelloGenerationCommandGrease; + } else if (c == 'K') { + return HelloGenerationCommandKey; + } else if (c == '[') { + return HelloGenerationCommandPushLengthPosition; + } else if (c == ']') { + return HelloGenerationCommandPopLengthPosition; + } else { + return HelloGenerationCommandInvalid; + } } + return HelloGenerationCommandInvalid; } static bool parseSpace(NSString *string, HelloParseState *state) { - if (state->position + 1 >= string.length) { - return false; - } - bool hadSpace = false; - while (true) { + bool seenSpace = false; + while (state->position < string.length) { unichar c = [string characterAtIndex:state->position]; - state->position += 1; if (c == ' ') { - hadSpace = true; + seenSpace = true; + state->position += 1; } else { - if (hadSpace) { - return true; - } else { - return false; - } + return seenSpace; } } - return true; + return seenSpace; } static bool parseEndlineOrEnd(NSString *string, HelloParseState *state) { - if (state->position == string.length) { - return true; - } else if (state->position + 1 >= string.length) { - return false; - } else { + while (state->position < string.length) { unichar c = [string characterAtIndex:state->position]; - state->position += 1; - return c == '\n'; + if (c == '\n') { + state->position += 1; + return true; + } else if (c == ' ' || c == '\r') { + state->position += 1; + continue; + } else { + return false; + } } + return true; } static bool parseHexByte(unichar c, uint8_t *output) { if (c >= '0' && c <= '9') { *output = (uint8_t)(c - '0'); } else if (c >= 'a' && c <= 'f') { - *output = (uint8_t)(c - 'a'); + *output = (uint8_t)(c - 'a' + 10); } else if (c >= 'A' && c <= 'F') { - *output = (uint8_t)(c - 'A'); + *output = (uint8_t)(c - 'A' + 10); } else { return false; } @@ -202,278 +220,326 @@ static NSData *parseHexStringArgument(NSString *string, HelloParseState *state) if (state->position >= string.length) { return nil; } - + if ([string characterAtIndex:state->position] != '"') { + return nil; + } + state->position += 1; + NSMutableData *data = [[NSMutableData alloc] init]; - - while (true) { - if (state->position == string.length) { - return data; - } - + + while (state->position < string.length) { unichar c = [string characterAtIndex:state->position]; state->position += 1; if (c == '\\') { if (state->position >= string.length) { return nil; } - c = [string characterAtIndex:state->position]; + unichar escapeType = [string characterAtIndex:state->position]; state->position += 1; - if (c == 'x') { - if (state->position >= string.length) { + + if (escapeType == 'x' || escapeType == 'X') { + if (state->position + 1 >= string.length) { return nil; } unichar d1 = [string characterAtIndex:state->position]; state->position += 1; - if (state->position >= string.length) { - return nil; - } unichar d0 = [string characterAtIndex:state->position]; state->position += 1; - - uint8_t c1 = 0; - if (!parseHexByte(d1, &c1)) { + + uint8_t value1 = 0; + uint8_t value0 = 0; + if (!parseHexByte(d1, &value1) || !parseHexByte(d0, &value0)) { return nil; } - uint8_t c0 = 0; - if (!parseHexByte(d0, &c0)) { - return nil; - } - uint8_t byteValue = (c1 << 4) | c0; + uint8_t byteValue = (uint8_t)((value1 << 4) | value0); [data appendBytes:&byteValue length:1]; } else { return nil; } + } else if (c == '"') { + if (state->position < string.length && [string characterAtIndex:state->position] == '\n') { + state->position += 1; + } + return data; } else if (c == '\n') { return data; } else { return nil; } } - - return nil; + + return data; } static bool parseIntArgument(NSString *string, HelloParseState *state, int *output) { if (state->position >= string.length) { return false; } + int value = 0; - while (true) { - if (state->position == string.length) { - *output = value; - return true; - } - + bool hasDigit = false; + while (state->position < string.length) { unichar c = [string characterAtIndex:state->position]; - state->position += 1; - - if (c == '\n') { - *output = value; - return true; - } else if (c >= '0' && c <= '9') { - value *= 10; - value += c; + if (c >= '0' && c <= '9') { + value = value * 10 + (c - '0'); + hasDigit = true; + state->position += 1; + } else if (c == ' ') { + state->position += 1; + } else if (c == '\n') { + state->position += 1; + break; } else { return false; } } - return false; + + if (!hasDigit) { + return false; + } + + if (output != NULL) { + *output = value; + } + + return true; } -static NSData *executeGenerationCode(id provider, NSData *domain) { - NSString *code = @"S \"\\x16\\x03\\x01\\x02\\x00\\x01\\x00\\x01\\xfc\\x03\\x03\\n" - "Z 32" - "S \"\\x20\"\n" - "R 32\n" - "S \"\\x00\\x36\"\n" - "G 0\n" - "S \"\\x13\\x01\\x13\\x02\\x13\\x03\\xc0\\x2c\\xc0\\x2b\\xcc\\xa9\\xc0\\x30\\xc0\\x2f\\xcc\\xa8\\xc0\\x24\\xc0\\x23\\xc0\\x0a\\xc0\\x09\\xc0\\x28\\xc0\\x27\\xc0\\x14\\xc0\\x13\\x00\\x9d\\x00\\x9c\\x00\\x3d\\x00\\x3c\\x00\\x35\\x00\\x2f\\xc0\\x08\\xc0\\x12\\x00\\x0a\\x01\\x00\\x01\\x7d\"\n" - "G 2\n" - "S \"\\x00\\x00\\x00\\x00\"\n" - "[\n" - "[\n" - "S \"\\x00\"\n" - "[\n" - "D\n" - "]\n" - "]\n" - "]\n" - "S \"\\x00\\x17\\x00\\x00\\xff\\x01\\x00\\x01\\x00\\x00\\x0a\\x00\\x0c\\x00\\x0a\"\n" - "G 4\n" - "S \"\\x00\\x1d\\x00\\x17\\x00\\x18\\x00\\x19\\x00\\x0b\\x00\\x02\\x01\\x00\\x00\\x10\\x00\\x0e\\x00\\x0c\\x02\\x68\\x32\\x08\\x68\\x74\\x74\\x70\\x2f\\x31\\x2e\\x31\\x00\\x05\\x00\\x05\\x01\\x00\\x00\\x00\\x00\\x00\\x0d\\x00\\x18\\x00\\x16\\x04\\x03\\x08\\x04\\x04\\x01\\x05\\x03\\x02\\x03\\x08\\x05\\x08\\x05\\x05\\x01\\x08\\x06\\x06\\x01\\x02\\x01\\x00\\x12\\x00\\x00\\x00\\x33\\x00\\x2b\\x00\\x29\"\n" - "G 4\n" - "S \"\\x00\\x01\\x00\\x00\\x1d\\x00\\x20\"\n" - "K\n" - "S \"\\x00\\x2d\\x00\\x02\\x01\\x01\\x00\\x2b\\x00\\x0b\\x0a\"\n" - "G 6\n" - "S \"\\x03\\x04\\x03\\x03\\x03\\x02\\x03\\x01\"\n" - "G 3\n" - "S \"\\x00\\x01\\x00\\x00\\x15\""; - - int greaseCount = 8; - NSMutableData *greaseData = [[NSMutableData alloc] initWithLength:greaseCount]; - uint8_t *greaseBytes = (uint8_t *)greaseData.mutableBytes; - int result; - result = SecRandomCopyBytes(nil, greaseData.length, greaseData.mutableBytes); - - for (int i = 0; i < greaseData.length; i++) { - uint8_t c = greaseBytes[i]; - c = (c & 0xf0) | 0x0a; - greaseBytes[i] = c; +static NSMutableData *executeGenerationCode(id provider, NSData *domain) { + NSString *code = @"S \"\\x16\\x03\\x01\\x02\\x00\\x01\\x00\\x01\\xfc\\x03\\x03\"\n" + "Z 32\n" + "S \"\\x20\"\n" + "R 32\n" + "S \"\\x00\\x2a\"\n" + "G 0\n" + "S \"\\x13\\x01\\x13\\x02\\x13\\x03\\xc0\\x2c\\xc0\\x2b\\xcc\\xa9\\xc0\\x30\\xc0\\x2f\\xcc\\xa8\\xc0\\x0a\\xc0\\x09\\xc0\\x14\\xc0\\x13\\x00\\x9d\\x00\\x9c\\x00\\x35\\x00\\x2f\\xc0\\x08\\xc0\\x12\\x00\\x0a\\x01\\x00\\x01\\x89\"\n" + "G 2\n" + "S \"\\x00\\x00\\x00\\x00\"\n" + "[\n" + "[\n" + "S \"\\x00\"\n" + "[\n" + "D\n" + "]\n" + "]\n" + "]\n" + "S \"\\x00\\x17\\x00\\x00\\xff\\x01\\x00\\x01\\x00\\x00\\x0a\\x00\\x0c\\x00\\x0a\"\n" + "G 4\n" + "S \"\\x00\\x1d\\x00\\x17\\x00\\x18\\x00\\x19\\x00\\x0b\\x00\\x02\\x01\\x00\\x00\\x10\\x00\\x0e\\x00\\x0c\\x02\\x68\\x32\\x08\\x68\\x74\\x74\\x70\\x2f\\x31\\x2e\\x31\\x00\\x05\\x00\\x05\\x01\\x00\\x00\\x00\\x00\\x00\\x0d\\x00\\x16\\x00\\x14\\x04\\x03\\x08\\x04\\x04\\x01\\x05\\x03\\x08\\x05\\x08\\x05\\x05\\x01\\x08\\x06\\x06\\x01\\x02\\x01\\x00\\x12\\x00\\x00\\x00\\x33\\x00\\x2b\\x00\\x29\"\n" + "G 4\n" + "S \"\\x00\\x01\\x00\\x00\\x1d\\x00\\x20\"\n" + "K\n" + "S \"\\x00\\x2d\\x00\\x02\\x01\\x01\\x00\\x2b\\x00\\x0b\\x0a\"\n" + "G 6\n" + "S \"\\x03\\x04\\x03\\x03\\x03\\x02\\x03\\x01\\x00\\x1b\\x00\\x03\\x02\\x00\\x01\"\n" + "G 3\n" + "S \"\\x00\\x01\\x00\\x00\\x15\"\n"; + + uint8_t grease[8]; + if (!MTGenerateGreaseValues(grease)) { + return nil; } - for (int i = 1; i < greaseData.length; i += 2) { - if (greaseBytes[i] == greaseBytes[i - 1]) { - greaseBytes[i] &= 0x10; - } - } - + NSMutableData *resultData = [[NSMutableData alloc] init]; NSMutableArray *lengthStack = [[NSMutableArray alloc] init]; - - HelloParseState state; - state.position = 0; - + + HelloParseState state = { .position = 0 }; + + NSUInteger trailingZeroStart = 0; + NSUInteger trailingZeroRemaining = 0; + while (true) { - if (state.position >= code.length) { + HelloGenerationCommand command = parseCommand(code, &state); + if (command == HelloGenerationCommandInvalid) { break; - } else { - HelloGenerationCommand command = parseCommand(code, &state); - switch (command) { - case HelloGenerationCommandString: { - if (!parseSpace(code, &state)) { - return nil; - } - NSData *data = parseHexStringArgument(code, &state); - if (data == nil) { - return nil; - } - - [resultData appendData:data]; - - break; - } - case HelloGenerationCommandZero: { - if (!parseSpace(code, &state)) { - return false; - } - int zeroLength = 0; - if (!parseIntArgument(code, &state, &zeroLength)) { - return nil; - } - - NSMutableData *zeroData = [[NSMutableData alloc] initWithLength:zeroLength]; - [resultData appendData:zeroData]; - - break; - } - case HelloGenerationCommandRandom: { - if (!parseSpace(code, &state)) { - return nil; - } - int randomLength = 0; - if (!parseIntArgument(code, &state, &randomLength)) { - return nil; - } - - NSMutableData *randomData = [[NSMutableData alloc] initWithLength:randomLength]; - int randomResult = SecRandomCopyBytes(kSecRandomDefault, randomLength, randomData.mutableBytes); - if (randomResult != errSecSuccess) { - return nil; - } - [resultData appendData:randomData]; - - break; - } - case HelloGenerationCommandDomain: { - [resultData appendData:domain]; - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - break; - } - case HelloGenerationCommandGrease: { - if (!parseSpace(code, &state)) { - return nil; - } - int greaseIndex = 0; - if (!parseIntArgument(code, &state, &greaseIndex)) { - return nil; - } - - if (greaseIndex < 0 || greaseIndex >= greaseCount) { - return nil; - } - - [resultData appendBytes:&greaseBytes[greaseIndex] length:1]; - [resultData appendBytes:&greaseBytes[greaseIndex] length:1]; - - break; - } - case HelloGenerationCommandKey: { - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - - NSMutableData *key = [[NSMutableData alloc] initWithLength:32]; - generate_public_key(key.mutableBytes, provider); - [resultData appendData:key]; - - break; - } - case HelloGenerationCommandPushLengthPosition: { - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - - [lengthStack addObject:@(resultData.length)]; - NSMutableData *zeroData = [[NSMutableData alloc] initWithLength:2]; - [resultData appendData:zeroData]; - - break; - } - case HelloGenerationCommandPopLengthPosition: { - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - - if (lengthStack.count == 0) { - return nil; - } - - int position = [lengthStack[lengthStack.count - 1] intValue]; - uint16_t calculatedLength = resultData.length - 2 - position; - ((uint8_t *)resultData.mutableBytes)[position] = ((uint8_t *)&calculatedLength)[1]; - ((uint8_t *)resultData.mutableBytes)[position + 1] = ((uint8_t *)&calculatedLength)[0]; - [lengthStack removeLastObject]; - - break; - } - case HelloGenerationCommandInvalid: { + } + + switch (command) { + case HelloGenerationCommandString: { + if (!parseSpace(code, &state)) { return nil; } - default: { + NSData *data = parseHexStringArgument(code, &state); + if (data == nil) { return nil; } + [resultData appendData:data]; + + if (data.length > 0) { + bool allZero = true; + const uint8_t *bytes = (const uint8_t *)data.bytes; + for (NSUInteger i = 0; i < data.length; i++) { + if (bytes[i] != 0) { + allZero = false; + break; + } + } + if (allZero) { + trailingZeroStart = resultData.length - data.length; + trailingZeroRemaining = data.length; + } else { + trailingZeroRemaining = 0; + } + } else { + trailingZeroRemaining = 0; + } + break; + } + case HelloGenerationCommandZero: { + if (!parseSpace(code, &state)) { + return nil; + } + int zeroLength = 0; + if (!parseIntArgument(code, &state, &zeroLength)) { + return nil; + } + if (zeroLength < 0) { + return nil; + } + NSMutableData *zeros = [[NSMutableData alloc] initWithLength:(NSUInteger)zeroLength]; + [resultData appendData:zeros]; + if (zeroLength > 0) { + trailingZeroStart = resultData.length - zeros.length; + trailingZeroRemaining = zeros.length; + } else { + trailingZeroRemaining = 0; + } + break; + } + case HelloGenerationCommandRandom: { + if (!parseSpace(code, &state)) { + return nil; + } + int randomLength = 0; + if (!parseIntArgument(code, &state, &randomLength)) { + return nil; + } + if (randomLength < 0) { + return nil; + } + NSMutableData *randomData = [[NSMutableData alloc] initWithLength:(NSUInteger)randomLength]; + if (!MTFillRandomBytes((uint8_t *)randomData.mutableBytes, randomData.length)) { + return nil; + } + [resultData appendData:randomData]; + trailingZeroRemaining = 0; + break; + } + case HelloGenerationCommandDomain: { + [resultData appendData:domain]; + if (!parseEndlineOrEnd(code, &state)) { + return nil; + } + trailingZeroRemaining = 0; + break; + } + case HelloGenerationCommandGrease: { + if (!parseSpace(code, &state)) { + return nil; + } + int greaseIndex = 0; + if (!parseIntArgument(code, &state, &greaseIndex)) { + return nil; + } + if (greaseIndex < 0 || greaseIndex >= 8) { + return nil; + } + uint8_t value = grease[greaseIndex]; + [resultData appendBytes:&value length:1]; + [resultData appendBytes:&value length:1]; + trailingZeroRemaining = 0; + break; + } + case HelloGenerationCommandKey: { + if (!parseEndlineOrEnd(code, &state)) { + return nil; + } + NSMutableData *key = [[NSMutableData alloc] initWithLength:32]; + generate_public_key((unsigned char *)key.mutableBytes, provider); + [resultData appendData:key]; + trailingZeroRemaining = 0; + break; + } + case HelloGenerationCommandPushLengthPosition: { + if (!parseEndlineOrEnd(code, &state)) { + return nil; + } + NSUInteger lengthPosition = 0; + if (trailingZeroRemaining >= 2) { + lengthPosition = trailingZeroStart + trailingZeroRemaining - 2; + trailingZeroRemaining -= 2; + } else { + lengthPosition = resultData.length; + uint8_t zeroBytes[2] = { 0, 0 }; + [resultData appendBytes:zeroBytes length:2]; + trailingZeroStart = lengthPosition; + trailingZeroRemaining = 0; + } + [lengthStack addObject:@(lengthPosition)]; + break; + } + case HelloGenerationCommandPopLengthPosition: { + if (!parseEndlineOrEnd(code, &state)) { + return nil; + } + if (lengthStack.count == 0) { + return nil; + } + NSUInteger position = (NSUInteger)[lengthStack.lastObject unsignedIntegerValue]; + [lengthStack removeLastObject]; + if (resultData.length < position + 2) { + return nil; + } + uint16_t blockLength = (uint16_t)(resultData.length - position - 2); + ((uint8_t *)resultData.mutableBytes)[position] = (uint8_t)((blockLength >> 8) & 0xff); + ((uint8_t *)resultData.mutableBytes)[position + 1] = (uint8_t)(blockLength & 0xff); + break; + } + default: { + return nil; } } + + if (trailingZeroRemaining == 0) { + trailingZeroStart = 0; + } } - - int paddingLengthPosition = (int)resultData.length; - [lengthStack addObject:@(resultData.length)]; - NSMutableData *zeroData = [[NSMutableData alloc] initWithLength:2]; - [resultData appendData:zeroData]; - + + if (lengthStack.count != 0) { + return nil; + } + + if (resultData.length > 517) { + return nil; + } + + NSUInteger paddingLengthPosition = resultData.length; + uint8_t paddingPlaceholder[2] = { 0, 0 }; + [resultData appendBytes:paddingPlaceholder length:2]; while (resultData.length < 517) { uint8_t zero = 0; [resultData appendBytes:&zero length:1]; } - - uint16_t calculatedLength = resultData.length - 2 - paddingLengthPosition; - ((uint8_t *)resultData.mutableBytes)[paddingLengthPosition] = ((uint8_t *)&calculatedLength)[1]; - ((uint8_t *)resultData.mutableBytes)[paddingLengthPosition + 1] = ((uint8_t *)&calculatedLength)[0]; - + if (resultData.length != 517) { + return nil; + } + + uint16_t paddingLength = (uint16_t)(resultData.length - paddingLengthPosition - 2); + ((uint8_t *)resultData.mutableBytes)[paddingLengthPosition] = (uint8_t)((paddingLength >> 8) & 0xff); + ((uint8_t *)resultData.mutableBytes)[paddingLengthPosition + 1] = (uint8_t)(paddingLength & 0xff); + return resultData; -}*/ +} + +static NSMutableData *MTCreateSafariClientHello(NSString *domain, id provider) { + NSData *domainData = [domain dataUsingEncoding:NSUTF8StringEncoding]; + if (domainData == nil || domainData.length == 0 || domainData.length > 0xffff) { + return nil; + } + + return executeGenerationCode(provider, domainData); +} + @interface MTTcpConnectionData : NSObject @@ -813,6 +879,7 @@ struct ctr_state { _mtpPort = _scheme.address.port; _mtpSecret = [MTProxySecret parseData:_scheme.address.secret]; } + if (context.apiEnvironment.socksProxySettings != nil) { if (context.apiEnvironment.socksProxySettings.secret != nil) { _mtpIp = context.apiEnvironment.socksProxySettings.ip; @@ -982,110 +1049,31 @@ struct ctr_state { } else if (strongSelf->_socksIp == nil) { if (strongSelf->_mtpIp != nil && [strongSelf->_mtpSecret isKindOfClass:[MTProxySecretType2 class]]) { MTProxySecretType2 *secret = (MTProxySecretType2 *)(strongSelf->_mtpSecret); - - int greaseCount = 8; - NSMutableData *greaseData = [[NSMutableData alloc] initWithLength:greaseCount]; - uint8_t *greaseBytes = (uint8_t *)greaseData.mutableBytes; - int result = SecRandomCopyBytes(nil, greaseData.length, greaseData.mutableBytes); - if (result != errSecSuccess) { - assert(false); + NSMutableData *helloData = MTCreateSafariClientHello(secret.domain, strongSelf->_encryptionProvider); + if (helloData == nil || helloData.length != 517) { + [strongSelf closeAndNotifyWithError:true]; + return; } - - for (int i = 0; i < greaseData.length; i++) { - uint8_t c = greaseBytes[i]; - c = (c & 0xf0) | 0x0a; - greaseBytes[i] = c; + + NSData *effectiveSecret = secret.secret; + if (effectiveSecret.length < 16) { + [strongSelf closeAndNotifyWithError:true]; + return; } - for (int i = 1; i < greaseData.length; i += 2) { - if (greaseBytes[i] == greaseBytes[i - 1]) { - greaseBytes[i] &= 0x10; - } - } - - NSMutableData *helloData = [[NSMutableData alloc] init]; - - uint8_t s1[11] = { 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03 }; - [helloData appendBytes:s1 length:11]; - - for (int i = 0; i < 32; i++) { - uint8_t zero = 0; - [helloData appendBytes:&zero length:1]; - } - - uint8_t s2[1] = { 0x20 }; - [helloData appendBytes:s2 length:1]; - - uint8_t r1[32]; - result = SecRandomCopyBytes(nil, 32, r1); - assert(result == errSecSuccess); - [helloData appendBytes:r1 length:32]; - - uint8_t s0[65] = { 0x00, 0x34, 0x13, 0x03, 0x13, 0x01, 0x13, 0x02, 0xc0, 0x2c, 0xc0, 0x2b, 0xc0, 0x24, 0xc0, 0x23, 0xc0, 0x0a, 0xc0, 0x09, 0xcc, 0xa9, 0xc0, 0x30, 0xc0, 0x2f, 0xc0, 0x28, 0xc0, 0x27, 0xc0, 0x14, 0xc0, 0x13, 0xcc, 0xa8, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0xc0, 0x08, 0xc0, 0x12, 0x00, 0x0a, 0x01, 0x00, 0x01, 0x7f, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00 }; - [helloData appendBytes:s0 length:65]; - - uint8_t stackZ[2] = { 0x00, 0x00 }; - - int stack1 = (int)helloData.length; - [helloData appendBytes:stackZ length:2]; - - int stack2 = (int)helloData.length; - [helloData appendBytes:stackZ length:2]; - - uint8_t s5[1] = { 0x00 }; - [helloData appendBytes:s5 length:1]; - - int stack3 = (int)helloData.length; - [helloData appendBytes:stackZ length:2]; - - NSString *d1 = secret.domain; - [helloData appendData:[d1 dataUsingEncoding:NSUTF8StringEncoding]]; - - int16_t stack3Value = (int16_t)(helloData.length - stack3 - 2); - stack3Value = OSSwapInt16(stack3Value); - memcpy(((uint8_t *)helloData.mutableBytes) + stack3, &stack3Value, 2); - - int16_t stack2Value = (int16_t)(helloData.length - stack2 - 2); - stack2Value = OSSwapInt16(stack2Value); - memcpy(((uint8_t *)helloData.mutableBytes) + stack2, &stack2Value, 2); - - int16_t stack1Value = (int16_t)(helloData.length - stack1 - 2); - stack1Value = OSSwapInt16(stack1Value); - memcpy(((uint8_t *)helloData.mutableBytes) + stack1, &stack1Value, 2); - - uint8_t s6[117] = { 0x00, 0x17, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x18, 0x00, 0x16, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x02, 0x03, 0x08, 0x05, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x33, 0x74, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x2e, 0x02, 0x68, 0x32, 0x05, 0x68, 0x32, 0x2d, 0x31, 0x36, 0x05, 0x68, 0x32, 0x2d, 0x31, 0x35, 0x05, 0x68, 0x32, 0x2d, 0x31, 0x34, 0x08, 0x73, 0x70, 0x64, 0x79, 0x2f, 0x33, 0x2e, 0x31, 0x06, 0x73, 0x70, 0x64, 0x79, 0x2f, 0x33, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20 }; - [helloData appendBytes:s6 length:117]; - - uint8_t r2[32]; - generate_public_key(r2, strongSelf->_encryptionProvider); - - [helloData appendBytes:r2 length:32]; - - uint8_t s9[35] = { 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x09, 0x08, 0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x15 }; - [helloData appendBytes:s9 length:35]; - - int stack4 = (int)helloData.length; - [helloData appendBytes:stackZ length:2]; - - while (helloData.length < 517) { - uint8_t zero = 0; - [helloData appendBytes:&zero length:1]; - } - - int16_t stack4Value = (int16_t)(helloData.length - stack4 - 2); - stack4Value = OSSwapInt16(stack4Value); - memcpy(((uint8_t *)helloData.mutableBytes) + stack4, &stack4Value, 2); - - NSData *effectiveSecret = strongSelf->_mtpSecret.secret; + uint8_t cHMAC[CC_SHA256_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA256, effectiveSecret.bytes, effectiveSecret.length, helloData.bytes, helloData.length, cHMAC); - int32_t timestamp = (int32_t)[[NSDate date] timeIntervalSince1970] + [MTContext fixedTimeDifference]; - uint8_t *timestampValue = (uint8_t *)×tamp; + + uint32_t timestamp = (uint32_t)([[NSDate date] timeIntervalSince1970] + [MTContext fixedTimeDifference]); + uint32_t timestampLe = OSSwapHostToLittleInt32(timestamp); + uint8_t *timestampBytes = (uint8_t *)×tampLe; for (int i = 0; i < 4; i++) { - cHMAC[CC_SHA256_DIGEST_LENGTH - 4 + i] ^= timestampValue[i]; + cHMAC[CC_SHA256_DIGEST_LENGTH - 4 + i] ^= timestampBytes[i]; } - _helloRandom = [[NSData alloc] initWithBytes:cHMAC length:32]; - memcpy(((uint8_t *)helloData.mutableBytes) + 11, cHMAC, 32); - + + _helloRandom = [[NSData alloc] initWithBytes:cHMAC length:CC_SHA256_DIGEST_LENGTH]; + memcpy(((uint8_t *)helloData.mutableBytes) + 11, cHMAC, CC_SHA256_DIGEST_LENGTH); + [strongSelf->_socket writeData:helloData]; [strongSelf->_socket readDataToLength:5 withTimeout:-1 tag:MTTcpSocksReceiveHelloResponse]; } else { @@ -1313,7 +1301,7 @@ struct ctr_state { [partitionedCompleteData appendData:[[NSData alloc] initWithBytes:helloHeader length:6]]; } - NSUInteger limit = 2878; + NSUInteger limit = 16408; NSUInteger offset = 0; while (offset < completeData.length) { NSUInteger partLength = MIN(limit, completeData.length - offset); diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 86fd6ff154..3cfa4c1599 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -937,6 +937,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { } self.messagesContext = accountContext.engine.messages.groupCallMessages( + appConfig: accountContext.currentAppConfiguration.with({ $0 }), callId: initialCall.description.id, reference: .id(id: initialCall.description.id, accessHash: initialCall.description.accessHash), e2eContext: self.e2eContext, @@ -4078,6 +4079,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { messagesContext.deleteMessage(id: id, reportSpam: reportSpam) } } + + public func deleteAllMessages(authorId: EnginePeer.Id, reportSpam: Bool) { + if let messagesContext = self.messagesContext { + messagesContext.deleteAllMessages(authorId: authorId, reportSpam: reportSpam) + } + } } public final class TelegramE2EEncryptionProviderImpl: TelegramE2EEncryptionProvider { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index e81b1be861..174537b318 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -3687,14 +3687,12 @@ public final class GroupCallMessagesContext { } } - public enum Color { - case purple - case blue - case green - case yellow - case orange - case red - case silver + public struct Color: RawRepresentable { + public var rawValue: UInt32 + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } } public let id: Id @@ -3824,6 +3822,7 @@ public final class GroupCallMessagesContext { private let isLiveStream: Bool let queue: Queue + let params: LiveChatMessageParams let account: Account let callId: Int64 let reference: InternalGroupCallReference @@ -3851,8 +3850,9 @@ public final class GroupCallMessagesContext { private var pendingSendStars: (fromPeer: Peer, messageId: Int64, amount: Int64)? private var pendingSendStarsTimer: SwiftSignalKit.Timer? - init(queue: Queue, account: Account, callId: Int64, reference: InternalGroupCallReference, e2eContext: ConferenceCallE2EContext?, messageLifetime: Int32, isLiveStream: Bool) { + init(queue: Queue, appConfig: AppConfiguration, account: Account, callId: Int64, reference: InternalGroupCallReference, e2eContext: ConferenceCallE2EContext?, messageLifetime: Int32, isLiveStream: Bool) { self.queue = queue + self.params = LiveChatMessageParams(appConfig: appConfig) self.account = account self.callId = callId self.reference = reference @@ -3944,7 +3944,7 @@ public final class GroupCallMessagesContext { let lifetime: Int32 if isLiveStream { - lifetime = Int32(GroupCallMessagesContext.getStarAmountParamMapping(value: addedMessage.paidMessageStars ?? 0).period) + lifetime = Int32(GroupCallMessagesContext.getStarAmountParamMapping(params: self.params, value: addedMessage.paidMessageStars ?? 0).period) } else { lifetime = self.messageLifetime } @@ -4228,7 +4228,7 @@ public final class GroupCallMessagesContext { let lifetime: Int32 if isLiveStream { - lifetime = Int32(GroupCallMessagesContext.getStarAmountParamMapping(value: paidStars ?? 0).period) + lifetime = Int32(GroupCallMessagesContext.getStarAmountParamMapping(params: self.params, value: paidStars ?? 0).period) } else { lifetime = self.messageLifetime } @@ -4448,7 +4448,7 @@ public final class GroupCallMessagesContext { self.processedIds.insert(randomId) } - let lifetime = Int32(GroupCallMessagesContext.getStarAmountParamMapping(value: totalAmount).period) + let lifetime = Int32(GroupCallMessagesContext.getStarAmountParamMapping(params: self.params, value: totalAmount).period) var state = self.state if let pendingSendStarsValue = self.pendingSendStars { @@ -4602,11 +4602,11 @@ public final class GroupCallMessagesContext { } } - init(account: Account, callId: Int64, reference: InternalGroupCallReference, e2eContext: ConferenceCallE2EContext?, messageLifetime: Int32, isLiveStream: Bool) { + init(account: Account, appConfig: AppConfiguration, callId: Int64, reference: InternalGroupCallReference, e2eContext: ConferenceCallE2EContext?, messageLifetime: Int32, isLiveStream: Bool) { let queue = Queue(name: "GroupCallMessagesContext") self.queue = queue self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, account: account, callId: callId, reference: reference, e2eContext: e2eContext, messageLifetime: messageLifetime, isLiveStream: isLiveStream) + return Impl(queue: queue, appConfig: appConfig, account: account, callId: callId, reference: reference, e2eContext: e2eContext, messageLifetime: messageLifetime, isLiveStream: isLiveStream) }) } @@ -4646,28 +4646,101 @@ public final class GroupCallMessagesContext { } } - public static func getStarAmountParamMapping(value: Int64) -> (period: Int, maxLength: Int, emojiCount: Int, color: Message.Color?) { - if value >= 10000 { - return (3600, 400, 20, .silver) - } - if value >= 2000 { - return (1800, 280, 10, .red) - } - if value >= 500 { - return (900, 200, 7, .orange) - } - if value >= 250 { - return (600, 150, 4, .yellow) - } - if value >= 100 { - return (300, 110, 3, .green) - } - if value >= 50 { - return (120, 80, 2, .blue) - } - if value >= 1 { - return (60, 60, 1, .purple) + public static func getStarAmountParamMapping(params: LiveChatMessageParams, value: Int64) -> (period: Int, maxLength: Int, emojiCount: Int, color: Message.Color?) { + for item in params.paramSets.reversed() { + if value >= item.minStars { + return (item.pinPeriod ?? 0, item.maxMessageLength, item.maxEmojiCount, item.color.flatMap(Message.Color.init(rawValue:))) + } } return (30, 30, 0, nil) } } + +private func colorFromHex(_ string: String) -> UInt32? { + guard let value = UInt32(string, radix: 16) else { + return nil + } + return value +} + +public struct LiveChatMessageParams: Equatable { + public struct ParamSet: Equatable { + public var minStars: Int64 + public var pinPeriod: Int? + public var maxMessageLength: Int + public var maxEmojiCount: Int + public var color: UInt32? + + public init(minStars: Int64, pinPeriod: Int?, maxMessageLength: Int, maxEmojiCount: Int, color: UInt32?) { + self.minStars = minStars + self.pinPeriod = pinPeriod + self.maxMessageLength = maxMessageLength + self.maxEmojiCount = maxEmojiCount + self.color = color + } + } + + public var paramSets: [ParamSet] + + public init(paramSets: [ParamSet]) { + self.paramSets = paramSets + } + + public init(appConfig: AppConfiguration) { + var paramSets: [ParamSet] = [] + if let list = appConfig.data?["stars_groupcall_message_limits"] as? [[String: Any]] { + for item in list { + guard let stars = item["stars"] as? Int64 else { + continue + } + let pinPeriod = item["pin_period"] as? Int + guard let maxMessageLength = item["text_length_max"] as? Int else { + continue + } + guard let maxEmojiCount = item["emoji_max"] as? Int else { + continue + } + guard let colorBgString = item["color_bg"] as? String, let colorBg = colorFromHex(colorBgString) else { + continue + } + paramSets.append(ParamSet( + minStars: stars, + pinPeriod: pinPeriod == 0 ? nil : 0, + maxMessageLength: maxMessageLength, + maxEmojiCount: maxEmojiCount, + color: colorBg + )) + } + } + if paramSets.isEmpty { + paramSets = [ + ParamSet( + minStars: 0, pinPeriod: nil, maxMessageLength: 30, maxEmojiCount: 0, color: nil + ), + ParamSet( + minStars: 1, pinPeriod: 60, maxMessageLength: 60, maxEmojiCount: 1, color: 0x985FDC + ), + ParamSet( + minStars: 50, pinPeriod: 120, maxMessageLength: 80, maxEmojiCount: 2, color: 0x3E9CDF + ), + ParamSet( + minStars: 100, pinPeriod: 300, maxMessageLength: 110, maxEmojiCount: 3, color: 0x5AB03D + ), + ParamSet( + minStars: 250, pinPeriod: 600, maxMessageLength: 150, maxEmojiCount: 4, color: 0xE4A20A + ), + ParamSet( + minStars: 500, pinPeriod: 900, maxMessageLength: 200, maxEmojiCount: 7, color: 0xEE7E20 + ), + ParamSet( + minStars: 2000, pinPeriod: 1800, maxMessageLength: 280, maxEmojiCount: 10, color: 0xE6514E + ), + ParamSet( + minStars: 10000, pinPeriod: 3600, maxMessageLength: 400, maxEmojiCount: 20, color: 0x7C8695 + ), + ] + } + paramSets.sort(by: { lhs, rhs in return lhs.minStars < rhs.minStars }) + self.paramSets = paramSets + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index dfbbebf1b1..bfa749ca11 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1652,8 +1652,8 @@ public extension TelegramEngine { return _internal_refreshGlobalPostSearchState(account: self.account) } - public func groupCallMessages(callId: Int64, reference: InternalGroupCallReference, e2eContext: ConferenceCallE2EContext?, messageLifetime: Int32, isLiveStream: Bool) -> GroupCallMessagesContext { - return GroupCallMessagesContext(account: self.account, callId: callId, reference: reference, e2eContext: e2eContext, messageLifetime: messageLifetime, isLiveStream: isLiveStream) + public func groupCallMessages(appConfig: AppConfiguration, callId: Int64, reference: InternalGroupCallReference, e2eContext: ConferenceCallE2EContext?, messageLifetime: Int32, isLiveStream: Bool) -> GroupCallMessagesContext { + return GroupCallMessagesContext(account: self.account, appConfig: appConfig, callId: callId, reference: reference, e2eContext: e2eContext, messageLifetime: messageLifetime, isLiveStream: isLiveStream) } } } diff --git a/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift b/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift index 0db3dca153..aeb5930a85 100644 --- a/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift +++ b/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift @@ -198,6 +198,9 @@ public final class AsyncListComponent: Component { if lhs.externalState !== rhs.externalState { return false } + if lhs.externalStateValue != rhs.externalStateValue { + return false + } if lhs.items != rhs.items { return false } diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 46661824be..0cd21cbe4b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -376,6 +376,7 @@ private final class PeerComponent: Component { let strings: PresentationStrings let peer: EnginePeer? let count: String + let topPlace: Int? let color: UIColor init( @@ -384,6 +385,7 @@ private final class PeerComponent: Component { strings: PresentationStrings, peer: EnginePeer?, count: String, + topPlace: Int?, color: UIColor ) { self.context = context @@ -391,6 +393,7 @@ private final class PeerComponent: Component { self.strings = strings self.peer = peer self.count = count + self.topPlace = topPlace self.color = color } @@ -410,6 +413,9 @@ private final class PeerComponent: Component { if lhs.count != rhs.count { return false } + if lhs.topPlace != rhs.topPlace { + return false + } if lhs.color != rhs.color { return false } @@ -419,6 +425,7 @@ private final class PeerComponent: Component { final class View: UIView { private var avatarNode: AvatarNode? private let badge = ComponentView() + private var crownIcon: UIImageView? private let title = ComponentView() private var component: PeerComponent? @@ -432,6 +439,7 @@ private final class PeerComponent: Component { } func update(component: PeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component self.component = component let avatarNode: AvatarNode @@ -471,6 +479,30 @@ private final class PeerComponent: Component { badgeView.frame = badgeFrame } + if let topPlace = component.topPlace { + let crownIcon: UIImageView + if let current = self.crownIcon { + crownIcon = current + } else { + crownIcon = UIImageView() + self.crownIcon = crownIcon + self.addSubview(crownIcon) + } + + if topPlace != previousComponent?.topPlace { + crownIcon.image = StoryLiveChatMessageComponent.generateCrownImage(place: topPlace, backgroundColor: component.color, foregroundColor: .white, borderColor: component.theme.actionSheet.opaqueItemBackgroundColor) + } + if let image = crownIcon.image { + let crownIconFrame = CGRect(origin: CGPoint(x: avatarFrame.minX + floor((avatarFrame.width - image.size.width) * 0.5), y: avatarFrame.minY - 13.0), size: image.size) + crownIcon.frame = crownIconFrame + } + } else { + if let crownIcon = self.crownIcon { + self.crownIcon = nil + crownIcon.removeFromSuperview() + } + } + let titleSpacing: CGFloat = 8.0 let peerTitle: String @@ -1120,7 +1152,10 @@ private final class ChatSendStarsScreenComponent: Component { var containerTransform = CATransform3DIdentity containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0) + #if DEBUG + #else containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) + #endif transition.setTransform(view: self.containerView, transform: containerTransform) transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius) } @@ -1613,12 +1648,12 @@ private final class ChatSendStarsScreenComponent: Component { self.isPastTopCutoff = nil } - if case .liveStream = reactData.reactSubject { - let color = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(self.amount.realValue)).color ?? .purple + if case let .liveStream(_, _, _, liveChatMessageParams) = reactData.reactSubject { + let color = GroupCallMessagesContext.getStarAmountParamMapping(params: liveChatMessageParams, value: Int64(self.amount.realValue)).color ?? GroupCallMessagesContext.Message.Color(rawValue: 0x985FDC) sliderColor = StoryLiveChatMessageComponent.getMessageColor(color: color) } - case .liveStreamMessage: - let color = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(self.amount.realValue)).color ?? .purple + case let .liveStreamMessage(liveStreamMessage): + let color = GroupCallMessagesContext.getStarAmountParamMapping(params: liveStreamMessage.liveChatMessageParams, value: Int64(self.amount.realValue)).color ?? GroupCallMessagesContext.Message.Color(rawValue: 0x985FDC) sliderColor = StoryLiveChatMessageComponent.getMessageColor(color: color) } @@ -1690,9 +1725,9 @@ private final class ChatSendStarsScreenComponent: Component { } switch component.initialData.subjectInitialData { - case .liveStreamMessage: + case let .liveStreamMessage(liveStreamMessageData): //TODO:localize - let params = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(self.amount.realValue)) + let params = GroupCallMessagesContext.getStarAmountParamMapping(params: liveStreamMessageData.liveChatMessageParams, value: Int64(self.amount.realValue)) var perks: [(String, String)] = [] perks.append(( @@ -1931,33 +1966,44 @@ private final class ChatSendStarsScreenComponent: Component { text = "Highlight and pin a message\nby adding Stars for **\(liveStreamMessageData.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast))**." } - let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor) - let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor) - - let descriptionTextSize = descriptionText.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .markdown(text: text, attributes: MarkdownAttributes( - body: body, - bold: bold, - link: body, - linkAttribute: { _ in nil } + let addDescriptionText: () -> Void = { + let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor) + + let descriptionTextSize = descriptionText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: text, attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + )), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 )), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - lineSpacing: 0.2 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0) - ) - let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) - if let descriptionTextView = descriptionText.view { - if descriptionTextView.superview == nil { - self.scrollContentView.addSubview(descriptionTextView) + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0) + ) + let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) + if let descriptionTextView = descriptionText.view { + if descriptionTextView.superview == nil { + self.scrollContentView.addSubview(descriptionTextView) + } + transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) + } + contentHeight += descriptionTextFrame.height + } + + switch component.initialData.subjectInitialData { + case .liveStreamMessage: + addDescriptionText() + case let .react(reactData): + if case .message = reactData.reactSubject { + addDescriptionText() } - transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) } - contentHeight += descriptionTextFrame.height var liveStreamMessagePreviewData: GroupCallMessagesContext.Message? if case let .liveStreamMessage(liveStreamMessage) = @@ -1994,8 +2040,10 @@ private final class ChatSendStarsScreenComponent: Component { ) } - if let liveStreamMessagePreviewData { - contentHeight += 29.0 + let addLiveStreamMessagePreview: (Int?) -> Void = { topIndex in + guard let liveStreamMessagePreviewData else { + return + } let liveStreamMessagePreview: ComponentView if let current = self.liveStreamMessagePreview { @@ -2018,6 +2066,7 @@ private final class ChatSendStarsScreenComponent: Component { transparentBackground: false ), message: liveStreamMessagePreviewData, + topPlace: topIndex, contextGesture: nil )), environment: {}, @@ -2031,11 +2080,21 @@ private final class ChatSendStarsScreenComponent: Component { transition.setFrame(view: liveStreamMessagePreviewView, frame: liveStreamMessagePreviewFrame) } contentHeight += liveStreamMessagePreviewSize.height - contentHeight += 28.0 - } else { - contentHeight += 24.0 } + if let _ = liveStreamMessagePreviewData, case .liveStreamMessage = component.initialData.subjectInitialData { + contentHeight += 29.0 + addLiveStreamMessagePreview(nil) + contentHeight += 10.0 + } else if case let .react(reactData) = component.initialData.subjectInitialData { + if case .liveStream = reactData.reactSubject { + contentHeight -= 3.0 + } else { + contentHeight += 24.0 + } + } + + var mappedTopPeers: [ChatSendStarsScreen.TopPeer] = [] switch component.initialData.subjectInitialData { case let .react(reactData): if !reactData.topPeers.isEmpty { @@ -2111,7 +2170,7 @@ private final class ChatSendStarsScreenComponent: Component { contentHeight += 60.0 } - var mappedTopPeers = reactData.topPeers + mappedTopPeers = reactData.topPeers if let index = mappedTopPeers.firstIndex(where: { $0.isMy }) { mappedTopPeers.remove(at: index) } @@ -2173,9 +2232,11 @@ private final class ChatSendStarsScreenComponent: Component { let itemCountString = presentationStringsFormattedNumber(Int32(topPeer.count), environment.dateTimeFormat.groupingSeparator) var peerColor: UIColor = UIColor(rgb: 0xFFB10D) - if case .liveStream = reactData.reactSubject { - let color = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(topPeer.count)).color ?? .purple + var topPlace: Int? + if case let .liveStream(_, _, _, liveChatMessageParams) = reactData.reactSubject { + let color = GroupCallMessagesContext.getStarAmountParamMapping(params: liveChatMessageParams, value: Int64(topPeer.count)).color ?? GroupCallMessagesContext.Message.Color(rawValue: 0x985FDC) peerColor = StoryLiveChatMessageComponent.getMessageColor(color: color) + topPlace = validIds.count - 1 } let itemSize = itemView.update( @@ -2187,6 +2248,7 @@ private final class ChatSendStarsScreenComponent: Component { strings: environment.strings, peer: topPeer.peer, count: itemCountString, + topPlace: topPlace, color: peerColor )), effectAlignment: .center, @@ -2390,6 +2452,28 @@ private final class ChatSendStarsScreenComponent: Component { break } + switch component.initialData.subjectInitialData { + case .liveStreamMessage: + break + case let .react(reactData): + if case .liveStream = reactData.reactSubject { + contentHeight += 14.0 + addDescriptionText() + } + } + + if let _ = liveStreamMessagePreviewData, case let .react(reactData) = component.initialData.subjectInitialData { + if case .liveStream = reactData.reactSubject { + contentHeight += 29.0 + } else { + contentHeight += 12.0 + } + addLiveStreamMessagePreview(mappedTopPeers.firstIndex(where: { $0.isMy })) + contentHeight += 18.0 + } else { + contentHeight += 18.0 + } + initialContentHeight = contentHeight if self.cachedStarImage == nil || self.cachedStarImage?.1 !== environment.theme { @@ -2627,7 +2711,7 @@ private final class ChatSendStarsScreenComponent: Component { public class ChatSendStarsScreen: ViewControllerComponentContainer { public enum ReactSubject { case message(EngineMessage.Id) - case liveStream(peerId: EnginePeer.Id, storyId: Int32, minAmount: Int) + case liveStream(peerId: EnginePeer.Id, storyId: Int32, minAmount: Int, liveChatMessageParams: LiveChatMessageParams) } fileprivate enum SubjectInitialData { @@ -2664,6 +2748,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { let myPeer: EnginePeer let minAmount: Int let maxAmount: Int + let liveChatMessageParams: LiveChatMessageParams let currentAmount: Int? let text: NSAttributedString let completion: (Int64, ChatSendStarsScreen.TransitionOut) -> Void @@ -2673,6 +2758,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { myPeer: EnginePeer, minAmount: Int, maxAmount: Int, + liveChatMessageParams: LiveChatMessageParams, currentAmount: Int?, text: NSAttributedString, completion: @escaping (Int64, ChatSendStarsScreen.TransitionOut) -> Void @@ -2681,6 +2767,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { self.myPeer = myPeer self.minAmount = minAmount self.maxAmount = maxAmount + self.liveChatMessageParams = liveChatMessageParams self.currentAmount = currentAmount self.text = text self.completion = completion @@ -2860,7 +2947,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { } var minAmount = 1 - if case let .liveStream(_, _, minAmountValue) = reactSubject { + if case let .liveStream(_, _, minAmountValue, _) = reactSubject { minAmount = minAmountValue } @@ -2990,6 +3077,7 @@ public class ChatSendStarsScreen: ViewControllerComponentContainer { myPeer: myPeer, minAmount: minAmount, maxAmount: maxAmount, + liveChatMessageParams: LiveChatMessageParams(appConfig: context.currentAppConfiguration.with({ $0 })), currentAmount: currentAmount, text: text, completion: completion diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatFloatingTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatFloatingTopicsPanel.swift index fea1f311ee..cc6b0ad431 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatFloatingTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatFloatingTopicsPanel.swift @@ -76,9 +76,8 @@ public final class ChatFloatingTopicsPanel: Component { public final class View: UIView { private let containerView: GlassBackgroundContainerView - private var sidePanelBackgroundView: GlassBackgroundView? + private var sharedPanelBackgroundView: GlassBackgroundView? private var sidePanel: ComponentView? - private var topPanelBackgroundView: GlassBackgroundView? private var topPanel: ComponentView? override public init(frame: CGRect) { @@ -106,6 +105,8 @@ public final class ChatFloatingTopicsPanel: Component { func update(component: ChatFloatingTopicsPanel, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ChatSidePanelEnvironment.self].value + var currentPanelBackgroundFrame: CGRect? + if case .side = component.location { let sidePanel: ComponentView var sidePanelTransition = transition @@ -116,13 +117,6 @@ public final class ChatFloatingTopicsPanel: Component { sidePanel = ComponentView() self.sidePanel = sidePanel } - let sidePanelBackgroundView: GlassBackgroundView - if let current = self.sidePanelBackgroundView { - sidePanelBackgroundView = current - } else { - sidePanelBackgroundView = GlassBackgroundView() - self.sidePanelBackgroundView = sidePanelBackgroundView - } let sidePanelSize = sidePanel.update( transition: sidePanelTransition, component: AnyComponent(ChatSideTopicsPanel( @@ -148,41 +142,22 @@ public final class ChatFloatingTopicsPanel: Component { }, containerSize: CGSize(width: 72.0 + 8.0, height: availableSize.height) ) - let sidePanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: 8.0 + 80.0, height: availableSize.height - 8.0 - 8.0 - environment.insets.bottom)) - let sidePanelBackgroundFrame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: 80.0, height: availableSize.height - 8.0 - 8.0 - 8.0 - environment.insets.bottom)) + let sidePanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: 8.0 + 80.0, height: availableSize.height - 8.0 - environment.insets.bottom)) + let sidePanelBackgroundFrame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: 80.0, height: availableSize.height - 8.0 - 8.0 - environment.insets.bottom)) + currentPanelBackgroundFrame = sidePanelBackgroundFrame if let sidePanelView = sidePanel.view as? ChatSideTopicsPanel.View { if sidePanelView.superview == nil { sidePanelView.layer.cornerRadius = 20.0 sidePanelView.clipsToBounds = true self.addSubview(sidePanelView) - self.containerView.contentView.addSubview(sidePanelBackgroundView) sidePanelView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: sidePanelSize.height, height: 8.0 + 40.0)) - - sidePanelBackgroundView.frame = CGRect(origin: sidePanelBackgroundFrame.origin, size: CGSize(width: sidePanelBackgroundFrame.width, height: 40.0)) - sidePanelBackgroundView.update(size: sidePanelBackgroundView.frame.size, cornerRadius: 20.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: .immediate) } - transition.setFrame(view: sidePanelView, frame: sidePanelFrame, completion: { [weak sidePanelView] flag in - if flag { - sidePanelView?.clipsToBounds = false - } - }) - - transition.setFrame(view: sidePanelBackgroundView, frame: sidePanelBackgroundFrame) - sidePanelBackgroundView.update(size: sidePanelBackgroundFrame.size, cornerRadius: 20.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition) + transition.setFrame(view: sidePanelView, frame: sidePanelFrame) } } else if let sidePanel = self.sidePanel { self.sidePanel = nil if let sidePanelView = sidePanel.view as? ChatSideTopicsPanel.View { - let sidePanelBackgroundView = self.sidePanelBackgroundView - self.sidePanelBackgroundView = nil - if let sidePanelBackgroundView { - transition.setFrame(view: sidePanelBackgroundView, frame: CGRect(origin: sidePanelBackgroundView.frame.origin, size: CGSize(width: sidePanelBackgroundView.bounds.width, height: 40.0)), completion: { [weak sidePanelBackgroundView] _ in - sidePanelBackgroundView?.removeFromSuperview() - }) - sidePanelBackgroundView.update(size: sidePanelBackgroundView.bounds.size, cornerRadius: 20.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition) - } - sidePanelView.clipsToBounds = true transition.setFrame(view: sidePanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: sidePanelView.bounds.width, height: 8.0 + 40.0)), completion: { [weak sidePanelView] _ in sidePanelView?.removeFromSuperview() @@ -200,13 +175,6 @@ public final class ChatFloatingTopicsPanel: Component { topPanel = ComponentView() self.topPanel = topPanel } - let topPanelBackgroundView: GlassBackgroundView - if let current = self.topPanelBackgroundView { - topPanelBackgroundView = current - } else { - topPanelBackgroundView = GlassBackgroundView() - self.topPanelBackgroundView = topPanelBackgroundView - } let _ = topPanel.update( transition: topPanelTransition, component: AnyComponent(ChatSideTopicsPanel( @@ -234,37 +202,19 @@ public final class ChatFloatingTopicsPanel: Component { ) let topPanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width - 8.0, height: 8.0 + 40.0)) let topPanelBackgroundFrame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: availableSize.width - 8.0 - 8.0, height: 40.0)) + currentPanelBackgroundFrame = topPanelBackgroundFrame if let topPanelView = topPanel.view as? ChatSideTopicsPanel.View { if topPanelView.superview == nil { topPanelView.clipsToBounds = true topPanelView.layer.cornerRadius = 20.0 self.addSubview(topPanelView) - self.containerView.contentView.addSubview(topPanelBackgroundView) topPanelView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 80.0 + 8.0, height: topPanelFrame.height)) - - topPanelBackgroundView.frame = CGRect(origin: topPanelBackgroundFrame.origin, size: CGSize(width: 80.0, height: topPanelBackgroundFrame.height)) - topPanelBackgroundView.update(size: topPanelBackgroundView.bounds.size, cornerRadius: 20.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: .immediate) } - transition.setFrame(view: topPanelView, frame: topPanelFrame, completion: { [weak topPanelView] flag in - if flag { - topPanelView?.clipsToBounds = false - } - }) - - transition.setFrame(view: topPanelBackgroundView, frame: topPanelBackgroundFrame) - topPanelBackgroundView.update(size: topPanelBackgroundFrame.size, cornerRadius: 20.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition) + transition.setFrame(view: topPanelView, frame: topPanelFrame) } } else if let topPanel = self.topPanel { self.topPanel = nil if let topPanelView = topPanel.view as? ChatSideTopicsPanel.View { - if let topPanelBackgroundView = self.topPanelBackgroundView { - self.topPanelBackgroundView = nil - - transition.setFrame(view: topPanelBackgroundView, frame: CGRect(origin: topPanelBackgroundView.frame.origin, size: CGSize(width: 80.0, height: topPanelBackgroundView.bounds.height)), completion: { [weak topPanelBackgroundView] _ in - topPanelBackgroundView?.removeFromSuperview() - }) - topPanelBackgroundView.update(size: topPanelBackgroundView.bounds.size, cornerRadius: 20.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition) - } topPanelView.clipsToBounds = true transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 8.0 + 72.0, height: topPanelView.bounds.height)), completion: { [weak topPanelView] _ in topPanelView?.removeFromSuperview() @@ -272,6 +222,20 @@ public final class ChatFloatingTopicsPanel: Component { } } + if let currentPanelBackgroundFrame { + let sharedPanelBackgroundView: GlassBackgroundView + if let current = self.sharedPanelBackgroundView { + sharedPanelBackgroundView = current + } else { + sharedPanelBackgroundView = GlassBackgroundView() + self.sharedPanelBackgroundView = sharedPanelBackgroundView + self.containerView.contentView.insertSubview(sharedPanelBackgroundView, at: 0) + } + + transition.setFrame(view: sharedPanelBackgroundView, frame: currentPanelBackgroundFrame) + sharedPanelBackgroundView.update(size: currentPanelBackgroundFrame.size, cornerRadius: 20.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition) + } + transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(), size: availableSize)) self.containerView.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition) diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index ae42042d98..1661246cc6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -332,8 +332,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg private let hapticFeedback = HapticFeedback() - private var currentInputHasText: Bool = false - public var inputTextState: ChatTextInputState { if let textInputNode = self.textInputNode { let selectionRange: Range = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length) @@ -1517,16 +1515,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { inputHasText = true } - let inputHadText = self.currentInputHasText - self.currentInputHasText = inputHasText - - var useBounceAnimation = inputHasText && !inputHadText - if accessoryPanel != nil || self.accessoryPanel != nil { - useBounceAnimation = false - } - if !self.enableBounceAnimations { - useBounceAnimation = false - } var hasMenuButton = false var menuButtonExpanded = false @@ -2728,10 +2716,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg self.updateCounterTextNode(backgroundSize: textInputContainerBackgroundFrame.size, transition: transition) - var textInputContainerBackgroundTransition = ComponentTransition(transition) - if useBounceAnimation, case let .animated(_, curve) = transition, case .spring = curve { - textInputContainerBackgroundTransition = textInputContainerBackgroundTransition.withUserData(GlassBackgroundView.TransitionFlagBounce()) - } + let textInputContainerBackgroundTransition = ComponentTransition(transition) self.textInputContainerBackgroundView.update(size: textInputContainerBackgroundFrame.size, cornerRadius: floor(minimalInputHeight * 0.5), isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), isInteractive: true, transition: textInputContainerBackgroundTransition) transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: textInputContainerBackgroundFrame) @@ -3018,7 +3003,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } if starReactionButtonView.superview == nil { - self.glassBackgroundContainer.contentView.addSubview(starReactionButtonView) + self.view.addSubview(starReactionButtonView) if transition.isAnimated { starReactionButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) transition.animateTransformScale(view: starReactionButtonView, from: 0.001) @@ -3038,13 +3023,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg sendActionButtonsFrame.origin.x += (sendActionButtonsSize.width - 3.0 * 2.0) * 0.5 - 3.0 } - if useBounceAnimation, case let .animated(duration, curve) = transition, case .spring = curve { - ContainedViewLayoutTransition.animated(duration: duration, curve: curve).updateScaleSpring(layer: self.sendActionButtons.layer, scale: sendActionsScale) - ContainedViewLayoutTransition.animated(duration: duration, curve: curve).updatePositionSpring(layer: self.sendActionButtons.layer, position: sendActionButtonsFrame.center) - } else { - transition.updateTransformScale(node: self.sendActionButtons, scale: CGPoint(x: sendActionsScale, y: sendActionsScale)) - transition.updatePosition(node: self.sendActionButtons, position: sendActionButtonsFrame.center) - } + transition.updateTransformScale(node: self.sendActionButtons, scale: CGPoint(x: sendActionsScale, y: sendActionsScale)) + transition.updatePosition(node: self.sendActionButtons, position: sendActionButtonsFrame.center) + transition.updateBounds(node: self.sendActionButtons, bounds: CGRect(origin: CGPoint(), size: sendActionButtonsFrame.size)) if let (rect, containerSize) = self.absoluteRect { self.sendActionButtons.updateAbsoluteRect(CGRect(x: rect.origin.x + sendActionButtonsFrame.origin.x, y: rect.origin.y + sendActionButtonsFrame.origin.y, width: sendActionButtonsFrame.width, height: sendActionButtonsFrame.height), within: containerSize, transition: transition) diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift index 2e9ac46f63..62a6ab63e2 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/StarReactionButtonComponent.swift @@ -73,7 +73,7 @@ final class StarReactionButtonBadgeComponent: Component { containerSize: CGSize(width: 100.0, height: 100.0) ) - let size = CGSize(width: textSize.width + sideInset * 2.0, height: height) + let size = CGSize(width: max(height, textSize.width + sideInset * 2.0), height: height) let backgroundFrame = CGRect(origin: CGPoint(), size: size) let backgroundTintColor: GlassBackgroundView.TintColor @@ -86,7 +86,7 @@ final class StarReactionButtonBadgeComponent: Component { self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: true, transition: transition) if let textView = self.text.view { - let textFrame = textSize.centered(in: CGRect(origin: CGPoint(), size: size)) + let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundFrame.width - textSize.width) * 0.5), y: floorToScreenPixels((backgroundFrame.height - textSize.height) * 0.5)), size: textSize) if textView.superview == nil { textView.isUserInteractionEnabled = false @@ -147,10 +147,11 @@ final class StarReactionButtonComponent: Component { } final class View: UIView { + private let containerView: UIView private let backgroundView: GlassBackgroundView private let backgroundEffectLayer: StarsParticleEffectLayer private let backgroundMaskView: UIView - private let backgroundBadgeMask: UIImageView + private var backgroundBadgeMask: UIImageView? private let iconView: UIImageView private var badge: ComponentView? @@ -160,15 +161,14 @@ final class StarReactionButtonComponent: Component { private weak var state: EmptyComponentState? override init(frame: CGRect) { + self.containerView = UIView() self.backgroundView = GlassBackgroundView() self.backgroundMaskView = UIView() - self.backgroundBadgeMask = UIImageView() - self.backgroundMaskView.addSubview(self.backgroundBadgeMask) self.backgroundEffectLayer = StarsParticleEffectLayer() self.backgroundView.contentView.layer.addSublayer(self.backgroundEffectLayer) - //self.backgroundView.mask = self.backgroundMaskView + self.backgroundView.mask = self.backgroundMaskView self.backgroundMaskView.backgroundColor = .white if let filter = CALayer.luminanceToAlpha() { @@ -179,13 +179,31 @@ final class StarReactionButtonComponent: Component { super.init(frame: frame) - self.addSubview(self.backgroundView) + self.addSubview(self.containerView) + self.containerView.addSubview(self.backgroundView) self.backgroundView.contentView.addSubview(self.iconView) let longTapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.longTapAction(_:))) longTapRecognizer.tapActionAtPoint = { _ in return .waitForSingleTap } + longTapRecognizer.highlight = { [weak self] point in + guard let self else { + return + } + + let currentTransform: CATransform3D + if self.containerView.layer.animation(forKey: "transform") != nil || self.containerView.layer.animation(forKey: "transform.scale") != nil { + currentTransform = self.containerView.layer.presentation()?.transform ?? layer.transform + } else { + currentTransform = self.containerView.layer.transform + } + let currentScale = sqrt((currentTransform.m11 * currentTransform.m11) + (currentTransform.m12 * currentTransform.m12) + (currentTransform.m13 * currentTransform.m13)) + let updatedScale: CGFloat = point != nil ? 1.35 : 1.0 + + self.containerView.layer.transform = CATransform3DMakeScale(updatedScale, updatedScale, 1.0) + self.containerView.layer.animateSpring(from: currentScale as NSNumber, to: updatedScale as NSNumber, keyPath: "transform.scale", duration: point != nil ? 0.4 : 0.8, damping: 70.0) + } self.longTapRecognizer = longTapRecognizer self.backgroundView.contentView.addGestureRecognizer(longTapRecognizer) } @@ -213,6 +231,7 @@ final class StarReactionButtonComponent: Component { } func update(component: StarReactionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component self.component = component self.state = state @@ -232,6 +251,15 @@ final class StarReactionButtonComponent: Component { badge = ComponentView() self.badge = badge } + + let backgroundBadgeMask: UIImageView + if let current = self.backgroundBadgeMask { + backgroundBadgeMask = current + } else { + backgroundBadgeMask = UIImageView() + self.backgroundBadgeMask = backgroundBadgeMask + } + let badgeSize = badge.update( transition: badgeTransition, component: AnyComponent(StarReactionButtonBadgeComponent( @@ -250,19 +278,54 @@ final class StarReactionButtonComponent: Component { if badgeView.superview == nil { badgeView.isUserInteractionEnabled = false - self.backgroundView.contentView.addSubview(badgeView) + self.containerView.addSubview(badgeView) badgeView.frame = badgeFrame transition.animateScale(view: badgeView, from: 0.001, to: 1.0) transition.animateAlpha(view: badgeView, from: 0.0, to: 1.0) } transition.setFrame(view: badgeView, frame: badgeFrame) + + let badgeBorderWidth: CGFloat = 1.0 + if backgroundBadgeMask.image?.size.height != (badgeFrame.height + badgeBorderWidth * 2.0) { + backgroundBadgeMask.image = generateStretchableFilledCircleImage(diameter: badgeFrame.height + badgeBorderWidth * 2.0, color: .black) + } + let backgroundBadgeFrame = badgeFrame.insetBy(dx: -badgeBorderWidth, dy: -badgeBorderWidth) + if backgroundBadgeMask.superview == nil { + self.backgroundMaskView.addSubview(backgroundBadgeMask) + backgroundBadgeMask.frame = backgroundBadgeFrame + transition.animateScale(view: backgroundBadgeMask, from: 0.001, to: 1.0) + transition.animateAlpha(view: backgroundBadgeMask, from: 0.0, to: 1.0) + } + transition.setFrame(view: backgroundBadgeMask, frame: backgroundBadgeFrame) } - } else if let badge = self.badge { - self.badge = nil - if let badgeView = badge.view { - transition.setScale(view: badgeView, scale: 0.001) - transition.setAlpha(view: badgeView, alpha: 0.0, completion: { [weak badgeView] _ in - badgeView?.removeFromSuperview() + } else { + if let badge = self.badge { + if let previousComponent { + let _ = badge.update( + transition: transition, + component: AnyComponent(StarReactionButtonBadgeComponent( + theme: component.theme, + count: previousComponent.count, + isFilled: previousComponent.isFilled + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + } + + self.badge = nil + if let badgeView = badge.view { + transition.setScale(view: badgeView, scale: 0.001) + transition.setAlpha(view: badgeView, alpha: 0.0, completion: { [weak badgeView] _ in + badgeView?.removeFromSuperview() + }) + } + } + if let backgroundBadgeMask = self.backgroundBadgeMask { + self.backgroundBadgeMask = nil + transition.setScale(view: backgroundBadgeMask, scale: 0.001) + transition.setAlpha(view: backgroundBadgeMask, alpha: 0.0, completion: { [weak backgroundBadgeMask] _ in + backgroundBadgeMask?.removeFromSuperview() }) } } @@ -276,21 +339,13 @@ final class StarReactionButtonComponent: Component { backgroundTintColor = .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)) } - self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: true, transition: transition) + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: backgroundTintColor, isInteractive: false, transition: transition) transition.setFrame(view: self.backgroundView, frame: backgroundFrame) transition.setFrame(view: self.backgroundMaskView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) transition.setFrame(layer: self.backgroundEffectLayer, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) self.backgroundEffectLayer.update(color: UIColor(white: 1.0, alpha: 0.25), rate: 10.0, size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, transition: transition) - let badgeDiameter: CGFloat = 15.0 - if self.backgroundBadgeMask.image == nil { - self.backgroundBadgeMask.image = generateStretchableFilledCircleImage(diameter: badgeDiameter + 1.0 * 2.0, color: .black) - } - let badgeWidth: CGFloat = 20.0 - let badgeFrame = CGRect(origin: CGPoint(x: backgroundFrame.width - badgeWidth, y: 0.0), size: CGSize(width: badgeWidth, height: badgeDiameter)) - transition.setFrame(view: self.backgroundBadgeMask, frame: badgeFrame.insetBy(dx: -1.0, dy: -1.0)) - self.iconView.tintColor = component.theme.chat.inputPanel.panelControlColor if let image = self.iconView.image { @@ -298,6 +353,9 @@ final class StarReactionButtonComponent: Component { transition.setFrame(view: self.iconView, frame: iconFrame) } + transition.setPosition(view: self.containerView, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: size)) + return size } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionScreen.swift index f80f38f7dd..9fe1fd8338 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionScreen.swift @@ -1615,7 +1615,8 @@ private final class GiftAuctionScreenComponent: Component { var sliderColor: UIColor = UIColor(rgb: 0xFFB10D) - let color = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(self.amount.realValue)).color ?? .purple + let liveStreamParams = LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 })) + let color = GroupCallMessagesContext.getStarAmountParamMapping(params: liveStreamParams, value: Int64(self.amount.realValue)).color ?? GroupCallMessagesContext.Message.Color(rawValue: 0x985FDC) sliderColor = StoryLiveChatMessageComponent.getMessageColor(color: color) let _ = self.sliderBackground.update( @@ -2259,7 +2260,7 @@ private final class GiftAuctionScreenComponent: Component { // // var peerColor: UIColor = UIColor(rgb: 0xFFB10D) // if case .liveStream = reactData.reactSubject { -// let color = GroupCallMessagesContext.getStarAmountParamMapping(value: Int64(topPeer.count)).color ?? .purple +// let color = GroupCallMessagesContext.getStarAmountParamMapping(appConfig: component.context.currentAppConfiguration.with({ $0}), value: Int64(topPeer.count)).color ?? .purple // peerColor = StoryLiveChatMessageComponent.getMessageColor(color: color) // } // diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index fb7e2f54cb..6c2fd0eb8a 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -48,11 +48,6 @@ private final class ContentContainer: UIView { } public class GlassBackgroundView: UIView { - public final class TransitionFlagBounce { - public init() { - } - } - public protocol ContentView: UIView { var tintMask: UIView { get } } @@ -266,14 +261,37 @@ public class GlassBackgroundView: UIView { } } + public enum Shape: Equatable { + case roundedRect(cornerRadius: CGFloat) + } + + private final class ClippingShapeContext { + let view: UIView + + private(set) var shape: Shape? + + init(view: UIView) { + self.view = view + } + + func update(shape: Shape, size: CGSize, transition: ComponentTransition) { + self.shape = shape + + switch shape { + case let .roundedRect(cornerRadius): + transition.setCornerRadius(layer: self.view.layer, cornerRadius: cornerRadius) + } + } + } + public struct Params: Equatable { - public let cornerRadius: CGFloat + public let shape: Shape public let isDark: Bool public let tintColor: TintColor public let isInteractive: Bool - init(cornerRadius: CGFloat, isDark: Bool, tintColor: TintColor, isInteractive: Bool) { - self.cornerRadius = cornerRadius + init(shape: Shape, isDark: Bool, tintColor: TintColor, isInteractive: Bool) { + self.shape = shape self.isDark = isDark self.tintColor = tintColor self.isInteractive = isInteractive @@ -281,7 +299,9 @@ public class GlassBackgroundView: UIView { } private let backgroundNode: NavigationBackgroundNode? + private let nativeView: UIVisualEffectView? + private let nativeViewClippingContext: ClippingShapeContext? private let nativeParamsView: EffectSettingsContainerView? private let foregroundView: UIImageView? @@ -312,6 +332,7 @@ public class GlassBackgroundView: UIView { let glassEffect = UIGlassEffect(style: .regular) glassEffect.isInteractive = false let nativeView = UIVisualEffectView(effect: glassEffect) + self.nativeViewClippingContext = ClippingShapeContext(view: nativeView) self.nativeView = nativeView let nativeParamsView = EffectSettingsContainerView(frame: CGRect()) @@ -322,8 +343,10 @@ public class GlassBackgroundView: UIView { self.foregroundView = nil self.shadowView = nil } else { - self.backgroundNode = NavigationBackgroundNode(color: .black, enableBlur: true, customBlurRadius: 8.0) + let backgroundNode = NavigationBackgroundNode(color: .black, enableBlur: true, customBlurRadius: 8.0) + self.backgroundNode = backgroundNode self.nativeView = nil + self.nativeViewClippingContext = nil self.nativeParamsView = nil self.foregroundView = UIImageView() @@ -377,27 +400,27 @@ public class GlassBackgroundView: UIView { } public func update(size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, transition: ComponentTransition) { - if let nativeView = self.nativeView, nativeView.bounds.size != size { + self.update(size: size, shape: .roundedRect(cornerRadius: cornerRadius), isDark: isDark, tintColor: tintColor, isInteractive: isInteractive, transition: transition) + } + + public func update(size: CGSize, shape: Shape, isDark: Bool, tintColor: TintColor, isInteractive: Bool = false, transition: ComponentTransition) { + if let nativeView = self.nativeView, let nativeViewClippingContext = self.nativeViewClippingContext, (nativeView.bounds.size != size || nativeViewClippingContext.shape != shape) { + nativeViewClippingContext.update(shape: shape, size: size, transition: transition) if transition.animation.isImmediate { - nativeView.layer.cornerRadius = cornerRadius nativeView.frame = CGRect(origin: CGPoint(), size: size) } else { - transition.setCornerRadius(layer: nativeView.layer, cornerRadius: cornerRadius) - let nativeFrame = CGRect(origin: CGPoint(), size: size) - - if transition.userData(TransitionFlagBounce.self) != nil { - transition.containedViewLayoutTransition.updatePositionSpring(layer: nativeView.layer, position: nativeFrame.center) - transition.containedViewLayoutTransition.updateBoundsSpring(layer: nativeView.layer, bounds: CGRect(origin: CGPoint(), size: nativeFrame.size)) - } else { - transition.setFrame(view: nativeView, frame: nativeFrame) - } + transition.setFrame(view: nativeView, frame: nativeFrame) } } if let backgroundNode = self.backgroundNode { backgroundNode.updateColor(color: .clear, forceKeepBlur: tintColor.color.alpha != 1.0, transition: transition.containedViewLayoutTransition) - backgroundNode.update(size: size, cornerRadius: cornerRadius, transition: transition.containedViewLayoutTransition) + + switch shape { + case let .roundedRect(cornerRadius): + backgroundNode.update(size: size, cornerRadius: cornerRadius, transition: transition.containedViewLayoutTransition) + } transition.setFrame(view: backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size)) } @@ -442,13 +465,19 @@ public class GlassBackgroundView: UIView { innerBackgroundView.removeFromSuperview() } - let params = Params(cornerRadius: cornerRadius, isDark: isDark, tintColor: tintColor, isInteractive: isInteractive) + let params = Params(shape: shape, isDark: isDark, tintColor: tintColor, isInteractive: isInteractive) if self.params != params { self.params = params + let outerCornerRadius: CGFloat + switch shape { + case let .roundedRect(cornerRadius): + outerCornerRadius = cornerRadius + } + if let shadowView = self.shadowView { let shadowInnerInset: CGFloat = 0.5 - shadowView.image = generateImage(CGSize(width: shadowInset * 2.0 + cornerRadius * 2.0, height: shadowInset * 2.0 + cornerRadius * 2.0), rotatedContext: { size, context in + shadowView.image = generateImage(CGSize(width: shadowInset * 2.0 + outerCornerRadius * 2.0, height: shadowInset * 2.0 + outerCornerRadius * 2.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.black.cgColor) @@ -458,11 +487,11 @@ public class GlassBackgroundView: UIView { context.setFillColor(UIColor.clear.cgColor) context.setBlendMode(.copy) context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowInset + shadowInnerInset, y: shadowInset + shadowInnerInset), size: CGSize(width: size.width - shadowInset * 2.0 - shadowInnerInset * 2.0, height: size.height - shadowInset * 2.0 - shadowInnerInset * 2.0))) - })?.stretchableImage(withLeftCapWidth: Int(shadowInset + cornerRadius), topCapHeight: Int(shadowInset + cornerRadius)) + })?.stretchableImage(withLeftCapWidth: Int(shadowInset + outerCornerRadius), topCapHeight: Int(shadowInset + outerCornerRadius)) } if let foregroundView = self.foregroundView { - foregroundView.image = GlassBackgroundView.generateLegacyGlassImage(size: CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0), inset: shadowInset, isDark: isDark, fillColor: tintColor.color) + foregroundView.image = GlassBackgroundView.generateLegacyGlassImage(size: CGSize(width: outerCornerRadius * 2.0, height: outerCornerRadius * 2.0), inset: shadowInset, isDark: isDark, fillColor: tintColor.color) } else { if let nativeParamsView = self.nativeParamsView, let nativeView = self.nativeView { if #available(iOS 26.0, *) { @@ -475,7 +504,13 @@ public class GlassBackgroundView: UIView { } glassEffect.isInteractive = params.isInteractive - nativeView.effect = glassEffect + if transition.animation.isImmediate { + nativeView.effect = glassEffect + } else { + UIView.animate(withDuration: 0.2, animations: { + nativeView.effect = glassEffect + }) + } if isDark { nativeParamsView.lumaMin = 0.0 diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index ec7b716d98..0e97c31965 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -183,11 +183,11 @@ public final class MessageInputPanelComponent: Component { } public struct StarStats: Equatable { - public var myStars: Int64 + public var hasOutgoingStars: Bool public var totalStars: Int64 - public init(myStars: Int64, totalStars: Int64) { - self.myStars = myStars + public init(hasOutgoingStars: Bool, totalStars: Int64) { + self.hasOutgoingStars = hasOutgoingStars self.totalStars = totalStars } } @@ -1031,7 +1031,7 @@ public final class MessageInputPanelComponent: Component { } component.toggleLiveChatExpanded?() }), - rightAction: ChatTextInputPanelComponent.RightAction(kind: .stars(count: Int(component.starStars?.totalStars ?? 0), isFilled: (component.starStars?.myStars ?? 0) != 0), action: { [weak self] sourceView in + rightAction: ChatTextInputPanelComponent.RightAction(kind: .stars(count: Int(component.starStars?.totalStars ?? 0), isFilled: component.starStars?.hasOutgoingStars ?? false), action: { [weak self] sourceView in guard let self, let component = self.component else { return } @@ -1046,7 +1046,8 @@ public final class MessageInputPanelComponent: Component { placeholder: placeholder, paidMessagePrice: component.sendPaidMessageStars, sendColor: component.sendPaidMessageStars.flatMap { value in - let color = GroupCallMessagesContext.getStarAmountParamMapping(value: value.value).color ?? .purple + let params = LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 })) + let color = GroupCallMessagesContext.getStarAmountParamMapping(params: params, value: value.value).color ?? GroupCallMessagesContext.Message.Color(rawValue: 0x985FDC) return StoryLiveChatMessageComponent.getMessageColor(color: color) }, isSendDisabled: isSendDisabled, diff --git a/submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/Sources/StoryLiveChatMessageComponent.swift b/submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/Sources/StoryLiveChatMessageComponent.swift index 6c9442163e..97487321b8 100644 --- a/submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/Sources/StoryLiveChatMessageComponent.swift +++ b/submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent/Sources/StoryLiveChatMessageComponent.swift @@ -35,6 +35,7 @@ public final class StoryLiveChatMessageComponent: Component { let theme: PresentationTheme let layout: Layout let message: GroupCallMessagesContext.Message + let topPlace: Int? let contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? public init( @@ -43,6 +44,7 @@ public final class StoryLiveChatMessageComponent: Component { theme: PresentationTheme, layout: Layout, message: GroupCallMessagesContext.Message, + topPlace: Int?, contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? ) { self.context = context @@ -50,6 +52,7 @@ public final class StoryLiveChatMessageComponent: Component { self.theme = theme self.layout = layout self.message = message + self.topPlace = topPlace self.contextGesture = contextGesture } @@ -72,6 +75,9 @@ public final class StoryLiveChatMessageComponent: Component { if lhs.message != rhs.message { return false } + if lhs.topPlace != rhs.topPlace { + return false + } return true } @@ -83,6 +89,7 @@ public final class StoryLiveChatMessageComponent: Component { private var avatarNode: AvatarNode? private let textExternal = MultilineTextWithEntitiesComponent.External() private let text = ComponentView() + private var crownIcon: UIImageView? private var backgroundView: UIImageView? private var effectLayer: StarsParticleEffectLayer? private var starsAmountBackgroundView: UIImageView? @@ -142,6 +149,7 @@ public final class StoryLiveChatMessageComponent: Component { self.isUpdating = false } + let previousComponent = self.component self.component = component self.state = state @@ -200,7 +208,7 @@ public final class StoryLiveChatMessageComponent: Component { } } - if displayStarsAmountBackground, let paidStars = component.message.paidStars, let baseColor = GroupCallMessagesContext.getStarAmountParamMapping(value: paidStars).color { + if displayStarsAmountBackground, let paidStars = component.message.paidStars, let baseColor = GroupCallMessagesContext.getStarAmountParamMapping(params: LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 })), value: paidStars).color { let starsAmountBackgroundView: UIImageView if let current = self.starsAmountBackgroundView { starsAmountBackgroundView = current @@ -230,15 +238,47 @@ public final class StoryLiveChatMessageComponent: Component { textString.append(NSAttributedString(string: component.message.text, font: Font.regular(15.0), textColor: primaryTextColor)) } - var textCutout: TextNodeCutout? - if let starsAmountTextSize { - var cutoutWidth: CGFloat = starsAmountTextSize.width + 20.0 - if displayStarsAmountBackground { - cutoutWidth += 10.0 + var textTopLeftCutout: CGFloat? + if let topPlace = component.topPlace { + let crownIcon: UIImageView + if let current = self.crownIcon { + crownIcon = current + } else { + crownIcon = UIImageView() + self.crownIcon = crownIcon + self.extractedContainerNode.contentNode.view.addSubview(crownIcon) + } + if topPlace != previousComponent?.topPlace { + crownIcon.image = generateCrownImage(place: topPlace, backgroundColor: .white, foregroundColor: .clear, borderColor: nil) + } + crownIcon.tintColor = secondaryTextColor + + if let image = crownIcon.image { + textTopLeftCutout = image.size.width + 4.0 + } + } else { + if let crownIcon = self.crownIcon { + self.crownIcon = nil + crownIcon.removeFromSuperview() } - textCutout = TextNodeCutout(bottomRight: CGSize(width: cutoutWidth, height: 4.0)) } + var textBottomRightCutout: CGFloat? + if let starsAmountTextSize, !displayStarsAmountBackground { + textBottomRightCutout = starsAmountTextSize.width + 20.0 + } + + var textCutout: TextNodeCutout? + if textBottomRightCutout != nil || textTopLeftCutout != nil { + textCutout = TextNodeCutout(topLeft: textTopLeftCutout.flatMap({ CGSize(width: $0, height: 8.0) }), bottomRight: textBottomRightCutout.flatMap({ CGSize(width: $0, height: 8.0) })) + } + + var maxTextWidth: CGFloat = availableSize.width - insets.left - insets.right - avatarSize - avatarSpacing + if let starsAmountTextSize, displayStarsAmountBackground { + var cutoutWidth: CGFloat = starsAmountTextSize.width + 20.0 + cutoutWidth += 30.0 + maxTextWidth -= cutoutWidth + } let textSize = self.text.update( transition: .immediate, component: AnyComponent(MultilineTextWithEntitiesComponent( @@ -253,7 +293,7 @@ public final class StoryLiveChatMessageComponent: Component { cutout: textCutout )), environment: {}, - containerSize: CGSize(width: availableSize.width - insets.left - insets.right - avatarSize - avatarSpacing, height: 100000.0) + containerSize: CGSize(width: maxTextWidth, height: 100000.0) ) var avatarFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: avatarSize, height: avatarSize)) @@ -285,7 +325,7 @@ public final class StoryLiveChatMessageComponent: Component { } } - let textFrame = CGRect(origin: CGPoint(x: insets.left + avatarSize + avatarSpacing, y: avatarFrame.minY + 4.0), size: textSize) + let textFrame = CGRect(origin: CGPoint(x: insets.left + avatarSize + avatarSpacing, y: avatarFrame.minY + 3.0), size: textSize) if let textView = self.text.view { if textView.superview == nil { textView.layer.anchorPoint = CGPoint() @@ -295,8 +335,15 @@ public final class StoryLiveChatMessageComponent: Component { textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) } + if let crownIcon = self.crownIcon, let image = crownIcon.image { + crownIcon.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.minY - 1.0), size: image.size) + } + let backgroundOrigin = CGPoint(x: avatarFrame.minX - avatarBackgroundInset, y: avatarFrame.minY - avatarBackgroundInset) var backgroundFrame = CGRect(origin: backgroundOrigin, size: CGSize(width: textFrame.maxX + 8.0 - backgroundOrigin.x, height: avatarFrame.maxY + avatarBackgroundInset - backgroundOrigin.y)) + if let starsAmountTextSize, displayStarsAmountBackground { + backgroundFrame.size.width += starsAmountTextSize.width + 30.0 + } if let textLayout = self.textExternal.layout { if textLayout.numberOfLines > 1 { backgroundFrame.size.height = max(backgroundFrame.size.height, textFrame.maxY + 8.0 - backgroundOrigin.y) @@ -333,7 +380,7 @@ public final class StoryLiveChatMessageComponent: Component { let backgroundCornerRadius = (avatarSize + avatarBackgroundInset * 2.0) * 0.5 - if let paidStars = component.message.paidStars, let baseColor = GroupCallMessagesContext.getStarAmountParamMapping(value: paidStars).color { + if let paidStars = component.message.paidStars, let baseColor = GroupCallMessagesContext.getStarAmountParamMapping(params: LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 })), value: paidStars).color { let backgroundView: UIImageView if let current = self.backgroundView { backgroundView = current @@ -374,7 +421,7 @@ public final class StoryLiveChatMessageComponent: Component { self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) - self.extractedContainerNode.contentRect = backgroundFrame.insetBy(dx: -4.0, dy: 0.0) + self.extractedContainerNode.contentRect = backgroundFrame.insetBy(dx: -2.0, dy: 0.0) self.containerNode.frame = CGRect(origin: CGPoint(), size: size) return size @@ -390,21 +437,98 @@ public final class StoryLiveChatMessageComponent: Component { } public static func getMessageColor(color: GroupCallMessagesContext.Message.Color) -> UIColor { - switch color { - case .silver: - return UIColor(rgb: 0x7C8695) - case .red: - return UIColor(rgb: 0xE6514E) - case .orange: - return UIColor(rgb: 0xEE7E20) - case .yellow: - return UIColor(rgb: 0xE4A20A) - case .green: - return UIColor(rgb: 0x5AB03D) - case .blue: - return UIColor(rgb: 0x3E9CDF) - case .purple: - return UIColor(rgb: 0x985FDC) + return UIColor(rgb: color.rawValue) + } + + private static let crownTemplateImage: UIImage = { + return generateTintedImage(image: UIImage(bundleImageName: "Stories/LiveChatCrown"), color: .white)! + }() + + private static let crownFont: UIFont = { + let weight: CGFloat = UIFont.Weight.semibold.rawValue + let width: CGFloat = -0.1 + let descriptor: UIFontDescriptor + if #available(iOS 14.0, *) { + descriptor = UIFont.systemFont(ofSize: 10.0).fontDescriptor + } else { + descriptor = UIFont.systemFont(ofSize: 10.0, weight: UIFont.Weight.semibold).fontDescriptor + } + let symbolicTraits = descriptor.symbolicTraits + var updatedDescriptor: UIFontDescriptor? = descriptor.withSymbolicTraits(symbolicTraits) + updatedDescriptor = updatedDescriptor?.withDesign(.default) + if #available(iOS 14.0, *) { + updatedDescriptor = updatedDescriptor?.addingAttributes([ + UIFontDescriptor.AttributeName.traits: [UIFontDescriptor.TraitKey.weight: weight] + ]) + } + if #available(iOS 16.0, *) { + updatedDescriptor = updatedDescriptor?.addingAttributes([ + UIFontDescriptor.AttributeName.traits: [UIFontDescriptor.TraitKey.width: width] + ]) + } + + let font: UIFont + if let updatedDescriptor { + font = UIFont(descriptor: updatedDescriptor, size: 9.0) + } else { + font = UIFont(descriptor: descriptor, size: 9.0) + } + return font + }() + + public static func generateCrownImage(place: Int, backgroundColor: UIColor, foregroundColor: UIColor, borderColor: UIColor?) -> UIImage { + let baseSize = crownTemplateImage.size + var borderWidth: CGFloat = 0.0 + if borderColor != nil { + borderWidth = 2.0 + } + + var size = baseSize + size.width += borderWidth * 2.0 + size.height += borderWidth * 2.0 + + let image = generateImage(size, rotatedContext: { size, context in + UIGraphicsPushContext(context) + defer { + UIGraphicsPopContext() + } + + context.clear(CGRect(origin: CGPoint(), size: size)) + + if let borderColor { + generateTintedImage(image: UIImage(bundleImageName: "Stories/LiveChatCrown"), color: borderColor)!.draw(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) + } + + if backgroundColor != .white { + generateTintedImage(image: UIImage(bundleImageName: "Stories/LiveChatCrown"), color: backgroundColor)!.draw(in: CGRect(origin: CGPoint(x: borderWidth, y: borderWidth), size: baseSize)) + } else { + crownTemplateImage.draw(in: CGRect(origin: CGPoint(x: borderWidth, y: borderWidth), size: baseSize)) + } + + if foregroundColor.alpha < 1.0 { + context.setBlendMode(.copy) + } + + let string = NSAttributedString(string: "\(place + 1)", font: crownFont, textColor: foregroundColor) + let stringSize = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + + let stringOffsets: [CGPoint] = [ + CGPoint(x: 0.25, y: -0.33), + CGPoint(x: 0.24749999999999983, y: -0.495), + CGPoint(x: 0.0, y: -1.4025), + ] + var stringPosition = CGPoint(x: borderWidth + floorToScreenPixels((baseSize.width - stringSize.width) * 0.5), y: borderWidth + floorToScreenPixels((baseSize.height - stringSize.height) * 0.5) + 1.0) + if place < stringOffsets.count { + stringPosition.x += stringOffsets[place].x * 0.8 + stringPosition.y += stringOffsets[place].y * 0.8 + } + string.draw(at: stringPosition) + })! + + if backgroundColor == .white && foregroundColor == .clear && borderColor == nil { + return image.withRenderingMode(.alwaysTemplate) + } else { + return image } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift index bafe924698..b6ff9164b9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift @@ -173,13 +173,15 @@ final class LiveChatReactionStreamView: UIView { let period: CGFloat let phaseOffset: CGFloat let baseX: CGFloat + let verticalVelocity: CGFloat var timeValue: CGFloat = 0.0 - init(image: UIImage, amplitude: CGFloat, period: CGFloat, phaseOffset: CGFloat, baseX: CGFloat) { + init(image: UIImage, amplitude: CGFloat, period: CGFloat, phaseOffset: CGFloat, baseX: CGFloat, verticalVelocity: CGFloat) { self.amplitude = amplitude self.period = period self.phaseOffset = phaseOffset self.baseX = baseX + self.verticalVelocity = verticalVelocity super.init() @@ -192,6 +194,7 @@ final class LiveChatReactionStreamView: UIView { self.period = 0.0 self.phaseOffset = 0.0 self.baseX = 0.0 + self.verticalVelocity = 0.0 super.init(layer: layer) } @@ -272,13 +275,12 @@ final class LiveChatReactionStreamView: UIView { let random = LokiRng(seed0: UInt(id), seed1: 1, seed2: 0) let itemX: CGFloat = -image.size.width - 8.0 + 20.0 * CGFloat(LokiRng.random(withSeed0: UInt(id), seed1: 0, seed2: 0) - 0.5) let phaseOffset: CGFloat = CGFloat(random.next()) - let itemLayer = ItemLayer(image: image, amplitude: 0.0 + CGFloat(random.next()) * 6.0, period: 1.0 + CGFloat(random.next()) * 1.0, phaseOffset: phaseOffset, baseX: itemX) + let itemLayer = ItemLayer(image: image, amplitude: 0.0 + CGFloat(random.next()) * 6.0, period: 1.5 + CGFloat(random.next()) * 2.0, phaseOffset: phaseOffset, baseX: itemX, verticalVelocity: -(1.0 + CGFloat(random.next()) * 0.2) * 90.0) itemLayer.frame = CGRect(origin: CGPoint(x: itemX, y: -image.size.height * 0.5), size: image.size) - itemLayer.transform = CATransform3DMakeRotation(CGFloat(random.next() - 0.5) * CGFloat.pi * 0.2, 0.0, 0.0, 1.0) self.itemLayers[id] = itemLayer self.itemLayerContainer.addSublayer(itemLayer) - itemLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -200.0), duration: 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, additive: true) + let itemDuration: Double = 1.2 + Double(random.next()) * 0.8 itemLayer.animateScale(from: 0.001, to: 1.0, duration: 0.2) itemLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, completion: { [weak self, weak itemLayer] _ in @@ -286,10 +288,13 @@ final class LiveChatReactionStreamView: UIView { return } - let delay: Double = 2.0 - 0.1 - 0.18 + let delay: Double = itemDuration - 0.1 - 0.18 - itemLayer.animateScale(from: 1.0, to: 0.001, duration: 0.18, delay: delay, removeOnCompletion: false) - itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, delay: delay, removeOnCompletion: false, completion: { [weak self] _ in + let transition: ComponentTransition = .easeInOut(duration: 0.2) + transition.animateBlur(layer: itemLayer, fromRadius: 0.0, toRadius: 8.0, delay: delay) + + itemLayer.animateScale(from: 1.0, to: 0.001, duration: 0.2, delay: delay, removeOnCompletion: false) + itemLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: delay, removeOnCompletion: false, completion: { [weak self] _ in guard let self else { return } @@ -306,12 +311,25 @@ final class LiveChatReactionStreamView: UIView { let dt = max(1.0 / 120.0, min(1.0 / 30.0, timestamp - self.previousPhysicsTimestamp)) self.previousPhysicsTimestamp = timestamp + CATransaction.begin() + CATransaction.setDisableActions(true) + for (_, itemLayer) in self.itemLayers { itemLayer.timeValue += dt let itemPhase = (itemLayer.timeValue.truncatingRemainder(dividingBy: itemLayer.period) / itemLayer.period + itemLayer.phaseOffset).truncatingRemainder(dividingBy: 1.0) - let phaseFraction = sin(itemPhase * CGFloat.pi * 2.0) - itemLayer.position.x = itemLayer.baseX + phaseFraction * itemLayer.amplitude + let phaseAngle = itemPhase * CGFloat.pi * 2.0 + let phaseFraction = sin(phaseAngle) + + let newX = itemLayer.baseX + phaseFraction * itemLayer.amplitude + let newY = itemLayer.position.y + itemLayer.verticalVelocity * dt + itemLayer.position = CGPoint(x: newX, y: newY) + + let horizontalVelocity = itemLayer.amplitude * cos(phaseAngle) * (CGFloat.pi * 2.0 / itemLayer.period) + let rotationAngle = atan2(itemLayer.verticalVelocity, horizontalVelocity) + CGFloat.pi * 0.5 + itemLayer.setValue(rotationAngle, forKeyPath: "transform.rotation.z") } + + CATransaction.commit() } func update(size: CGSize, sourcePoint: CGPoint, transition: ComponentTransition) { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/PinnedBarComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/PinnedBarComponent.swift new file mode 100644 index 0000000000..7855ec85b4 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/PinnedBarComponent.swift @@ -0,0 +1,466 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import AccountContext +import TelegramCore +import TelegramPresentationData +import SwiftSignalKit +import TelegramCallsUI +import AsyncListComponent +import AvatarNode +import ContextUI +import StarsParticleEffect +import StoryLiveChatMessageComponent + +private final class PinnedBarMessageComponent: Component { + let context: AccountContext + let strings: PresentationStrings + let theme: PresentationTheme + let message: GroupCallMessagesContext.Message + let topPlace: Int? + let action: () -> Void + let contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)? + + init(context: AccountContext, strings: PresentationStrings, theme: PresentationTheme, message: GroupCallMessagesContext.Message, topPlace: Int?, action: @escaping () -> Void, contextGesture: ((ContextGesture, ContextExtractedContentContainingNode) -> Void)?) { + self.context = context + self.strings = strings + self.theme = theme + self.message = message + self.topPlace = topPlace + self.action = action + self.contextGesture = contextGesture + } + + static func ==(lhs: PinnedBarMessageComponent, rhs: PinnedBarMessageComponent) -> Bool { + if lhs === rhs { + return true + } + if lhs.context !== rhs.context { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.message != rhs.message { + return false + } + if lhs.topPlace != rhs.topPlace { + return false + } + if (lhs.contextGesture == nil) != (rhs.contextGesture == nil) { + return false + } + return true + } + + final class View: UIView { + private let extractedContainerNode: ContextExtractedContentContainingNode + private let containerNode: ContextControllerSourceNode + + private let backgroundView: UIImageView + private let foregroundClippingView: UIView + private let foregroundView: UIImageView + private let effectLayer: StarsParticleEffectLayer + + private var avatarNode: AvatarNode? + private let title = ComponentView() + private var crownIcon: UIImageView? + + private var component: PinnedBarMessageComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + private var updateTimer: Foundation.Timer? + + override init(frame: CGRect) { + self.extractedContainerNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.backgroundView = UIImageView() + self.foregroundClippingView = UIView() + self.foregroundClippingView.clipsToBounds = true + self.foregroundView = UIImageView() + self.effectLayer = StarsParticleEffectLayer() + + super.init(frame: frame) + + self.containerNode.addSubnode(self.extractedContainerNode) + self.containerNode.targetNodeForActivationProgress = self.extractedContainerNode.contentNode + self.addSubview(self.containerNode.view) + + self.containerNode.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + return + } + component.contextGesture?(gesture, self.extractedContainerNode) + } + + self.extractedContainerNode.contentNode.view.addSubview(self.backgroundView) + + self.foregroundClippingView.addSubview(self.foregroundView) + self.extractedContainerNode.contentNode.view.addSubview(self.foregroundClippingView) + self.extractedContainerNode.contentNode.view.layer.addSublayer(self.effectLayer) + + self.extractedContainerNode.contentNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.updateTimer?.invalidate() + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + if case .ended = recognizer.state { + component.action() + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + + guard let result = super.hitTest(point, with: event) else { + return nil + } + + return result + } + + func update(component: PinnedBarMessageComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.updateTimer == nil { + self.updateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + if !self.isUpdating { + self.state?.updated(transition: .immediate, isLocal: true) + } + }) + } + + let previousComponent = self.component + self.component = component + self.state = state + + self.containerNode.isGestureEnabled = component.contextGesture != nil + + let itemHeight: CGFloat = 32.0 + let avatarInset: CGFloat = 4.0 + let avatarSize: CGFloat = 24.0 + let avatarSpacing: CGFloat = 6.0 + let rightInset: CGFloat = 10.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.message.author?.displayTitle(strings: component.strings, displayOrder: .firstLast) ?? " ", font: Font.semibold(15.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: 200.0, height: itemHeight) + ) + + var size = CGSize(width: avatarInset + avatarSize + avatarSpacing + titleSize.width + rightInset, height: itemHeight) + + if let topPlace = component.topPlace { + let crownIcon: UIImageView + if let current = self.crownIcon { + crownIcon = current + } else { + crownIcon = UIImageView() + self.crownIcon = crownIcon + self.extractedContainerNode.contentNode.view.addSubview(crownIcon) + } + if topPlace != previousComponent?.topPlace { + crownIcon.image = StoryLiveChatMessageComponent.generateCrownImage(place: topPlace, backgroundColor: .white, foregroundColor: .clear, borderColor: nil) + } + crownIcon.tintColor = .white + + if let image = crownIcon.image { + size.width += image.size.width + 4.0 + } + } else { + if let crownIcon = self.crownIcon { + self.crownIcon = nil + crownIcon.removeFromSuperview() + } + } + + if self.backgroundView.image == nil { + self.backgroundView.image = generateStretchableFilledCircleImage(diameter: itemHeight, color: .white)?.withRenderingMode(.alwaysTemplate) + self.foregroundView.image = self.backgroundView.image + } + + let params = LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 })) + let baseColor = StoryLiveChatMessageComponent.getMessageColor(color: GroupCallMessagesContext.getStarAmountParamMapping(params: params, value: component.message.paidStars ?? 0).color ?? GroupCallMessagesContext.Message.Color(rawValue: 0x985FDC)) + self.backgroundView.tintColor = baseColor.withMultipliedBrightnessBy(0.7) + self.foregroundView.tintColor = baseColor + + let timestamp = CFAbsoluteTimeGetCurrent() + let currentDuration = max(0.0, timestamp - Double(component.message.date)) + let timeFraction: CGFloat = 1.0 - min(1.0, currentDuration / Double(component.message.lifetime)) + + let backgroundFrame = CGRect(origin: CGPoint(), size: size) + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + transition.setFrame(view: self.foregroundView, frame: CGRect(origin: CGPoint(), size: size)) + transition.setFrame(view: self.foregroundClippingView, frame: CGRect(origin: CGPoint(), size: CGSize(width: floorToScreenPixels(size.width * timeFraction), height: size.height))) + + transition.setFrame(layer: self.effectLayer, frame: CGRect(origin: CGPoint(), size: size)) + self.effectLayer.update(color: UIColor(white: 1.0, alpha: 0.5), size: size, cornerRadius: size.height * 0.5, transition: transition) + + let avatarFrame = CGRect(origin: CGPoint(x: avatarInset, y: floor((itemHeight - avatarSize) * 0.5)), size: CGSize(width: avatarSize, height: avatarSize)) + do { + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 10.0)) + self.avatarNode = avatarNode + self.extractedContainerNode.contentNode.view.addSubview(avatarNode.view) + } + transition.setFrame(view: avatarNode.view, frame: avatarFrame) + avatarNode.updateSize(size: avatarFrame.size) + if let peer = component.message.author { + if peer.smallProfileImage != nil { + avatarNode.setPeerV2(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } else { + avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + } + } else { + avatarNode.setCustomLetters([" "]) + } + } + + var titleFrame = CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((itemHeight - titleSize.height) * 0.5)), size: titleSize) + if let crownIcon = self.crownIcon, let image = crownIcon.image { + crownIcon.frame = CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.minY - 1.0), size: image.size) + titleFrame.origin.x += image.size.width + 4.0 + } + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.layer.anchorPoint = CGPoint() + self.extractedContainerNode.contentNode.view.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.origin) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + + self.extractedContainerNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentNode.frame = CGRect(origin: CGPoint(), size: size) + self.extractedContainerNode.contentRect = backgroundFrame.insetBy(dx: -4.0, dy: 0.0) + self.containerNode.frame = CGRect(origin: CGPoint(), size: size) + + return CGSize(width: size.width + 10.0, height: size.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class PinnedBarComponent: Component { + let context: AccountContext + let strings: PresentationStrings + let theme: PresentationTheme + let isExpanded: Bool + let messages: [GroupCallMessagesContext.Message] + let topIndices: [EnginePeer.Id: Int] + let action: (GroupCallMessagesContext.Message) -> Void + let contextGesture: (GroupCallMessagesContext.Message, ContextGesture, ContextExtractedContentContainingNode) -> Void + + init(context: AccountContext, strings: PresentationStrings, theme: PresentationTheme, isExpanded: Bool, messages: [GroupCallMessagesContext.Message], topIndices: [EnginePeer.Id: Int], action: @escaping (GroupCallMessagesContext.Message) -> Void, contextGesture: @escaping (GroupCallMessagesContext.Message, ContextGesture, ContextExtractedContentContainingNode) -> Void) { + self.context = context + self.strings = strings + self.theme = theme + self.isExpanded = isExpanded + self.messages = messages + self.topIndices = topIndices + self.action = action + self.contextGesture = contextGesture + } + + static func ==(lhs: PinnedBarComponent, rhs: PinnedBarComponent) -> Bool { + if lhs === rhs { + return true + } + if lhs.context !== rhs.context { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.isExpanded != rhs.isExpanded { + return false + } + if lhs.messages != rhs.messages { + return false + } + if lhs.topIndices != rhs.topIndices { + return false + } + return true + } + + final class View: UIView { + private let listContainer: UIView + private let listState = AsyncListComponent.ExternalState() + private let list = ComponentView() + + private var component: PinnedBarComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + override init(frame: CGRect) { + self.listContainer = UIView() + self.listContainer.clipsToBounds = true + + super.init(frame: frame) + + self.addSubview(self.listContainer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + + guard let result = super.hitTest(point, with: event) else { + return nil + } + + return result + } + + func update(component: PinnedBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let previousComponent = self.component + self.component = component + self.state = state + + let itemHeight: CGFloat = 32.0 + + let insets = UIEdgeInsets(top: 13.0, left: 20.0, bottom: 13.0, right: 20.0) + + let size = CGSize(width: availableSize.width, height: insets.top + itemHeight + insets.bottom) + + var listItems: [AnyComponentWithIdentity] = [] + for message in component.messages { + if let author = message.author { + let id = message.id + listItems.append(AnyComponentWithIdentity(id: author.id, component: AnyComponent(PinnedBarMessageComponent( + context: component.context, + strings: component.strings, + theme: component.theme, + message: message, + topPlace: message.author.flatMap { component.topIndices[$0.id] }, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + if let message = component.messages.first(where: { $0.id == id }) { + component.action(message) + } + }, + contextGesture: message.isIncoming ? { [weak self] gesture, sourceNode in + guard let self, let component = self.component else { + return + } + if let message = component.messages.first(where: { $0.id == id }) { + component.contextGesture(message, gesture, sourceNode) + } else { + gesture.cancel() + } + } : nil + )))) + } + } + + let listInsets = UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right) + let listFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) + + var listTransition = transition + var animateIn = false + if let previousComponent { + if previousComponent.messages.isEmpty { + listTransition = listTransition.withAnimation(.none) + animateIn = true + } + } else { + listTransition = listTransition.withAnimation(.none) + animateIn = true + } + + let _ = self.list.update( + transition: listTransition, + component: AnyComponent(AsyncListComponent( + externalState: self.listState, + items: listItems, + itemSetId: AnyHashable(0), + direction: .horizontal, + insets: listInsets + )), + environment: {}, + containerSize: listFrame.size + ) + if let listView = self.list.view { + if listView.superview == nil { + self.listContainer.addSubview(listView) + } + transition.setPosition(view: listView, position: CGRect(origin: CGPoint(), size: listFrame.size).center) + transition.setBounds(view: listView, bounds: CGRect(origin: CGPoint(), size: listFrame.size)) + + if animateIn { + transition.animateAlpha(view: listView, from: 0.0, to: 1.0) + } + } + + transition.setFrame(view: self.listContainer, frame: listFrame) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift index 7fdddb25a8..17600e6a66 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift @@ -19,365 +19,6 @@ import StarsParticleEffect import StoryLiveChatMessageComponent import AdminUserActionsSheet -private final class PinnedBarMessageComponent: Component { - let context: AccountContext - let strings: PresentationStrings - let theme: PresentationTheme - let message: GroupCallMessagesContext.Message - - init(context: AccountContext, strings: PresentationStrings, theme: PresentationTheme, message: GroupCallMessagesContext.Message) { - self.context = context - self.strings = strings - self.theme = theme - self.message = message - } - - static func ==(lhs: PinnedBarMessageComponent, rhs: PinnedBarMessageComponent) -> Bool { - if lhs === rhs { - return true - } - if lhs.context !== rhs.context { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.message != rhs.message { - return false - } - return true - } - - final class View: UIView { - private let backgroundView: UIImageView - private let foregroundClippingView: UIView - private let foregroundView: UIImageView - private let effectLayer: StarsParticleEffectLayer - - private var avatarNode: AvatarNode? - private let title = ComponentView() - - private var component: PinnedBarMessageComponent? - private weak var state: EmptyComponentState? - private var isUpdating: Bool = false - - private var updateTimer: Foundation.Timer? - - override init(frame: CGRect) { - self.backgroundView = UIImageView() - self.foregroundClippingView = UIView() - self.foregroundClippingView.clipsToBounds = true - self.foregroundView = UIImageView() - self.effectLayer = StarsParticleEffectLayer() - - super.init(frame: frame) - - self.addSubview(self.backgroundView) - - self.foregroundClippingView.addSubview(self.foregroundView) - self.addSubview(self.foregroundClippingView) - self.layer.addSublayer(self.effectLayer) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.updateTimer?.invalidate() - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - - guard let result = super.hitTest(point, with: event) else { - return nil - } - - return result - } - - func update(component: PinnedBarMessageComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.isUpdating = true - defer { - self.isUpdating = false - } - - if self.updateTimer == nil { - self.updateTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 30.0, repeats: true, block: { [weak self] _ in - guard let self else { - return - } - if !self.isUpdating { - self.state?.updated(transition: .immediate, isLocal: true) - } - }) - } - - self.component = component - self.state = state - - let itemHeight: CGFloat = 32.0 - let avatarInset: CGFloat = 4.0 - let avatarSize: CGFloat = 24.0 - let avatarSpacing: CGFloat = 6.0 - let rightInset: CGFloat = 10.0 - - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.message.author?.displayTitle(strings: component.strings, displayOrder: .firstLast) ?? " ", font: Font.semibold(15.0), textColor: .white)) - )), - environment: {}, - containerSize: CGSize(width: 1000.0, height: itemHeight) - ) - - let size = CGSize(width: avatarInset + avatarSize + avatarSpacing + titleSize.width + rightInset, height: itemHeight) - - if self.backgroundView.image == nil { - self.backgroundView.image = generateStretchableFilledCircleImage(diameter: itemHeight, color: .white)?.withRenderingMode(.alwaysTemplate) - self.foregroundView.image = self.backgroundView.image - } - - let baseColor = StoryLiveChatMessageComponent.getMessageColor(color: GroupCallMessagesContext.getStarAmountParamMapping(value: component.message.paidStars ?? 0).color ?? .purple) - self.backgroundView.tintColor = baseColor.withMultipliedBrightnessBy(0.7) - self.foregroundView.tintColor = baseColor - - let timestamp = CFAbsoluteTimeGetCurrent() - let currentDuration = max(0.0, timestamp - Double(component.message.date)) - let timeFraction: CGFloat = 1.0 - min(1.0, currentDuration / Double(component.message.lifetime)) - - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) - transition.setFrame(view: self.foregroundView, frame: CGRect(origin: CGPoint(), size: size)) - transition.setFrame(view: self.foregroundClippingView, frame: CGRect(origin: CGPoint(), size: CGSize(width: floorToScreenPixels(size.width * timeFraction), height: size.height))) - - transition.setFrame(layer: self.effectLayer, frame: CGRect(origin: CGPoint(), size: size)) - self.effectLayer.update(color: UIColor(white: 1.0, alpha: 0.5), size: size, cornerRadius: size.height * 0.5, transition: transition) - - let avatarFrame = CGRect(origin: CGPoint(x: avatarInset, y: floor((itemHeight - avatarSize) * 0.5)), size: CGSize(width: avatarSize, height: avatarSize)) - do { - let avatarNode: AvatarNode - if let current = self.avatarNode { - avatarNode = current - } else { - avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 10.0)) - self.avatarNode = avatarNode - self.addSubview(avatarNode.view) - } - transition.setFrame(view: avatarNode.view, frame: avatarFrame) - avatarNode.updateSize(size: avatarFrame.size) - if let peer = component.message.author { - if peer.smallProfileImage != nil { - avatarNode.setPeerV2(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) - } else { - avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) - } - } else { - avatarNode.setCustomLetters([" "]) - } - } - - let titleFrame = CGRect(origin: CGPoint(x: avatarInset + avatarSize + avatarSpacing, y: floor((itemHeight - titleSize.height) * 0.5)), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - titleView.layer.anchorPoint = CGPoint() - self.addSubview(titleView) - } - transition.setPosition(view: titleView, position: titleFrame.origin) - titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) - } - - return CGSize(width: size.width + 10.0, height: size.height) - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - -private final class PinnedBarComponent: Component { - let context: AccountContext - let strings: PresentationStrings - let theme: PresentationTheme - let isExpanded: Bool - let messages: [GroupCallMessagesContext.Message] - let toggleExpandedAction: () -> Void - - init(context: AccountContext, strings: PresentationStrings, theme: PresentationTheme, isExpanded: Bool, messages: [GroupCallMessagesContext.Message], toggleExpandedAction: @escaping () -> Void) { - self.context = context - self.strings = strings - self.theme = theme - self.isExpanded = isExpanded - self.messages = messages - self.toggleExpandedAction = toggleExpandedAction - } - - static func ==(lhs: PinnedBarComponent, rhs: PinnedBarComponent) -> Bool { - if lhs === rhs { - return true - } - if lhs.context !== rhs.context { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.isExpanded != rhs.isExpanded { - return false - } - if lhs.messages != rhs.messages { - return false - } - return true - } - - final class View: UIView { - private let listContainer: UIView - private let listState = AsyncListComponent.ExternalState() - private let list = ComponentView() - - private let toggleButtonBackground: GlassBackgroundView - private let toggleButton: HighlightTrackingButton - private let toggleButtonIcon: UIImageView - - private var component: PinnedBarComponent? - private weak var state: EmptyComponentState? - private var isUpdating: Bool = false - - override init(frame: CGRect) { - self.listContainer = UIView() - self.listContainer.clipsToBounds = true - - self.toggleButtonBackground = GlassBackgroundView() - self.toggleButton = HighlightTrackingButton() - self.toggleButtonIcon = UIImageView() - - super.init(frame: frame) - - self.addSubview(self.listContainer) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - } - - @objc private func toggleButtonPressed() { - guard let component = self.component else { - return - } - component.toggleExpandedAction() - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - - guard let result = super.hitTest(point, with: event) else { - return nil - } - - return result - } - - func update(component: PinnedBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.isUpdating = true - defer { - self.isUpdating = false - } - - let previousComponent = self.component - self.component = component - self.state = state - - let itemHeight: CGFloat = 32.0 - - let insets = UIEdgeInsets(top: 13.0, left: 20.0, bottom: 13.0, right: 20.0) - - let size = CGSize(width: availableSize.width, height: insets.top + itemHeight + insets.bottom) - - var listItems: [AnyComponentWithIdentity] = [] - for message in component.messages { - if let author = message.author { - listItems.append(AnyComponentWithIdentity(id: author.id, component: AnyComponent(PinnedBarMessageComponent( - context: component.context, - strings: component.strings, - theme: component.theme, - message: message - )))) - } - } - - let listInsets = UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right) - let listFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) - - var listTransition = transition - var animateIn = false - if let previousComponent { - if previousComponent.messages.isEmpty { - listTransition = listTransition.withAnimation(.none) - animateIn = true - } - } else { - listTransition = listTransition.withAnimation(.none) - animateIn = true - } - - let _ = self.list.update( - transition: listTransition, - component: AnyComponent(AsyncListComponent( - externalState: self.listState, - items: listItems, - itemSetId: AnyHashable(0), - direction: .horizontal, - insets: listInsets - )), - environment: {}, - containerSize: listFrame.size - ) - if let listView = self.list.view { - if listView.superview == nil { - self.listContainer.addSubview(listView) - } - transition.setPosition(view: listView, position: CGRect(origin: CGPoint(), size: listFrame.size).center) - transition.setBounds(view: listView, bounds: CGRect(origin: CGPoint(), size: listFrame.size)) - - if animateIn { - transition.animateAlpha(view: listView, from: 0.0, to: 1.0) - } - } - - transition.setFrame(view: self.listContainer, frame: listFrame) - - return size - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - final class StoryContentLiveChatComponent: Component { final class External { fileprivate(set) var hasUnseenMessages: Bool = false @@ -681,7 +322,15 @@ final class StoryContentLiveChatComponent: Component { } } - private func openMessageContextMenu(id: GroupCallMessagesContext.Message.Id, gesture: ContextGesture, sourceNode: ContextExtractedContentContainingNode) { + private func scrollToMessage(id: GroupCallMessagesContext.Message.Id) { + guard let messagesState = self.messagesState, let message = messagesState.messages.first(where: { $0.id == id }) else { + return + } + self.listState.resetScrolling(id: AnyHashable(message.stableId)) + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + } + + private func openMessageContextMenu(id: GroupCallMessagesContext.Message.Id, isPinned: Bool, gesture: ContextGesture, sourceNode: ContextExtractedContentContainingNode) { Task { @MainActor [weak self] in guard let self else { return @@ -697,20 +346,22 @@ final class StoryContentLiveChatComponent: Component { var items: [ContextMenuItem] = [] //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Copy", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in - guard let self else { - return - } - - c?.dismiss(completion: { [weak self] in + if !isPinned { + items.append(.action(ContextMenuActionItem(text: "Copy", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in guard let self else { return } - if let messagesState = self.messagesState, let message = messagesState.messages.first(where: { $0.id == id }) { - UIPasteboard.general.string = message.text - } - }) - }))) + + c?.dismiss(completion: { [weak self] in + guard let self else { + return + } + if let messagesState = self.messagesState, let message = messagesState.messages.first(where: { $0.id == id }) { + UIPasteboard.general.string = message.text + } + }) + }))) + } let state = await (component.call.state |> take(1)).get() @@ -872,6 +523,10 @@ final class StoryContentLiveChatComponent: Component { if let messagesState = self.messagesState { for message in messagesState.messages.reversed() { let messageId = message.id + var topPlace = self.messagesState?.topStars.firstIndex(where: { $0.peerId != nil && $0.peerId == message.author?.id }) + if let topPlaceValue = topPlace, topPlaceValue >= 3 { + topPlace = nil + } listItems.append(AnyComponentWithIdentity(id: message.stableId, component: AnyComponent(StoryLiveChatMessageComponent( context: component.context, strings: component.strings, @@ -883,11 +538,12 @@ final class StoryContentLiveChatComponent: Component { transparentBackground: true ), message: message, + topPlace: topPlace, contextGesture: { [weak self] gesture, sourceNode in guard let self else { return } - self.openMessageContextMenu(id: messageId, gesture: gesture, sourceNode: sourceNode) + self.openMessageContextMenu(id: messageId, isPinned: false, gesture: gesture, sourceNode: sourceNode) } )))) } @@ -913,6 +569,17 @@ final class StoryContentLiveChatComponent: Component { return lhs.date > rhs.date }) + var topIndices: [EnginePeer.Id: Int] = [:] + if let messagesState = self.messagesState { + for topMessage in topMessages { + if let author = topMessage.author, topIndices[author.id] == nil { + if let index = messagesState.topStars.firstIndex(where: { $0.peerId != nil && $0.peerId == author.id }), index < 3 { + topIndices[author.id] = index + } + } + } + } + self.currentListIsEmpty = listItems.isEmpty let pinnedBarSize = self.pinnedBar.update( @@ -923,14 +590,18 @@ final class StoryContentLiveChatComponent: Component { theme: component.theme, isExpanded: self.isChatExpanded, messages: topMessages, - toggleExpandedAction: { [weak self] in + topIndices: topIndices, + action: { [weak self] message in guard let self else { return } - self.isChatExpanded = !self.isChatExpanded - if !self.isUpdating { - self.state?.updated(transition: .spring(duration: 0.4)) + self.scrollToMessage(id: message.id) + }, + contextGesture: { [weak self] message, gesture, sourceNode in + guard let self else { + return } + self.openMessageContextMenu(id: message.id, isPinned: true, gesture: gesture, sourceNode: sourceNode) } )), environment: {}, @@ -942,7 +613,14 @@ final class StoryContentLiveChatComponent: Component { self.addSubview(pinnedBarView) } transition.setFrame(view: pinnedBarView, frame: pinnedBarFrame) - transition.setAlpha(view: pinnedBarView, alpha: topMessages.isEmpty ? 0.0 : 1.0) + + let pinnedBarAlpha: CGFloat + if self.isMessageContextMenuOpen { + pinnedBarAlpha = 0.25 + } else { + pinnedBarAlpha = topMessages.isEmpty ? 0.0 : 1.0 + } + transition.setAlpha(view: pinnedBarView, alpha: pinnedBarAlpha) } var listInsets = UIEdgeInsets(top: component.insets.bottom + 8.0, left: component.insets.right, bottom: component.insets.top + 8.0, right: component.insets.left) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 27882ef6d3..3e625cd73b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -2973,7 +2973,7 @@ public final class StoryItemSetContainerComponent: Component { ) starStats = liveChatStateValue.starStats.flatMap { starStats in return MessageInputPanelComponent.StarStats( - myStars: starStats.myStars, + hasOutgoingStars: self.sendMessageContext.currentLiveStreamStarsIsActive, totalStars: starStats.totalStars ) } @@ -3007,7 +3007,7 @@ public final class StoryItemSetContainerComponent: Component { var maxInputLength = 4096 var maxEmojiCount: Int? if isLiveStream { - let params = GroupCallMessagesContext.getStarAmountParamMapping(value: sendPaidMessageStars?.value ?? 0) + let params = GroupCallMessagesContext.getStarAmountParamMapping(params: LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 })), value: sendPaidMessageStars?.value ?? 0) maxInputLength = params.maxLength maxEmojiCount = params.emojiCount } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 719bad7ac6..c492c474d3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -104,6 +104,8 @@ final class StoryItemSetContainerSendMessage { var currentLiveStreamMessageStars: StarsAmount? weak var currentSendStarsUndoController: UndoOverlayController? + var currentLiveStreamStarsIsActive: Bool = false + var currentLiveStreamStarsIsActiveTimer: Foundation.Timer? var sendAsData: (isPremium: Bool, availablePeers: [SendAsPeer])? var currentSendAsPeer: SendAsPeer? @@ -709,7 +711,7 @@ final class StoryItemSetContainerSendMessage { if !text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { var maxInputLength: Int? var maxEmojiCount: Int? - let params = GroupCallMessagesContext.getStarAmountParamMapping(value: sendPaidMessageStars?.value ?? 0) + let params = GroupCallMessagesContext.getStarAmountParamMapping(params: LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 })), value: sendPaidMessageStars?.value ?? 0) maxInputLength = params.maxLength maxEmojiCount = params.emojiCount @@ -4009,7 +4011,7 @@ final class StoryItemSetContainerSendMessage { context: component.context, peerId: peerId, myPeer: (self.currentSendAsPeer?.peer).flatMap(EnginePeer.init), - reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id, minAmount: Int(minAmount)), + reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id, minAmount: Int(minAmount), liveChatMessageParams: LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 }))), topPeers: topPeers, completion: { [weak self, weak view] amount, _, _, _ in guard let self, let view else { @@ -4128,16 +4130,23 @@ final class StoryItemSetContainerSendMessage { return } + self.currentLiveStreamStarsIsActive = true + self.currentLiveStreamStarsIsActiveTimer?.invalidate() + self.currentLiveStreamStarsIsActiveTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self, weak view] _ in + guard let self, let view else { + return + } + self.currentLiveStreamStarsIsActive = false + view.state?.updated(transition: .spring(duration: 0.4)) + }) + var totalStars = 0 if let pendingMyStars = visibleItemView.liveChatState?.starStats?.pendingMyStars, pendingMyStars > 0 { totalStars += count totalStars += Int(pendingMyStars) self.commitSendStars(view: view, count: count, delay: true) } else { - var minAmount: Int64 = 1 - if let minMessagePrice = visibleItemView.liveChatState?.minMessagePrice { - minAmount = minMessagePrice - } + let minAmount: Int64 = 1 var count = count count = max(Int(minAmount), count) totalStars += count diff --git a/submodules/TelegramUI/Images.xcassets/Stories/LiveChatCrown.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/LiveChatCrown.imageset/Contents.json new file mode 100644 index 0000000000..93dc7834c5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/LiveChatCrown.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "crown.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/LiveChatCrown.imageset/crown.pdf b/submodules/TelegramUI/Images.xcassets/Stories/LiveChatCrown.imageset/crown.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bff78f2c69e7592e2ed3d0d263a26e99db3ae252 GIT binary patch literal 4519 zcmZu!c|26>8z;0(xmu(obxNCJX3m*8XXdopOO}Qt6={r-5i^Vti4di(w2&o4NF;Kp zWUr`jDN8A95iL^65|Z+JkL|Yn&L5olzVGvW_Van3Kg~Js>t8{q#9oWi% z1U!^c(N!Fwz?Dy6KgrX6kyp1DafN)ghzk^R5Arx%JkZItxOowZ{B)`2aGMp&5QE~{M|l{G?(4Ca(F<=IpClctm|se$Y_z8sF4 zDqkYzkdopsTogTgp!BhIbnlr-P|S3-9@Xnw=kdjwW&HVST85`4KlPTYlz8YZZtpGO zIJxt#!ho;ZOw!VOc6X(wK~g8w(tP2YfKQkFR1 zSDZTI^1c~)lG~dV!+oHsQ;eY1mnZWyC!dwKmVlS+EK%4!NlNn^Ylf`Mv?bGSiq}t` ztM-6}n@H8DZ_<2wDWDQflF5+?RGU+2S-En)R4{u%$~1+-Q0kmMZ;57>+uVgRiA5dF z=57r0$vcY@n{5V`dPpqtPWY_nwm^!t>^y1flm}|Ck%|elnl#scb}Sa#ZIqB?XCjd- zUE`bM8x*&~XmOPpeXhY~TIFNgO}!-gyo;w__>r~3;dym@MNu)0N~Z{``zWk>}mu&-HE!c`_SGl>2X+^}1GeDGnSN zavqwY?z2zLajE2NH@{ncOEsip104gXI^j!J&kj66zR$8+>a-^PRIOT`zQ-01&86o= zx~DI{UM9|)e)W_nLKH&R7~EpIG%{#cn{9?)UZh7v{jkBDxt~RV}M+sW?r7S;aWr;$`;gYma%axX^y-a_E4qcw4evjpoaM!5z zGAq{6kiCVy`&_bZ>ineeII|=-5Suz0jbRo5YHcR;<8%gvmtgTgN=Uy9y` z)vTOXEw`4^MlQE>vuTl#HY+kwGUMbwgaacNJc-yHQ4qn4n1WJxhIUtNvkxq{3$b;v zHL%@&%i@+){qp*GH!Uke+86a&yUXWCZcK=eZB10Fx?0^{bD;{Ywup_4Z%F#KP>1JEwpyE-LUP6d#0V{@W}paroY3dI-`%F^&kJ~$oOXUX&)s+@K20mrRxc&B+-1U z>TvaPjW;_spNdaxwJAPWe8#ZP&?=E;=(Uy>6&$r~?HLNTyWGZKvT@&aJOadQQdB@d9Rhb?YliHgjNpLF7B~;9{Jt* z?)f2kJKe$#Z|bZ1abJrk;(8>BSd?rlMQxrJO+ALy)N7Ui?iStFKvWm;K!nQ!P$6)!n^#k?h1 zEBzK^tZ1_?$z@ek##Oe=eYj@Xnw-M6qPU{>g)jUMADic|r=+5U=`@}&IU}o;snZfy z7-tuMWv6>u;~k+&J$Ws~-7%yp`NNLqrq|L8_j(=)99r-=;szWZ{y97tj=_DR>>i6$ z)@33G|1NvAsJWmmyJxH09FKWN;Ne42dge}zZh3k`1p_A~Z~ImI>5A*I$Zi+kg}E*3 z@-*I8-F3OH?Q`2hS&yyy|KhPhRNdN($srLJ>%(k9L&H`WxNdzJvvE0TW{r)S{553v z1A5zL;dU#PI`H!+Ai!%rKczQ?{%Igr|ruYL~aQm3xKv_+BQ_#AcCfCEB(dN;s z1h)P?r`J*j2UO#qs%bkG(&>OuBZo`e?rmkavS>bz2)X7-kdzY;GM=hX#eJo6o#(!X%X zJeswU)v~L~?5a`rweIgrUc~+=>hxDQZM#UB+1;Mh{4Rg>%j7q9MenOQwVVsZ(KUWG zy+b8K>DNv3GcWa(Ikh;opKPAflbq$=e-aX{m|(#$c;3(Q}_MZ(*rLXJjz;3I~xM zvNTxdYJTs~iRUGc5|?cA-<%y!@v0d3-ecBD`}aom+xm)*hYk-LR(h=9-!c1Y^?_So zcA+$Hlk9Mk2Y2Xw*rul8JbW&U_U-O6XSp1uNq5tdRryHXN zN``8Cl!u=5Tu7hm2~Bv za37xziHKS~t4gz)X}o6kO$&gK4^2vkc5In9 zwcShM$#6l>osK;*FZ0sFV^pI%XsAQ#mYFfol^e&b3bvG-bGfe@y-x95O7SU^b7qh( zoTy}2{g?NtEN#qLPu=>y5+}O+aRDqTwsX?mu(-Oq3(0kLloAzIk(g-f52?VZRZ7Ir zIyMoGkFWHJ@pEK+9EZ^fcn?I;)zs8rJ9DieVpcVVEGA}OY~mI%DQgPs1;Xu)Yz}v1 zaMpw&g2fmNyFbsnj9+BNMpx$8Mbemc6dcc+m`R7J1W2P(0Tsj~ zGcXLsXk!q9F(8}_(-0;+DhlH$4cJP8VKR>6R0Le4l2I6Dz&K=2f)O%;V+cwoPy~gU zghGVOKwz8+6BvwR7!4aSf`I}q0;4f#R62v;M9CT5TVmy5NE<~j4%RZ7r08NkGTrMgM%U= z1Djw#%Oulr7=gipMIclL6G&o!4Mvc045vXT8DSzg4b;R46@vlM$p{0)hajKONF&1_ z$y89kz%V8bZXv*B=17=EtVgK;p~6hUw+SaXL_yF%;6@WP8V-9Bs3sVMk`6o{2^_`* zYaIv%f&vAj@nrztU^1u*P%Z>CmIw|BR2!9n5T_G4vnK(IF)D(NXaUK^!7&If6QzwR z1~mW@1t6R-7$k`(7AgaznDhTi(w<~XvLW$FMx_7K!9?$juB!~3Hqn~+1WL4~I-f5P zIYWCa#`Z&#Lylm>WU=>;uaE{@YaUxe;0Emhe*fNUFmoJ2CJx7$jJeJN7a@o13=tm# z<1v0s0bj)B1C2ztkAp0>5MaR9G3=)$cm(@z=hov{17jh=2jX)xxD-)!+=%4VKDN?y^+t& zZ}$);VE+SSfQJ2*55fQBqcIque}273V-Owt8-{~V&TklKy`S`oglwKYS4bj?O;-!# zZWW4B%|TyaRG5Y-|Ne6%rxas>03>uGAC&DpcPin+jRa1 DQF6uR literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 10a1d00070..b114be4907 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2620,10 +2620,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - inputNodeHeight), size: CGSize(width: layout.size.width, height: inputNodeHeight)) if immediatelyLayoutInputNodeAndAnimateAppearance { var adjustedForPreviousInputHeightFrame = inputNodeFrame - var heightDifference = inputNodeHeight - previousInputHeight - if previousInputHeight.isLessThanOrEqualTo(cleanInsets.bottom) { - heightDifference = inputNodeHeight - } + let heightDifference = inputNodeHeight adjustedForPreviousInputHeightFrame.origin.y += heightDifference inputNode.frame = adjustedForPreviousInputHeightFrame transition.updateFrame(node: inputNode, frame: inputNodeFrame) @@ -2867,12 +2864,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } if let dismissedInputNode = dismissedInputNode { self.disappearingNode = dismissedInputNode - let targetY: CGFloat - if cleanInsets.bottom.isLess(than: insets.bottom) { - targetY = layout.size.height - insets.bottom - } else { - targetY = layout.size.height - } + let targetY = layout.size.height if let dismissedInputNodeExternalTopPanelContainer = dismissedInputNodeExternalTopPanelContainer { transition.updateFrame(view: dismissedInputNodeExternalTopPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: targetY), size: CGSize(width: layout.size.width, height: 0.0)), force: true, completion: { [weak self, weak dismissedInputNodeExternalTopPanelContainer] completed in