This commit is contained in:
Isaac 2025-10-31 17:51:37 +04:00
parent 0e5337636d
commit 5145b9e605
23 changed files with 1515 additions and 1013 deletions

View file

@ -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

View file

@ -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)

View file

@ -10,6 +10,7 @@
#import <arpa/inet.h>
#import <CommonCrypto/CommonDigest.h>
#import <CommonCrypto/CommonHMac.h>
#import <Security/SecRandom.h>
#import <MtProtoKit/MTInternalId.h>
@ -108,7 +109,26 @@ static void generate_public_key(unsigned char key[32], id<EncryptionProvider> 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<EncryptionProvider> 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<EncryptionProvider> 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<EncryptionProvider> 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<NSNumber *> *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<EncryptionProvider> 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 *)&timestamp;
uint32_t timestamp = (uint32_t)([[NSDate date] timeIntervalSince1970] + [MTContext fixedTimeDifference]);
uint32_t timestampLe = OSSwapHostToLittleInt32(timestamp);
uint8_t *timestampBytes = (uint8_t *)&timestampLe;
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);

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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<Empty>()
private var crownIcon: UIImageView?
private let title = ComponentView<Empty>()
private var component: PeerComponent?
@ -432,6 +439,7 @@ private final class PeerComponent: Component {
}
func update(component: PeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, 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<Empty>
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

View file

@ -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<ChatSidePanelEnvironment>?
private var topPanelBackgroundView: GlassBackgroundView?
private var topPanel: ComponentView<ChatSidePanelEnvironment>?
override public init(frame: CGRect) {
@ -106,6 +105,8 @@ public final class ChatFloatingTopicsPanel: Component {
func update(component: ChatFloatingTopicsPanel, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ChatSidePanelEnvironment>, transition: ComponentTransition) -> CGSize {
let environment = environment[ChatSidePanelEnvironment.self].value
var currentPanelBackgroundFrame: CGRect?
if case .side = component.location {
let sidePanel: ComponentView<ChatSidePanelEnvironment>
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)

View file

@ -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<Int> = 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)

View file

@ -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<Empty>?
@ -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<Empty>, 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
}
}

View file

@ -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)
// }
//

View file

@ -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

View file

@ -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,

View file

@ -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<Empty>()
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
}
}
}

View file

@ -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) {

View file

@ -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<Empty>()
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<Empty>, 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<Empty>, 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<Empty>()
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<Empty>, 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<Empty>] = []
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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -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<Empty>()
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<Empty>, 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<Empty>, 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<Empty>()
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<Empty>, 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<Empty>] = []
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<Empty>, 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)

View file

@ -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
}

View file

@ -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

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "crown.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -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