mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Stories
This commit is contained in:
parent
0e5337636d
commit
5145b9e605
23 changed files with 1515 additions and 1013 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 *)×tamp;
|
||||
|
||||
uint32_t timestamp = (uint32_t)([[NSDate date] timeIntervalSince1970] + [MTContext fixedTimeDifference]);
|
||||
uint32_t timestampLe = OSSwapHostToLittleInt32(timestamp);
|
||||
uint8_t *timestampBytes = (uint8_t *)×tampLe;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
cHMAC[CC_SHA256_DIGEST_LENGTH - 4 + i] ^= timestampValue[i];
|
||||
cHMAC[CC_SHA256_DIGEST_LENGTH - 4 + i] ^= timestampBytes[i];
|
||||
}
|
||||
_helloRandom = [[NSData alloc] initWithBytes:cHMAC length:32];
|
||||
memcpy(((uint8_t *)helloData.mutableBytes) + 11, cHMAC, 32);
|
||||
|
||||
|
||||
_helloRandom = [[NSData alloc] initWithBytes:cHMAC length:CC_SHA256_DIGEST_LENGTH];
|
||||
memcpy(((uint8_t *)helloData.mutableBytes) + 11, cHMAC, CC_SHA256_DIGEST_LENGTH);
|
||||
|
||||
[strongSelf->_socket writeData:helloData];
|
||||
[strongSelf->_socket readDataToLength:5 withTimeout:-1 tag:MTTcpSocksReceiveHelloResponse];
|
||||
} else {
|
||||
|
|
@ -1313,7 +1301,7 @@ struct ctr_state {
|
|||
[partitionedCompleteData appendData:[[NSData alloc] initWithBytes:helloHeader length:6]];
|
||||
}
|
||||
|
||||
NSUInteger limit = 2878;
|
||||
NSUInteger limit = 16408;
|
||||
NSUInteger offset = 0;
|
||||
while (offset < completeData.length) {
|
||||
NSUInteger partLength = MIN(limit, completeData.length - offset);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// }
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Stories/LiveChatCrown.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Stories/LiveChatCrown.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "crown.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Stories/LiveChatCrown.imageset/crown.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Stories/LiveChatCrown.imageset/crown.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue