Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>$(APP_NAME)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>$(PRODUCT_BUNDLE_SHORT_VERSION)</string>
<key>CFBundleVersion</key>
<string>${BUILD_NUMBER}</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>WKAppBundleIdentifier</key>
<string>$(APP_BUNDLE_ID).watchkitapp</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.watchkit</string>
</dict>
<key>WKExtensionDelegateClassName</key>
<string>TGExtensionDelegate</string>
</dict>
</plist>
Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

@@ -0,0 +1,12 @@
#import <Foundation/Foundation.h>
@interface SAtomic : NSObject
- (instancetype)initWithValue:(id)value;
- (instancetype)initWithValue:(id)value recursive:(bool)recursive;
- (id)swap:(id)newValue;
- (id)value;
- (id)modify:(id (^)(id))f;
- (id)with:(id (^)(id))f;
@end
@@ -0,0 +1,93 @@
#import "SAtomic.h"
#import <pthread.h>
@interface SAtomic ()
{
pthread_mutex_t _lock;
pthread_mutexattr_t _attr;
bool _isRecursive;
id _value;
}
@end
@implementation SAtomic
- (instancetype)initWithValue:(id)value
{
self = [super init];
if (self != nil)
{
pthread_mutex_init(&_lock, NULL);
_value = value;
}
return self;
}
- (instancetype)initWithValue:(id)value recursive:(bool)recursive {
self = [super init];
if (self != nil)
{
_isRecursive = recursive;
if (recursive) {
pthread_mutexattr_init(&_attr);
pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&_lock, &_attr);
} else {
pthread_mutex_init(&_lock, NULL);
}
_value = value;
}
return self;
}
- (void)dealloc {
if (_isRecursive) {
pthread_mutexattr_destroy(&_attr);
}
pthread_mutex_destroy(&_lock);
}
- (id)swap:(id)newValue
{
id previousValue = nil;
pthread_mutex_lock(&_lock);
previousValue = _value;
_value = newValue;
pthread_mutex_unlock(&_lock);
return previousValue;
}
- (id)value
{
id previousValue = nil;
pthread_mutex_lock(&_lock);
previousValue = _value;
pthread_mutex_unlock(&_lock);
return previousValue;
}
- (id)modify:(id (^)(id))f
{
id newValue = nil;
pthread_mutex_lock(&_lock);
newValue = f(_value);
_value = newValue;
pthread_mutex_unlock(&_lock);
return newValue;
}
- (id)with:(id (^)(id))f
{
id result = nil;
pthread_mutex_lock(&_lock);
result = f(_value);
pthread_mutex_unlock(&_lock);
return result;
}
@end
@@ -0,0 +1,11 @@
#import <Foundation/Foundation.h>
@interface SBag : NSObject
- (NSInteger)addItem:(id)item;
- (void)enumerateItems:(void (^)(id))block;
- (void)removeItem:(NSInteger)key;
- (bool)isEmpty;
- (NSArray *)copyItems;
@end
@@ -0,0 +1,74 @@
#import "SBag.h"
@interface SBag ()
{
NSInteger _nextKey;
NSMutableArray *_items;
NSMutableArray *_itemKeys;
}
@end
@implementation SBag
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_items = [[NSMutableArray alloc] init];
_itemKeys = [[NSMutableArray alloc] init];
}
return self;
}
- (NSInteger)addItem:(id)item
{
if (item == nil)
return -1;
NSInteger key = _nextKey;
[_items addObject:item];
[_itemKeys addObject:@(key)];
_nextKey++;
return key;
}
- (void)enumerateItems:(void (^)(id))block
{
if (block)
{
for (id item in _items)
{
block(item);
}
}
}
- (void)removeItem:(NSInteger)key
{
NSUInteger index = 0;
for (NSNumber *itemKey in _itemKeys)
{
if ([itemKey integerValue] == key)
{
[_items removeObjectAtIndex:index];
[_itemKeys removeObjectAtIndex:index];
break;
}
index++;
}
}
- (bool)isEmpty
{
return _items.count == 0;
}
- (NSArray *)copyItems
{
return [[NSArray alloc] initWithArray:_items];
}
@end
@@ -0,0 +1,7 @@
#import <SSignalKit/SDisposable.h>
@interface SBlockDisposable : NSObject <SDisposable>
- (instancetype)initWithBlock:(void (^)())block;
@end
@@ -0,0 +1,58 @@
#import "SBlockDisposable.h"
#import <libkern/OSAtomic.h>
#import <objc/runtime.h>
@interface SBlockDisposable ()
{
void *_block;
}
@end
@implementation SBlockDisposable
- (instancetype)initWithBlock:(void (^)())block
{
self = [super init];
if (self != nil)
{
_block = (__bridge_retained void *)[block copy];
}
return self;
}
- (void)dealloc
{
void *block = _block;
if (block != NULL)
{
if (OSAtomicCompareAndSwapPtr(block, 0, &_block))
{
if (block != nil)
{
__strong id strongBlock = (__bridge_transfer id)block;
strongBlock = nil;
}
}
}
}
- (void)dispose
{
void *block = _block;
if (block != NULL)
{
if (OSAtomicCompareAndSwapPtr(block, 0, &_block))
{
if (block != nil)
{
__strong id strongBlock = (__bridge_transfer id)block;
((dispatch_block_t)strongBlock)();
strongBlock = nil;
}
}
}
}
@end
@@ -0,0 +1,7 @@
#import <Foundation/Foundation.h>
@protocol SDisposable <NSObject>
- (void)dispose;
@end
@@ -0,0 +1,10 @@
#import <SSignalKit/SDisposable.h>
@class SSignal;
@interface SDisposableSet : NSObject <SDisposable>
- (void)add:(id<SDisposable>)disposable;
- (void)remove:(id<SDisposable>)disposable;
@end
@@ -0,0 +1,95 @@
#import "SDisposableSet.h"
#import "SSignal.h"
#import <libkern/OSAtomic.h>
@interface SDisposableSet ()
{
OSSpinLock _lock;
bool _disposed;
id<SDisposable> _singleDisposable;
NSArray *_multipleDisposables;
}
@end
@implementation SDisposableSet
- (void)add:(id<SDisposable>)disposable
{
if (disposable == nil)
return;
bool dispose = false;
OSSpinLockLock(&_lock);
dispose = _disposed;
if (!dispose)
{
if (_multipleDisposables != nil)
{
NSMutableArray *multipleDisposables = [[NSMutableArray alloc] initWithArray:_multipleDisposables];
[multipleDisposables addObject:disposable];
_multipleDisposables = multipleDisposables;
}
else if (_singleDisposable != nil)
{
NSMutableArray *multipleDisposables = [[NSMutableArray alloc] initWithObjects:_singleDisposable, disposable, nil];
_multipleDisposables = multipleDisposables;
_singleDisposable = nil;
}
else
{
_singleDisposable = disposable;
}
}
OSSpinLockUnlock(&_lock);
if (dispose)
[disposable dispose];
}
- (void)remove:(id<SDisposable>)disposable {
OSSpinLockLock(&_lock);
if (_multipleDisposables != nil)
{
NSMutableArray *multipleDisposables = [[NSMutableArray alloc] initWithArray:_multipleDisposables];
[multipleDisposables removeObject:disposable];
_multipleDisposables = multipleDisposables;
}
else if (_singleDisposable == disposable)
{
_singleDisposable = nil;
}
OSSpinLockUnlock(&_lock);
}
- (void)dispose
{
id<SDisposable> singleDisposable = nil;
NSArray *multipleDisposables = nil;
OSSpinLockLock(&_lock);
if (!_disposed)
{
_disposed = true;
singleDisposable = _singleDisposable;
multipleDisposables = _multipleDisposables;
_singleDisposable = nil;
_multipleDisposables = nil;
}
OSSpinLockUnlock(&_lock);
if (singleDisposable != nil)
[singleDisposable dispose];
if (multipleDisposables != nil)
{
for (id<SDisposable> disposable in multipleDisposables)
{
[disposable dispose];
}
}
}
@end
@@ -0,0 +1,7 @@
#import <SSignalKit/SDisposable.h>
@interface SMetaDisposable : NSObject <SDisposable>
- (void)setDisposable:(id<SDisposable>)disposable;
@end
@@ -0,0 +1,53 @@
#import "SMetaDisposable.h"
#import <libkern/OSAtomic.h>
@interface SMetaDisposable ()
{
OSSpinLock _lock;
bool _disposed;
id<SDisposable> _disposable;
}
@end
@implementation SMetaDisposable
- (void)setDisposable:(id<SDisposable>)disposable
{
id<SDisposable> previousDisposable = nil;
bool dispose = false;
OSSpinLockLock(&_lock);
dispose = _disposed;
if (!dispose)
{
previousDisposable = _disposable;
_disposable = disposable;
}
OSSpinLockUnlock(&_lock);
if (previousDisposable != nil)
[previousDisposable dispose];
if (dispose)
[disposable dispose];
}
- (void)dispose
{
id<SDisposable> disposable = nil;
OSSpinLockLock(&_lock);
if (!_disposed)
{
disposable = _disposable;
_disposed = true;
}
OSSpinLockUnlock(&_lock);
if (disposable != nil)
[disposable dispose];
}
@end
@@ -0,0 +1,11 @@
#import <SSignalKit/SSignal.h>
@interface SMulticastSignalManager : NSObject
- (SSignal *)multicastedSignalForKey:(NSString *)key producer:(SSignal *(^)())producer;
- (void)startStandaloneSignalIfNotRunningForKey:(NSString *)key producer:(SSignal *(^)())producer;
- (SSignal *)multicastedPipeForKey:(NSString *)key;
- (void)putNext:(id)next toMulticastedPipeForKey:(NSString *)key;
@end
@@ -0,0 +1,171 @@
#import "SMulticastSignalManager.h"
#import "SSignal+Multicast.h"
#import "SSignal+SideEffects.h"
#import "SBag.h"
#import "SMetaDisposable.h"
#import "SBlockDisposable.h"
#import <libkern/OSAtomic.h>
@interface SMulticastSignalManager ()
{
OSSpinLock _lock;
NSMutableDictionary *_multicastSignals;
NSMutableDictionary *_standaloneSignalDisposables;
NSMutableDictionary *_pipeListeners;
}
@end
@implementation SMulticastSignalManager
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_multicastSignals = [[NSMutableDictionary alloc] init];
_standaloneSignalDisposables = [[NSMutableDictionary alloc] init];
_pipeListeners = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)dealloc
{
NSArray *disposables = nil;
OSSpinLockLock(&_lock);
disposables = [_standaloneSignalDisposables allValues];
OSSpinLockUnlock(&_lock);
for (id<SDisposable> disposable in disposables)
{
[disposable dispose];
}
}
- (SSignal *)multicastedSignalForKey:(NSString *)key producer:(SSignal *(^)())producer
{
if (key == nil)
{
if (producer)
return producer();
else
return nil;
}
SSignal *signal = nil;
OSSpinLockLock(&_lock);
signal = _multicastSignals[key];
if (signal == nil)
{
__weak SMulticastSignalManager *weakSelf = self;
if (producer)
signal = producer();
if (signal != nil)
{
signal = [[signal onDispose:^
{
__strong SMulticastSignalManager *strongSelf = weakSelf;
if (strongSelf != nil)
{
OSSpinLockLock(&strongSelf->_lock);
[strongSelf->_multicastSignals removeObjectForKey:key];
OSSpinLockUnlock(&strongSelf->_lock);
}
}] multicast];
_multicastSignals[key] = signal;
}
}
OSSpinLockUnlock(&_lock);
return signal;
}
- (void)startStandaloneSignalIfNotRunningForKey:(NSString *)key producer:(SSignal *(^)())producer
{
if (key == nil)
return;
bool produce = false;
OSSpinLockLock(&_lock);
if (_standaloneSignalDisposables[key] == nil)
{
_standaloneSignalDisposables[key] = [[SMetaDisposable alloc] init];
produce = true;
}
OSSpinLockUnlock(&_lock);
if (produce)
{
__weak SMulticastSignalManager *weakSelf = self;
id<SDisposable> disposable = [producer() startWithNext:nil error:^(__unused id error)
{
__strong SMulticastSignalManager *strongSelf = weakSelf;
if (strongSelf != nil)
{
OSSpinLockLock(&strongSelf->_lock);
[strongSelf->_standaloneSignalDisposables removeObjectForKey:key];
OSSpinLockUnlock(&strongSelf->_lock);
}
} completed:^
{
__strong SMulticastSignalManager *strongSelf = weakSelf;
if (strongSelf != nil)
{
OSSpinLockLock(&strongSelf->_lock);
[strongSelf->_standaloneSignalDisposables removeObjectForKey:key];
OSSpinLockUnlock(&strongSelf->_lock);
}
}];
OSSpinLockLock(&_lock);
[(SMetaDisposable *)_standaloneSignalDisposables[key] setDisposable:disposable];
OSSpinLockUnlock(&_lock);
}
}
- (SSignal *)multicastedPipeForKey:(NSString *)key
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
{
OSSpinLockLock(&_lock);
SBag *bag = _pipeListeners[key];
if (bag == nil)
{
bag = [[SBag alloc] init];
_pipeListeners[key] = bag;
}
NSInteger index = [bag addItem:[^(id next)
{
[subscriber putNext:next];
} copy]];
OSSpinLockUnlock(&_lock);
return [[SBlockDisposable alloc] initWithBlock:^
{
OSSpinLockLock(&_lock);
SBag *bag = _pipeListeners[key];
[bag removeItem:index];
if ([bag isEmpty]) {
[_pipeListeners removeObjectForKey:key];
}
OSSpinLockUnlock(&_lock);
}];
}];
}
- (void)putNext:(id)next toMulticastedPipeForKey:(NSString *)key
{
OSSpinLockLock(&_lock);
NSArray *pipeListeners = [(SBag *)_pipeListeners[key] copyItems];
OSSpinLockUnlock(&_lock);
for (void (^listener)(id) in pipeListeners)
{
listener(next);
}
}
@end
@@ -0,0 +1,19 @@
#import <Foundation/Foundation.h>
@interface SQueue : NSObject
+ (SQueue *)mainQueue;
+ (SQueue *)concurrentDefaultQueue;
+ (SQueue *)concurrentBackgroundQueue;
+ (SQueue *)wrapConcurrentNativeQueue:(dispatch_queue_t)nativeQueue;
- (void)dispatch:(dispatch_block_t)block;
- (void)dispatchSync:(dispatch_block_t)block;
- (void)dispatch:(dispatch_block_t)block synchronous:(bool)synchronous;
- (dispatch_queue_t)_dispatch_queue;
- (bool)isCurrentQueue;
@end
@@ -0,0 +1,124 @@
#import "SQueue.h"
static const void *SQueueSpecificKey = &SQueueSpecificKey;
@interface SQueue ()
{
dispatch_queue_t _queue;
void *_queueSpecific;
bool _specialIsMainQueue;
}
@end
@implementation SQueue
+ (SQueue *)mainQueue
{
static SQueue *queue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
queue = [[SQueue alloc] initWithNativeQueue:dispatch_get_main_queue() queueSpecific:NULL];
queue->_specialIsMainQueue = true;
});
return queue;
}
+ (SQueue *)concurrentDefaultQueue
{
static SQueue *queue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
queue = [[SQueue alloc] initWithNativeQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) queueSpecific:NULL];
});
return queue;
}
+ (SQueue *)concurrentBackgroundQueue
{
static SQueue *queue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
queue = [[SQueue alloc] initWithNativeQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0) queueSpecific:NULL];
});
return queue;
}
+ (SQueue *)wrapConcurrentNativeQueue:(dispatch_queue_t)nativeQueue
{
return [[SQueue alloc] initWithNativeQueue:nativeQueue queueSpecific:NULL];
}
- (instancetype)init
{
dispatch_queue_t queue = dispatch_queue_create(NULL, NULL);
dispatch_queue_set_specific(queue, SQueueSpecificKey, (__bridge void *)self, NULL);
return [self initWithNativeQueue:queue queueSpecific:(__bridge void *)self];
}
- (instancetype)initWithNativeQueue:(dispatch_queue_t)queue queueSpecific:(void *)queueSpecific
{
self = [super init];
if (self != nil)
{
_queue = queue;
_queueSpecific = queueSpecific;
}
return self;
}
- (dispatch_queue_t)_dispatch_queue
{
return _queue;
}
- (void)dispatch:(dispatch_block_t)block
{
if (_queueSpecific != NULL && dispatch_get_specific(SQueueSpecificKey) == _queueSpecific)
block();
else if (_specialIsMainQueue && [NSThread isMainThread])
block();
else
dispatch_async(_queue, block);
}
- (void)dispatchSync:(dispatch_block_t)block
{
if (_queueSpecific != NULL && dispatch_get_specific(SQueueSpecificKey) == _queueSpecific)
block();
else if (_specialIsMainQueue && [NSThread isMainThread])
block();
else
dispatch_sync(_queue, block);
}
- (void)dispatch:(dispatch_block_t)block synchronous:(bool)synchronous {
if (_queueSpecific != NULL && dispatch_get_specific(SQueueSpecificKey) == _queueSpecific)
block();
else if (_specialIsMainQueue && [NSThread isMainThread])
block();
else {
if (synchronous) {
dispatch_sync(_queue, block);
} else {
dispatch_async(_queue, block);
}
}
}
- (bool)isCurrentQueue
{
if (_queueSpecific != NULL && dispatch_get_specific(SQueueSpecificKey) == _queueSpecific)
return true;
else if (_specialIsMainQueue && [NSThread isMainThread])
return true;
return false;
}
@end
@@ -0,0 +1,8 @@
#import <SSignalKit/SSignal.h>
@interface SSignal (Accumulate)
- (SSignal *)reduceLeft:(id)value with:(id (^)(id, id))f;
- (SSignal *)reduceLeftWithPassthrough:(id)value with:(id (^)(id, id, void (^)(id)))f;
@end
@@ -0,0 +1,52 @@
#import "SSignal+Accumulate.h"
@implementation SSignal (Accumulate)
- (SSignal *)reduceLeft:(id)value with:(id (^)(id, id))f
{
return [[SSignal alloc] initWithGenerator:^(SSubscriber *subscriber)
{
__block id intermediateResult = value;
return [self startWithNext:^(id next)
{
intermediateResult = f(intermediateResult, next);
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
if (intermediateResult != nil)
[subscriber putNext:intermediateResult];
[subscriber putCompletion];
}];
}];
}
- (SSignal *)reduceLeftWithPassthrough:(id)value with:(id (^)(id, id, void (^)(id)))f
{
return [[SSignal alloc] initWithGenerator:^(SSubscriber *subscriber)
{
__block id intermediateResult = value;
void (^emit)(id) = ^(id next)
{
[subscriber putNext:next];
};
return [self startWithNext:^(id next)
{
intermediateResult = f(intermediateResult, next, emit);
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
if (intermediateResult != nil)
[subscriber putNext:intermediateResult];
[subscriber putCompletion];
}];
}];
}
@end
@@ -0,0 +1,9 @@
#import <SSignalKit/SSignal.h>
@interface SSignal (Catch)
- (SSignal *)catch:(SSignal *(^)(id error))f;
- (SSignal *)restart;
- (SSignal *)retryIf:(bool (^)(id error))predicate;
@end
@@ -0,0 +1,147 @@
#import "SSignal+Catch.h"
#import "SMetaDisposable.h"
#import "SDisposableSet.h"
#import "SBlockDisposable.h"
#import "SAtomic.h"
@implementation SSignal (Catch)
- (SSignal *)catch:(SSignal *(^)(id error))f
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
SDisposableSet *disposable = [[SDisposableSet alloc] init];
[disposable add:[self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
SSignal *signal = f(error);
[disposable add:[signal startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}]];
} completed:^
{
[subscriber putCompletion];
}]];
return disposable;
}];
}
static dispatch_block_t recursiveBlock(void (^block)(dispatch_block_t recurse))
{
return ^
{
block(recursiveBlock(block));
};
}
- (SSignal *)restart
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
SAtomic *shouldRestart = [[SAtomic alloc] initWithValue:@true];
SMetaDisposable *currentDisposable = [[SMetaDisposable alloc] init];
void (^start)() = recursiveBlock(^(dispatch_block_t recurse)
{
NSNumber *currentShouldRestart = [shouldRestart with:^id(NSNumber *current)
{
return current;
}];
if ([currentShouldRestart boolValue])
{
id<SDisposable> disposable = [self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
recurse();
}];
[currentDisposable setDisposable:disposable];
}
});
start();
return [[SBlockDisposable alloc] initWithBlock:^
{
[currentDisposable dispose];
[shouldRestart modify:^id(__unused id current)
{
return @false;
}];
}];
}];
}
- (SSignal *)retryIf:(bool (^)(id error))predicate {
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
SAtomic *shouldRestart = [[SAtomic alloc] initWithValue:@true];
SMetaDisposable *currentDisposable = [[SMetaDisposable alloc] init];
void (^start)() = recursiveBlock(^(dispatch_block_t recurse)
{
NSNumber *currentShouldRestart = [shouldRestart with:^id(NSNumber *current)
{
return current;
}];
if ([currentShouldRestart boolValue])
{
id<SDisposable> disposable = [self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
if (predicate(error)) {
recurse();
} else {
[subscriber putError:error];
}
} completed:^
{
[shouldRestart modify:^id(__unused id current) {
return @false;
}];
[subscriber putCompletion];
}];
[currentDisposable setDisposable:disposable];
} else {
[subscriber putCompletion];
}
});
start();
return [[SBlockDisposable alloc] initWithBlock:^
{
[currentDisposable dispose];
[shouldRestart modify:^id(__unused id current)
{
return @false;
}];
}];
}];
}
@end
@@ -0,0 +1,10 @@
#import <SSignalKit/SSignal.h>
@interface SSignal (Combine)
+ (SSignal *)combineSignals:(NSArray *)signals;
+ (SSignal *)combineSignals:(NSArray *)signals withInitialStates:(NSArray *)initialStates;
+ (SSignal *)mergeSignals:(NSArray *)signals;
@end
@@ -0,0 +1,177 @@
#import "SSignal+Combine.h"
#import "SAtomic.h"
#import "SDisposableSet.h"
#import "SSignal+Single.h"
@interface SSignalCombineState : NSObject
@property (nonatomic, strong, readonly) NSDictionary *latestValues;
@property (nonatomic, strong, readonly) NSArray *completedStatuses;
@property (nonatomic) bool error;
@end
@implementation SSignalCombineState
- (instancetype)initWithLatestValues:(NSDictionary *)latestValues completedStatuses:(NSArray *)completedStatuses error:(bool)error
{
self = [super init];
if (self != nil)
{
_latestValues = latestValues;
_completedStatuses = completedStatuses;
_error = error;
}
return self;
}
@end
@implementation SSignal (Combine)
+ (SSignal *)combineSignals:(NSArray *)signals
{
if (signals.count == 0)
return [SSignal single:@[]];
else
return [self combineSignals:signals withInitialStates:nil];
}
+ (SSignal *)combineSignals:(NSArray *)signals withInitialStates:(NSArray *)initialStates
{
return [[SSignal alloc] initWithGenerator:^(SSubscriber *subscriber)
{
NSMutableArray *completedStatuses = [[NSMutableArray alloc] init];
for (NSUInteger i = 0; i < signals.count; i++)
{
[completedStatuses addObject:@false];
}
NSMutableDictionary *initialLatestValues = [[NSMutableDictionary alloc] init];
for (NSUInteger i = 0; i < initialStates.count; i++)
{
initialLatestValues[@(i)] = initialStates[i];
}
SAtomic *combineState = [[SAtomic alloc] initWithValue:[[SSignalCombineState alloc] initWithLatestValues:initialLatestValues completedStatuses:completedStatuses error:false]];
SDisposableSet *compositeDisposable = [[SDisposableSet alloc] init];
NSUInteger index = 0;
NSUInteger count = signals.count;
for (SSignal *signal in signals)
{
id<SDisposable> disposable = [signal startWithNext:^(id next)
{
SSignalCombineState *currentState = [combineState modify:^id(SSignalCombineState *state)
{
NSMutableDictionary *latestValues = [[NSMutableDictionary alloc] initWithDictionary:state.latestValues];
latestValues[@(index)] = next;
return [[SSignalCombineState alloc] initWithLatestValues:latestValues completedStatuses:state.completedStatuses error:state.error];
}];
NSMutableArray *latestValues = [[NSMutableArray alloc] init];
for (NSUInteger i = 0; i < count; i++)
{
id value = currentState.latestValues[@(i)];
if (value == nil)
{
latestValues = nil;
break;
}
latestValues[i] = value;
}
if (latestValues != nil)
[subscriber putNext:latestValues];
}
error:^(id error)
{
__block bool hadError = false;
[combineState modify:^id(SSignalCombineState *state)
{
hadError = state.error;
return [[SSignalCombineState alloc] initWithLatestValues:state.latestValues completedStatuses:state.completedStatuses error:true];
}];
if (!hadError)
[subscriber putError:error];
} completed:^
{
__block bool wasCompleted = false;
__block bool isCompleted = false;
[combineState modify:^id(SSignalCombineState *state)
{
NSMutableArray *completedStatuses = [[NSMutableArray alloc] initWithArray:state.completedStatuses];
bool everyStatusWasCompleted = true;
for (NSNumber *nStatus in completedStatuses)
{
if (![nStatus boolValue])
{
everyStatusWasCompleted = false;
break;
}
}
completedStatuses[index] = @true;
bool everyStatusIsCompleted = true;
for (NSNumber *nStatus in completedStatuses)
{
if (![nStatus boolValue])
{
everyStatusIsCompleted = false;
break;
}
}
wasCompleted = everyStatusWasCompleted;
isCompleted = everyStatusIsCompleted;
return [[SSignalCombineState alloc] initWithLatestValues:state.latestValues completedStatuses:completedStatuses error:state.error];
}];
if (!wasCompleted && isCompleted)
[subscriber putCompletion];
}];
[compositeDisposable add:disposable];
index++;
}
return compositeDisposable;
}];
}
+ (SSignal *)mergeSignals:(NSArray *)signals
{
if (signals.count == 0)
return [SSignal complete];
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
{
SDisposableSet *disposables = [[SDisposableSet alloc] init];
SAtomic *completedStates = [[SAtomic alloc] initWithValue:[[NSSet alloc] init]];
NSInteger index = -1;
NSUInteger count = signals.count;
for (SSignal *signal in signals)
{
index++;
id<SDisposable> disposable = [signal startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
NSSet *set = [completedStates modify:^id(NSSet *set)
{
return [set setByAddingObject:@(index)];
}];
if (set.count == count)
[subscriber putCompletion];
}];
[disposables add:disposable];
}
return disposables;
}];
}
@end
@@ -0,0 +1,14 @@
#import <SSignalKit/SSignal.h>
#import <SSignalKit/SQueue.h>
#import <SSignalKit/SThreadPool.h>
@interface SSignal (Dispatch)
- (SSignal *)deliverOn:(SQueue *)queue;
- (SSignal *)deliverOnThreadPool:(SThreadPool *)threadPool;
- (SSignal *)startOn:(SQueue *)queue;
- (SSignal *)startOnThreadPool:(SThreadPool *)threadPool;
- (SSignal *)throttleOn:(SQueue *)queue delay:(NSTimeInterval)delay;
@end
@@ -0,0 +1,212 @@
#import "SSignal+Dispatch.h"
#import "SAtomic.h"
#import "STimer.h"
#import "SBlockDisposable.h"
#import "SMetaDisposable.h"
@interface SSignal_ThrottleContainer : NSObject
@property (nonatomic, strong, readonly) id value;
@property (nonatomic, readonly) bool committed;
@property (nonatomic, readonly) bool last;
@end
@implementation SSignal_ThrottleContainer
- (instancetype)initWithValue:(id)value committed:(bool)committed last:(bool)last {
self = [super init];
if (self != nil) {
_value = value;
_committed = committed;
_last = last;
}
return self;
}
@end
@implementation SSignal (Dispatch)
- (SSignal *)deliverOn:(SQueue *)queue
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
return [self startWithNext:^(id next)
{
[queue dispatch:^
{
[subscriber putNext:next];
}];
} error:^(id error)
{
[queue dispatch:^
{
[subscriber putError:error];
}];
} completed:^
{
[queue dispatch:^
{
[subscriber putCompletion];
}];
}];
}];
}
- (SSignal *)deliverOnThreadPool:(SThreadPool *)threadPool
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
SThreadPoolQueue *queue = [threadPool nextQueue];
return [self startWithNext:^(id next)
{
SThreadPoolTask *task = [[SThreadPoolTask alloc] initWithBlock:^(bool (^cancelled)())
{
if (!cancelled())
[subscriber putNext:next];
}];
[queue addTask:task];
} error:^(id error)
{
SThreadPoolTask *task = [[SThreadPoolTask alloc] initWithBlock:^(bool (^cancelled)())
{
if (!cancelled())
[subscriber putError:error];
}];
[queue addTask:task];
} completed:^
{
SThreadPoolTask *task = [[SThreadPoolTask alloc] initWithBlock:^(bool (^cancelled)())
{
if (!cancelled())
[subscriber putCompletion];
}];
[queue addTask:task];
}];
}];
}
- (SSignal *)startOn:(SQueue *)queue
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
__block bool isCancelled = false;
SMetaDisposable *disposable = [[SMetaDisposable alloc] init];
[disposable setDisposable:[[SBlockDisposable alloc] initWithBlock:^
{
isCancelled = true;
}]];
[queue dispatch:^
{
if (!isCancelled)
{
[disposable setDisposable:[self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}]];
}
}];
return disposable;
}];
}
- (SSignal *)startOnThreadPool:(SThreadPool *)threadPool
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
SMetaDisposable *disposable = [[SMetaDisposable alloc] init];
SThreadPoolTask *task = [[SThreadPoolTask alloc] initWithBlock:^(bool (^cancelled)())
{
if (cancelled && cancelled())
return;
[disposable setDisposable:[self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}]];
}];
[disposable setDisposable:[[SBlockDisposable alloc] initWithBlock:^
{
[task cancel];
}]];
[threadPool addTask:task];
return disposable;
}];
}
- (SSignal *)throttleOn:(SQueue *)queue delay:(NSTimeInterval)delay
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber) {
SAtomic *value = [[SAtomic alloc] initWithValue:nil];
STimer *timer = [[STimer alloc] initWithTimeout:delay repeat:false completion:^{
[value modify:^id(SSignal_ThrottleContainer *container) {
if (container != nil) {
if (!container.committed) {
[subscriber putNext:container.value];
container = [[SSignal_ThrottleContainer alloc] initWithValue:container.value committed:true last:container.last];
}
if (container.last) {
[subscriber putCompletion];
}
}
return container;
}];
} queue:queue];
return [[self deliverOn:queue] startWithNext:^(id next) {
[value modify:^id(SSignal_ThrottleContainer *container) {
if (container == nil) {
container = [[SSignal_ThrottleContainer alloc] initWithValue:next committed:false last:false];
}
return container;
}];
[timer invalidate];
[timer start];
} error:^(id error) {
[timer invalidate];
[subscriber putError:error];
} completed:^{
[timer invalidate];
__block bool start = false;
[value modify:^id(SSignal_ThrottleContainer *container) {
bool wasCommitted = false;
if (container == nil) {
wasCommitted = true;
container = [[SSignal_ThrottleContainer alloc] initWithValue:nil committed:true last:true];
} else {
wasCommitted = container.committed;
container = [[SSignal_ThrottleContainer alloc] initWithValue:container.value committed:container.committed last:true];
}
start = wasCommitted;
return container;
}];
if (start) {
[timer start];
} else {
[timer fireAndInvalidate];
}
}];
}];
}
@end
@@ -0,0 +1,9 @@
#import <SSignalKit/SSignal.h>
@interface SSignal (Mapping)
- (SSignal *)map:(id (^)(id))f;
- (SSignal *)filter:(bool (^)(id))f;
- (SSignal *)ignoreRepeated;
@end
@@ -0,0 +1,83 @@
#import "SSignal+Mapping.h"
#import "SAtomic.h"
@interface SSignalIgnoreRepeatedState: NSObject
@property (nonatomic, strong) id value;
@property (nonatomic) bool hasValue;
@end
@implementation SSignalIgnoreRepeatedState
@end
@implementation SSignal (Mapping)
- (SSignal *)map:(id (^)(id))f
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
return [self startWithNext:^(id next)
{
[subscriber putNext:f(next)];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}];
}];
}
- (SSignal *)filter:(bool (^)(id))f
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
return [self startWithNext:^(id next)
{
if (f(next))
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}];
}];
}
- (SSignal *)ignoreRepeated {
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber) {
SAtomic *state = [[SAtomic alloc] initWithValue:[[SSignalIgnoreRepeatedState alloc] init]];
return [self startWithNext:^(id next) {
bool shouldPassthrough = [[state with:^id(SSignalIgnoreRepeatedState *state) {
if (!state.hasValue) {
state.hasValue = true;
state.value = next;
return @true;
} else if ((state.value == nil && next == nil) || [(id<NSObject>)state.value isEqual:next]) {
return @false;
}
state.value = next;
return @true;
}] boolValue];
if (shouldPassthrough) {
[subscriber putNext:next];
}
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}];
}];
}
@end
@@ -0,0 +1,22 @@
#import <SSignalKit/SSignal.h>
@class SQueue;
@interface SSignal (Meta)
- (SSignal *)switchToLatest;
- (SSignal *)mapToSignal:(SSignal *(^)(id))f;
- (SSignal *)mapToQueue:(SSignal *(^)(id))f;
- (SSignal *)mapToThrottled:(SSignal *(^)(id))f;
- (SSignal *)then:(SSignal *)signal;
- (SSignal *)queue;
- (SSignal *)throttled;
+ (SSignal *)defer:(SSignal *(^)())generator;
@end
@interface SSignalQueue : NSObject
- (SSignal *)enqueue:(SSignal *)signal;
@end
@@ -0,0 +1,325 @@
#import "SSignal+Meta.h"
#import "SDisposableSet.h"
#import "SMetaDisposable.h"
#import "SSignal+Mapping.h"
#import "SAtomic.h"
#import "SSignal+Pipe.h"
#import <libkern/OSAtomic.h>
@interface SSignalQueueState : NSObject <SDisposable>
{
OSSpinLock _lock;
bool _executingSignal;
bool _terminated;
id<SDisposable> _disposable;
SMetaDisposable *_currentDisposable;
SSubscriber *_subscriber;
NSMutableArray *_queuedSignals;
bool _queueMode;
bool _throttleMode;
}
@end
@implementation SSignalQueueState
- (instancetype)initWithSubscriber:(SSubscriber *)subscriber queueMode:(bool)queueMode throttleMode:(bool)throttleMode
{
self = [super init];
if (self != nil)
{
_subscriber = subscriber;
_currentDisposable = [[SMetaDisposable alloc] init];
_queuedSignals = queueMode ? [[NSMutableArray alloc] init] : nil;
_queueMode = queueMode;
_throttleMode = throttleMode;
}
return self;
}
- (void)beginWithDisposable:(id<SDisposable>)disposable
{
_disposable = disposable;
}
- (void)enqueueSignal:(SSignal *)signal
{
bool startSignal = false;
OSSpinLockLock(&_lock);
if (_queueMode && _executingSignal) {
if (_throttleMode) {
[_queuedSignals removeAllObjects];
}
[_queuedSignals addObject:signal];
}
else
{
_executingSignal = true;
startSignal = true;
}
OSSpinLockUnlock(&_lock);
if (startSignal)
{
__weak SSignalQueueState *weakSelf = self;
id<SDisposable> disposable = [signal startWithNext:^(id next)
{
[_subscriber putNext:next];
} error:^(id error)
{
[_subscriber putError:error];
} completed:^
{
__strong SSignalQueueState *strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf headCompleted];
}
}];
[_currentDisposable setDisposable:disposable];
}
}
- (void)headCompleted
{
SSignal *nextSignal = nil;
bool terminated = false;
OSSpinLockLock(&_lock);
_executingSignal = false;
if (_queueMode)
{
if (_queuedSignals.count != 0)
{
nextSignal = _queuedSignals[0];
[_queuedSignals removeObjectAtIndex:0];
_executingSignal = true;
}
else
terminated = _terminated;
}
else
terminated = _terminated;
OSSpinLockUnlock(&_lock);
if (terminated)
[_subscriber putCompletion];
else if (nextSignal != nil)
{
__weak SSignalQueueState *weakSelf = self;
id<SDisposable> disposable = [nextSignal startWithNext:^(id next)
{
[_subscriber putNext:next];
} error:^(id error)
{
[_subscriber putError:error];
} completed:^
{
__strong SSignalQueueState *strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf headCompleted];
}
}];
[_currentDisposable setDisposable:disposable];
}
}
- (void)beginCompletion
{
bool executingSignal = false;
OSSpinLockLock(&_lock);
executingSignal = _executingSignal;
_terminated = true;
OSSpinLockUnlock(&_lock);
if (!executingSignal)
[_subscriber putCompletion];
}
- (void)dispose
{
[_currentDisposable dispose];
[_disposable dispose];
}
@end
@implementation SSignal (Meta)
- (SSignal *)switchToLatest
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
SSignalQueueState *state = [[SSignalQueueState alloc] initWithSubscriber:subscriber queueMode:false throttleMode:false];
[state beginWithDisposable:[self startWithNext:^(id next)
{
[state enqueueSignal:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[state beginCompletion];
}]];
return state;
}];
}
- (SSignal *)mapToSignal:(SSignal *(^)(id))f
{
return [[self map:f] switchToLatest];
}
- (SSignal *)mapToQueue:(SSignal *(^)(id))f
{
return [[self map:f] queue];
}
- (SSignal *)mapToThrottled:(SSignal *(^)(id))f {
return [[self map:f] throttled];
}
- (SSignal *)then:(SSignal *)signal
{
return [[SSignal alloc] initWithGenerator:^(SSubscriber *subscriber)
{
SDisposableSet *compositeDisposable = [[SDisposableSet alloc] init];
SMetaDisposable *currentDisposable = [[SMetaDisposable alloc] init];
[compositeDisposable add:currentDisposable];
[currentDisposable setDisposable:[self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[compositeDisposable add:[signal startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}]];
}]];
return compositeDisposable;
}];
}
- (SSignal *)queue
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
SSignalQueueState *state = [[SSignalQueueState alloc] initWithSubscriber:subscriber queueMode:true throttleMode:false];
[state beginWithDisposable:[self startWithNext:^(id next)
{
[state enqueueSignal:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[state beginCompletion];
}]];
return state;
}];
}
- (SSignal *)throttled {
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber) {
SSignalQueueState *state = [[SSignalQueueState alloc] initWithSubscriber:subscriber queueMode:true throttleMode:true];
[state beginWithDisposable:[self startWithNext:^(id next)
{
[state enqueueSignal:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[state beginCompletion];
}]];
return state;
}];
}
+ (SSignal *)defer:(SSignal *(^)())generator
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
{
return [generator() startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}];
}];
}
@end
@interface SSignalQueue () {
SPipe *_pipe;
id<SDisposable> _disposable;
}
@end
@implementation SSignalQueue
- (instancetype)init {
self = [super init];
if (self != nil) {
_pipe = [[SPipe alloc] init];
_disposable = [[_pipe.signalProducer() queue] startWithNext:nil];
}
return self;
}
- (void)dealloc {
[_disposable dispose];
}
- (SSignal *)enqueue:(SSignal *)signal {
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber) {
SPipe *disposePipe = [[SPipe alloc] init];
SSignal *proxy = [[[[signal onNext:^(id next) {
[subscriber putNext:next];
}] onError:^(id error) {
[subscriber putError:error];
}] onCompletion:^{
[subscriber putCompletion];
}] catch:^SSignal *(__unused id error) {
return [SSignal complete];
}];
_pipe.sink([proxy takeUntilReplacement:disposePipe.signalProducer()]);
return [[SBlockDisposable alloc] initWithBlock:^{
disposePipe.sink([SSignal complete]);
}];
}];
}
@end
@@ -0,0 +1,7 @@
#import <SSignalKit/SSignal.h>
@interface SSignal (Multicast)
- (SSignal *)multicast;
@end
@@ -0,0 +1,158 @@
#import "SSignal+Multicast.h"
#import <libkern/OSAtomic.h>
#import "SBag.h"
#import "SBlockDisposable.h"
typedef enum {
SSignalMulticastStateReady,
SSignalMulticastStateStarted,
SSignalMulticastStateCompleted
} SSignalMulticastState;
@interface SSignalMulticastSubscribers : NSObject
{
volatile OSSpinLock _lock;
SBag *_subscribers;
SSignalMulticastState _state;
id<SDisposable> _disposable;
}
@end
@implementation SSignalMulticastSubscribers
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_subscribers = [[SBag alloc] init];
}
return self;
}
- (void)setDisposable:(id<SDisposable>)disposable
{
[_disposable dispose];
_disposable = disposable;
}
- (id<SDisposable>)addSubscriber:(SSubscriber *)subscriber start:(bool *)start
{
OSSpinLockLock(&_lock);
NSInteger index = [_subscribers addItem:subscriber];
switch (_state) {
case SSignalMulticastStateReady:
*start = true;
_state = SSignalMulticastStateStarted;
break;
default:
break;
}
OSSpinLockUnlock(&_lock);
return [[SBlockDisposable alloc] initWithBlock:^
{
[self remove:index];
}];
}
- (void)remove:(NSInteger)index
{
id<SDisposable> currentDisposable = nil;
OSSpinLockLock(&_lock);
[_subscribers removeItem:index];
switch (_state) {
case SSignalMulticastStateStarted:
if ([_subscribers isEmpty])
{
currentDisposable = _disposable;
_disposable = nil;
}
break;
default:
break;
}
OSSpinLockUnlock(&_lock);
[currentDisposable dispose];
}
- (void)notifyNext:(id)next
{
NSArray *currentSubscribers = nil;
OSSpinLockLock(&_lock);
currentSubscribers = [_subscribers copyItems];
OSSpinLockUnlock(&_lock);
for (SSubscriber *subscriber in currentSubscribers)
{
[subscriber putNext:next];
}
}
- (void)notifyError:(id)error
{
NSArray *currentSubscribers = nil;
OSSpinLockLock(&_lock);
currentSubscribers = [_subscribers copyItems];
_state = SSignalMulticastStateCompleted;
OSSpinLockUnlock(&_lock);
for (SSubscriber *subscriber in currentSubscribers)
{
[subscriber putError:error];
}
}
- (void)notifyCompleted
{
NSArray *currentSubscribers = nil;
OSSpinLockLock(&_lock);
currentSubscribers = [_subscribers copyItems];
_state = SSignalMulticastStateCompleted;
OSSpinLockUnlock(&_lock);
for (SSubscriber *subscriber in currentSubscribers)
{
[subscriber putCompletion];
}
}
@end
@implementation SSignal (Multicast)
- (SSignal *)multicast
{
SSignalMulticastSubscribers *subscribers = [[SSignalMulticastSubscribers alloc] init];
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
bool start = false;
id<SDisposable> currentDisposable = [subscribers addSubscriber:subscriber start:&start];
if (start)
{
id<SDisposable> disposable = [self startWithNext:^(id next)
{
[subscribers notifyNext:next];
} error:^(id error)
{
[subscribers notifyError:error];
} completed:^
{
[subscribers notifyCompleted];
}];
[subscribers setDisposable:[[SBlockDisposable alloc] initWithBlock:^
{
[disposable dispose];
}]];
}
return currentDisposable;
}];
}
@end
@@ -0,0 +1,11 @@
#import <SSignalKit/SSignalKit.h>
@interface SPipe : NSObject
@property (nonatomic, copy, readonly) SSignal *(^signalProducer)();
@property (nonatomic, copy, readonly) void (^sink)(id);
- (instancetype)initWithReplay:(bool)replay;
@end
@@ -0,0 +1,103 @@
#import "SSignal+Pipe.h"
#import "SBlockDisposable.h"
#import "SAtomic.h"
#import "SBag.h"
@interface SPipeReplayState : NSObject
@property (nonatomic, readonly) bool hasReceivedValue;
@property (nonatomic, strong, readonly) id recentValue;
@end
@implementation SPipeReplayState
- (instancetype)initWithReceivedValue:(bool)receivedValue recentValue:(id)recentValue
{
self = [super init];
if (self != nil)
{
_hasReceivedValue = receivedValue;
_recentValue = recentValue;
}
return self;
}
@end
@implementation SPipe
- (instancetype)init
{
return [self initWithReplay:false];
}
- (instancetype)initWithReplay:(bool)replay
{
self = [super init];
if (self != nil)
{
SAtomic *subscribers = [[SAtomic alloc] initWithValue:[[SBag alloc] init]];
SAtomic *replayState = replay ? [[SAtomic alloc] initWithValue:[[SPipeReplayState alloc] initWithReceivedValue:false recentValue:nil]] : nil;
_signalProducer = [^SSignal *
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
{
__block NSUInteger index = 0;
[subscribers with:^id(SBag *bag)
{
index = [bag addItem:[^(id next)
{
[subscriber putNext:next];
} copy]];
return nil;
}];
if (replay)
{
[replayState with:^id(SPipeReplayState *state)
{
if (state.hasReceivedValue)
[subscriber putNext:state.recentValue];
return nil;
}];
}
return [[SBlockDisposable alloc] initWithBlock:^
{
[subscribers with:^id(SBag *bag)
{
[bag removeItem:index];
return nil;
}];
}];
}];
} copy];
_sink = [^(id next)
{
NSArray *items = [subscribers with:^id(SBag *bag)
{
return [bag copyItems];
}];
for (void (^item)(id) in items)
{
item(next);
}
if (replay)
{
[replayState modify:^id(__unused SPipeReplayState *state)
{
return [[SPipeReplayState alloc] initWithReceivedValue:true recentValue:next];
}];
}
} copy];
}
return self;
}
@end
@@ -0,0 +1,13 @@
#import <SSignalKit/SSignal.h>
@interface SSignal (SideEffects)
- (SSignal *)onStart:(void (^)())f;
- (SSignal *)onNext:(void (^)(id next))f;
- (SSignal *)afterNext:(void (^)(id next))f;
- (SSignal *)onError:(void (^)(id error))f;
- (SSignal *)onCompletion:(void (^)())f;
- (SSignal *)afterCompletion:(void (^)())f;
- (SSignal *)onDispose:(void (^)())f;
@end
@@ -0,0 +1,141 @@
#import "SSignal+SideEffects.h"
#import "SBlockDisposable.h"
#import "SDisposableSet.h"
@implementation SSignal (SideEffects)
- (SSignal *)onStart:(void (^)())f
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
f();
return [self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}];
}];
}
- (SSignal *)onNext:(void (^)(id next))f
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
return [self startWithNext:^(id next)
{
f(next);
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}];
}];
}
- (SSignal *)afterNext:(void (^)(id next))f
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
return [self startWithNext:^(id next)
{
[subscriber putNext:next];
f(next);
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}];
}];
}
- (SSignal *)onError:(void (^)(id error))f
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
return [self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
f(error);
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}];
}];
}
- (SSignal *)onCompletion:(void (^)())f
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
return [self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
f();
[subscriber putCompletion];
}];
}];
}
- (SSignal *)afterCompletion:(void (^)())f {
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
return [self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
f();
}];
}];
}
- (SSignal *)onDispose:(void (^)())f
{
return [[SSignal alloc] initWithGenerator:^(SSubscriber *subscriber)
{
SDisposableSet *compositeDisposable = [[SDisposableSet alloc] init];
[compositeDisposable add:[self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}]];
[compositeDisposable add:[[SBlockDisposable alloc] initWithBlock:^
{
f();
}]];
return compositeDisposable;
}];
}
@end
@@ -0,0 +1,10 @@
#import <SSignalKit/SSignal.h>
@interface SSignal (Single)
+ (SSignal *)single:(id)next;
+ (SSignal *)fail:(id)error;
+ (SSignal *)never;
+ (SSignal *)complete;
@end
@@ -0,0 +1,41 @@
#import "SSignal+Single.h"
@implementation SSignal (Single)
+ (SSignal *)single:(id)next
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
[subscriber putNext:next];
[subscriber putCompletion];
return nil;
}];
}
+ (SSignal *)fail:(id)error
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
[subscriber putError:error];
return nil;
}];
}
+ (SSignal *)never
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (__unused SSubscriber *subscriber)
{
return nil;
}];
}
+ (SSignal *)complete
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
[subscriber putCompletion];
return nil;
}];
}
@end
@@ -0,0 +1,9 @@
#import <SSignalKit/SSignalKit.h>
@interface SSignal (Take)
- (SSignal *)take:(NSUInteger)count;
- (SSignal *)takeLast;
- (SSignal *)takeUntilReplacement:(SSignal *)replacement;
@end
@@ -0,0 +1,122 @@
#import "SSignal+Take.h"
#import "SAtomic.h"
@interface SSignal_ValueContainer : NSObject
@property (nonatomic, strong, readonly) id value;
@end
@implementation SSignal_ValueContainer
- (instancetype)initWithValue:(id)value {
self = [super init];
if (self != nil) {
_value = value;
}
return self;
}
@end
@implementation SSignal (Take)
- (SSignal *)take:(NSUInteger)count
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
{
SAtomic *counter = [[SAtomic alloc] initWithValue:@(0)];
return [self startWithNext:^(id next)
{
__block bool passthrough = false;
__block bool complete = false;
[counter modify:^id(NSNumber *currentCount)
{
NSUInteger updatedCount = [currentCount unsignedIntegerValue] + 1;
if (updatedCount <= count)
passthrough = true;
if (updatedCount == count)
complete = true;
return @(updatedCount);
}];
if (passthrough)
[subscriber putNext:next];
if (complete)
[subscriber putCompletion];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}];
}];
}
- (SSignal *)takeLast
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
{
SAtomic *last = [[SAtomic alloc] initWithValue:nil];
return [self startWithNext:^(id next)
{
[last swap:[[SSignal_ValueContainer alloc] initWithValue:next]];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
SSignal_ValueContainer *value = [last with:^id(id value) {
return value;
}];
if (value != nil)
{
[subscriber putNext:value.value];
}
[subscriber putCompletion];
}];
}];
}
- (SSignal *)takeUntilReplacement:(SSignal *)replacement {
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber) {
SDisposableSet *disposable = [[SDisposableSet alloc] init];
SMetaDisposable *selfDisposable = [[SMetaDisposable alloc] init];
SMetaDisposable *replacementDisposable = [[SMetaDisposable alloc] init];
[disposable add:selfDisposable];
[disposable add:replacementDisposable];
[disposable add:[replacement startWithNext:^(SSignal *next) {
[selfDisposable dispose];
[replacementDisposable setDisposable:[next startWithNext:^(id next) {
[subscriber putNext:next];
} error:^(id error) {
[subscriber putError:error];
} completed:^{
[subscriber putCompletion];
}]];
} error:^(id error) {
[subscriber putError:error];
} completed:^{
}]];
[selfDisposable setDisposable:[self startWithNext:^(id next) {
[subscriber putNext:next];
} error:^(id error) {
[replacementDisposable dispose];
[subscriber putError:error];
} completed:^{
[replacementDisposable dispose];
[subscriber putCompletion];
}]];
return disposable;
}];
}
@end
@@ -0,0 +1,11 @@
#import <SSignalKit/SSignal.h>
#import <SSignalKit/SQueue.h>
@interface SSignal (Timing)
- (SSignal *)delay:(NSTimeInterval)seconds onQueue:(SQueue *)queue;
- (SSignal *)timeout:(NSTimeInterval)seconds onQueue:(SQueue *)queue orSignal:(SSignal *)signal;
- (SSignal *)wait:(NSTimeInterval)seconds;
@end
@@ -0,0 +1,109 @@
#import "SSignal+Timing.h"
#import "SMetaDisposable.h"
#import "SDisposableSet.h"
#import "SBlockDisposable.h"
#import "SSignal+Dispatch.h"
#import "STimer.h"
@implementation SSignal (Timing)
- (SSignal *)delay:(NSTimeInterval)seconds onQueue:(SQueue *)queue
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
SMetaDisposable *disposable = [[SMetaDisposable alloc] init];
STimer *timer = [[STimer alloc] initWithTimeout:seconds repeat:false completion:^
{
[disposable setDisposable:[self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}]];
} queue:queue];
[timer start];
[disposable setDisposable:[[SBlockDisposable alloc] initWithBlock:^
{
[timer invalidate];
}]];
return disposable;
}];
}
- (SSignal *)timeout:(NSTimeInterval)seconds onQueue:(SQueue *)queue orSignal:(SSignal *)signal
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable> (SSubscriber *subscriber)
{
SMetaDisposable *disposable = [[SMetaDisposable alloc] init];
STimer *timer = [[STimer alloc] initWithTimeout:seconds repeat:false completion:^
{
[disposable setDisposable:[signal startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
}]];
} queue:queue];
[timer start];
[disposable setDisposable:[self startWithNext:^(id next)
{
[timer invalidate];
[subscriber putNext:next];
} error:^(id error)
{
[timer invalidate];
[subscriber putError:error];
} completed:^
{
[timer invalidate];
[subscriber putCompletion];
}]];
return disposable;
}];
}
- (SSignal *)wait:(NSTimeInterval)seconds
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
id<SDisposable> disposable = [self startWithNext:^(id next)
{
dispatch_semaphore_signal(semaphore);
[subscriber putNext:next];
} error:^(id error)
{
dispatch_semaphore_signal(semaphore);
[subscriber putError:error];
} completed:^
{
dispatch_semaphore_signal(semaphore);
[subscriber putCompletion];
}];
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(seconds * NSEC_PER_SEC)));
return disposable;
}];
}
@end
@@ -0,0 +1,18 @@
#import <SSignalKit/SSubscriber.h>
@interface SSignal : NSObject
{
@public
id<SDisposable> (^_generator)(SSubscriber *);
}
- (instancetype)initWithGenerator:(id<SDisposable> (^)(SSubscriber *))generator;
- (id<SDisposable>)startWithNext:(void (^)(id next))next error:(void (^)(id error))error completed:(void (^)())completed;
- (id<SDisposable>)startWithNext:(void (^)(id next))next;
- (id<SDisposable>)startWithNext:(void (^)(id next))next completed:(void (^)())completed;
- (SSignal *)trace:(NSString *)name;
@end
@@ -0,0 +1,107 @@
#import "SSignal.h"
#import "SBlockDisposable.h"
@interface SSubscriberDisposable : NSObject <SDisposable>
{
SSubscriber *_subscriber;
id<SDisposable> _disposable;
}
@end
@implementation SSubscriberDisposable
- (instancetype)initWithSubscriber:(SSubscriber *)subscriber disposable:(id<SDisposable>)disposable
{
self = [super init];
if (self != nil)
{
_subscriber = subscriber;
_disposable = disposable;
}
return self;
}
- (void)dispose
{
[_subscriber _markTerminatedWithoutDisposal];
[_disposable dispose];
}
@end
@interface SSignal ()
{
}
@end
@implementation SSignal
- (instancetype)initWithGenerator:(id<SDisposable> (^)(SSubscriber *))generator
{
self = [super init];
if (self != nil)
{
_generator = [generator copy];
}
return self;
}
- (id<SDisposable>)startWithNext:(void (^)(id next))next error:(void (^)(id error))error completed:(void (^)())completed traceName:(NSString *)traceName
{
STracingSubscriber *subscriber = [[STracingSubscriber alloc] initWithName:traceName next:next error:error completed:completed];
id<SDisposable> disposable = _generator(subscriber);
[subscriber _assignDisposable:disposable];
return [[SSubscriberDisposable alloc] initWithSubscriber:subscriber disposable:disposable];
}
- (id<SDisposable>)startWithNext:(void (^)(id next))next error:(void (^)(id error))error completed:(void (^)())completed
{
SSubscriber *subscriber = [[SSubscriber alloc] initWithNext:next error:error completed:completed];
id<SDisposable> disposable = _generator(subscriber);
[subscriber _assignDisposable:disposable];
return [[SSubscriberDisposable alloc] initWithSubscriber:subscriber disposable:disposable];
}
- (id<SDisposable>)startWithNext:(void (^)(id next))next
{
SSubscriber *subscriber = [[SSubscriber alloc] initWithNext:next error:nil completed:nil];
id<SDisposable> disposable = _generator(subscriber);
[subscriber _assignDisposable:disposable];
return [[SSubscriberDisposable alloc] initWithSubscriber:subscriber disposable:disposable];
}
- (id<SDisposable>)startWithNext:(void (^)(id next))next completed:(void (^)())completed
{
SSubscriber *subscriber = [[SSubscriber alloc] initWithNext:next error:nil completed:completed];
id<SDisposable> disposable = _generator(subscriber);
[subscriber _assignDisposable:disposable];
return [[SSubscriberDisposable alloc] initWithSubscriber:subscriber disposable:disposable];
}
- (SSignal *)trace:(NSString *)name
{
#ifdef DEBUG
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
{
NSString *traceName = [[NSString alloc] initWithFormat:@"%@#0x%x", name, (int)random()];
NSLog(@"trace(%@ start)", traceName);
return [self startWithNext:^(id next)
{
[subscriber putNext:next];
} error:^(id error)
{
[subscriber putError:error];
} completed:^
{
[subscriber putCompletion];
} traceName:traceName];
}];
#else
return self;
#endif
}
@end
@@ -0,0 +1,45 @@
//
// SSignalKit.h
// SSignalKit
//
// Created by Peter on 31/01/15.
// Copyright (c) 2015 Telegram. All rights reserved.
//
#if __IPHONE_OS_VERSION_MIN_REQUIRED
#import <UIKit/UIKit.h>
#else
#import <Foundation/Foundation.h>
#endif
//! Project version number for SSignalKit.
FOUNDATION_EXPORT double SSignalKitVersionNumber;
//! Project version string for SSignalKit.
FOUNDATION_EXPORT const unsigned char SSignalKitVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <SSignalKit/PublicHeader.h>
#import <SSignalKit/SAtomic.h>
#import <SSignalKit/SBag.h>
#import <SSignalKit/SSignal.h>
#import <SSignalKit/SSubscriber.h>
#import <SSignalKit/SDisposable.h>
#import <SSignalKit/SDisposableSet.h>
#import <SSignalKit/SBlockDisposable.h>
#import <SSignalKit/SMetaDisposable.h>
#import <SSignalKit/SSignal+Single.h>
#import <SSignalKit/SSignal+Mapping.h>
#import <SSignalKit/SSignal+Multicast.h>
#import <SSignalKit/SSignal+Meta.h>
#import <SSignalKit/SSignal+Accumulate.h>
#import <SSignalKit/SSignal+Dispatch.h>
#import <SSignalKit/SSignal+Catch.h>
#import <SSignalKit/SSignal+SideEffects.h>
#import <SSignalKit/SSignal+Combine.h>
#import <SSignalKit/SSignal+Timing.h>
#import <SSignalKit/SSignal+Take.h>
#import <SSignalKit/SSignal+Pipe.h>
#import <SSignalKit/SMulticastSignalManager.h>
#import <SSignalKit/STimer.h>
#import <SSignalKit/SVariable.h>
@@ -0,0 +1,22 @@
#import <SSignalKit/SDisposable.h>
@interface SSubscriber : NSObject <SDisposable>
{
}
- (instancetype)initWithNext:(void (^)(id))next error:(void (^)(id))error completed:(void (^)())completed;
- (void)_assignDisposable:(id<SDisposable>)disposable;
- (void)_markTerminatedWithoutDisposal;
- (void)putNext:(id)next;
- (void)putError:(id)error;
- (void)putCompletion;
@end
@interface STracingSubscriber : SSubscriber
- (instancetype)initWithName:(NSString *)name next:(void (^)(id))next error:(void (^)(id))error completed:(void (^)())completed;
@end
@@ -0,0 +1,276 @@
#import "SSubscriber.h"
#import <libkern/OSAtomic.h>
@interface SSubscriberBlocks : NSObject {
@public
void (^_next)(id);
void (^_error)(id);
void (^_completed)();
}
@end
@implementation SSubscriberBlocks
- (instancetype)initWithNext:(void (^)(id))next error:(void (^)(id))error completed:(void (^)())completed {
self = [super init];
if (self != nil) {
_next = [next copy];
_error = [error copy];
_completed = [completed copy];
}
return self;
}
@end
@interface SSubscriber ()
{
@protected
OSSpinLock _lock;
bool _terminated;
id<SDisposable> _disposable;
SSubscriberBlocks *_blocks;
}
@end
@implementation SSubscriber
- (instancetype)initWithNext:(void (^)(id))next error:(void (^)(id))error completed:(void (^)())completed
{
self = [super init];
if (self != nil)
{
_blocks = [[SSubscriberBlocks alloc] initWithNext:next error:error completed:completed];
}
return self;
}
- (void)_assignDisposable:(id<SDisposable>)disposable
{
bool dispose = false;
OSSpinLockLock(&_lock);
if (_terminated) {
dispose = true;
} else {
_disposable = disposable;
}
OSSpinLockUnlock(&_lock);
if (dispose) {
[disposable dispose];
}
}
- (void)_markTerminatedWithoutDisposal
{
OSSpinLockLock(&_lock);
SSubscriberBlocks *blocks = nil;
if (!_terminated)
{
blocks = _blocks;
_blocks = nil;
_terminated = true;
}
OSSpinLockUnlock(&_lock);
if (blocks) {
blocks = nil;
}
}
- (void)putNext:(id)next
{
SSubscriberBlocks *blocks = nil;
OSSpinLockLock(&_lock);
if (!_terminated) {
blocks = _blocks;
}
OSSpinLockUnlock(&_lock);
if (blocks && blocks->_next) {
blocks->_next(next);
}
}
- (void)putError:(id)error
{
bool shouldDispose = false;
SSubscriberBlocks *blocks = nil;
OSSpinLockLock(&_lock);
if (!_terminated)
{
blocks = _blocks;
_blocks = nil;
shouldDispose = true;
_terminated = true;
}
OSSpinLockUnlock(&_lock);
if (blocks && blocks->_error) {
blocks->_error(error);
}
if (shouldDispose)
[self->_disposable dispose];
}
- (void)putCompletion
{
bool shouldDispose = false;
SSubscriberBlocks *blocks = nil;
OSSpinLockLock(&_lock);
if (!_terminated)
{
blocks = _blocks;
_blocks = nil;
shouldDispose = true;
_terminated = true;
}
OSSpinLockUnlock(&_lock);
if (blocks && blocks->_completed)
blocks->_completed();
if (shouldDispose)
[self->_disposable dispose];
}
- (void)dispose
{
[self->_disposable dispose];
}
@end
@interface STracingSubscriber ()
{
NSString *_name;
}
@end
@implementation STracingSubscriber
- (instancetype)initWithName:(NSString *)name next:(void (^)(id))next error:(void (^)(id))error completed:(void (^)())completed
{
self = [super initWithNext:next error:error completed:completed];
if (self != nil)
{
_name = name;
}
return self;
}
/*- (void)_assignDisposable:(id<SDisposable>)disposable
{
if (_terminated)
[disposable dispose];
else
_disposable = disposable;
}
- (void)_markTerminatedWithoutDisposal
{
OSSpinLockLock(&_lock);
if (!_terminated)
{
NSLog(@"trace(%@ terminated)", _name);
_terminated = true;
_next = nil;
_error = nil;
_completed = nil;
}
OSSpinLockUnlock(&_lock);
}
- (void)putNext:(id)next
{
void (^fnext)(id) = nil;
OSSpinLockLock(&_lock);
if (!_terminated)
fnext = self->_next;
OSSpinLockUnlock(&_lock);
if (fnext)
{
NSLog(@"trace(%@ next: %@)", _name, next);
fnext(next);
}
else
NSLog(@"trace(%@ next: %@, not accepted)", _name, next);
}
- (void)putError:(id)error
{
bool shouldDispose = false;
void (^ferror)(id) = nil;
OSSpinLockLock(&_lock);
if (!_terminated)
{
ferror = self->_error;
shouldDispose = true;
self->_next = nil;
self->_error = nil;
self->_completed = nil;
_terminated = true;
}
OSSpinLockUnlock(&_lock);
if (ferror)
{
NSLog(@"trace(%@ error: %@)", _name, error);
ferror(error);
}
else
NSLog(@"trace(%@ error: %@, not accepted)", _name, error);
if (shouldDispose)
[self->_disposable dispose];
}
- (void)putCompletion
{
bool shouldDispose = false;
void (^completed)() = nil;
OSSpinLockLock(&_lock);
if (!_terminated)
{
completed = self->_completed;
shouldDispose = true;
self->_next = nil;
self->_error = nil;
self->_completed = nil;
_terminated = true;
}
OSSpinLockUnlock(&_lock);
if (completed)
{
NSLog(@"trace(%@ completed)", _name);
completed();
}
else
NSLog(@"trace(%@ completed, not accepted)", _name);
if (shouldDispose)
[self->_disposable dispose];
}
- (void)dispose
{
NSLog(@"trace(%@ dispose)", _name);
[self->_disposable dispose];
}*/
@end
@@ -0,0 +1,15 @@
#import <Foundation/Foundation.h>
#import <SSignalKit/SThreadPoolTask.h>
#import <SSignalKit/SThreadPoolQueue.h>
@interface SThreadPool : NSObject
- (instancetype)initWithThreadCount:(NSUInteger)threadCount threadPriority:(double)threadPriority;
- (void)addTask:(SThreadPoolTask *)task;
- (SThreadPoolQueue *)nextQueue;
- (void)_workOnQueue:(SThreadPoolQueue *)queue block:(void (^)())block;
@end
@@ -0,0 +1,128 @@
#import "SThreadPool.h"
#import <libkern/OSAtomic.h>
#import <pthread.h>
#import "SQueue.h"
@interface SThreadPool ()
{
SQueue *_managementQueue;
NSMutableArray *_threads;
NSMutableArray *_queues;
NSMutableArray *_takenQueues;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
}
@end
@implementation SThreadPool
+ (void)threadEntryPoint:(SThreadPool *)threadPool
{
SThreadPoolQueue *queue = nil;
while (true)
{
SThreadPoolTask *task = nil;
pthread_mutex_lock(&threadPool->_mutex);
if (queue != nil)
{
[threadPool->_takenQueues removeObject:queue];
if ([queue _hasTasks])
[threadPool->_queues addObject:queue];
}
while (true)
{
while (threadPool->_queues.count == 0)
pthread_cond_wait(&threadPool->_cond, &threadPool->_mutex);
queue = threadPool->_queues.firstObject;
task = [queue _popFirstTask];
if (queue != nil)
{
[threadPool->_takenQueues addObject:queue];
[threadPool->_queues removeObjectAtIndex:0];
break;
}
}
pthread_mutex_unlock(&threadPool->_mutex);
@autoreleasepool
{
[task execute];
}
}
}
- (instancetype)init
{
return [self initWithThreadCount:2 threadPriority:0.5];
}
- (instancetype)initWithThreadCount:(NSUInteger)threadCount threadPriority:(double)threadPriority
{
self = [super init];
if (self != nil)
{
pthread_mutex_init(&_mutex, 0);
pthread_cond_init(&_cond, 0);
_managementQueue = [[SQueue alloc] init];
[_managementQueue dispatch:^
{
_threads = [[NSMutableArray alloc] init];
_queues = [[NSMutableArray alloc] init];
_takenQueues = [[NSMutableArray alloc] init];
for (NSUInteger i = 0; i < threadCount; i++)
{
NSThread *thread = [[NSThread alloc] initWithTarget:[SThreadPool class] selector:@selector(threadEntryPoint:) object:self];
thread.name = [[NSString alloc] initWithFormat:@"SThreadPool-%p-%d", self, (int)i];
[thread setThreadPriority:threadPriority];
[_threads addObject:thread];
[thread start];
}
}];
}
return self;
}
- (void)dealloc
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
- (void)addTask:(SThreadPoolTask *)task
{
SThreadPoolQueue *tempQueue = [self nextQueue];
[tempQueue addTask:task];
}
- (SThreadPoolQueue *)nextQueue
{
return [[SThreadPoolQueue alloc] initWithThreadPool:self];
}
- (void)_workOnQueue:(SThreadPoolQueue *)queue block:(void (^)())block
{
[_managementQueue dispatch:^
{
pthread_mutex_lock(&_mutex);
block();
if (![_queues containsObject:queue] && ![_takenQueues containsObject:queue])
[_queues addObject:queue];
pthread_cond_broadcast(&_cond);
pthread_mutex_unlock(&_mutex);
}];
}
@end
@@ -0,0 +1,13 @@
#import <Foundation/Foundation.h>
@class SThreadPool;
@class SThreadPoolTask;
@interface SThreadPoolQueue : NSObject
- (instancetype)initWithThreadPool:(SThreadPool *)threadPool;
- (void)addTask:(SThreadPoolTask *)task;
- (SThreadPoolTask *)_popFirstTask;
- (bool)_hasTasks;
@end
@@ -0,0 +1,51 @@
#import "SThreadPoolQueue.h"
#import "SThreadPool.h"
@interface SThreadPoolQueue ()
{
__weak SThreadPool *_threadPool;
NSMutableArray *_tasks;
}
@end
@implementation SThreadPoolQueue
- (instancetype)initWithThreadPool:(SThreadPool *)threadPool
{
self = [super init];
if (self != nil)
{
_threadPool = threadPool;
_tasks = [[NSMutableArray alloc] init];
}
return self;
}
- (void)addTask:(SThreadPoolTask *)task
{
SThreadPool *threadPool = _threadPool;
[threadPool _workOnQueue:self block:^
{
[_tasks addObject:task];
}];
}
- (SThreadPoolTask *)_popFirstTask
{
if (_tasks.count != 0)
{
SThreadPoolTask *task = _tasks[0];
[_tasks removeObjectAtIndex:0];
return task;
}
return nil;
}
- (bool)_hasTasks
{
return _tasks.count != 0;
}
@end
@@ -0,0 +1,9 @@
#import <Foundation/Foundation.h>
@interface SThreadPoolTask : NSObject
- (instancetype)initWithBlock:(void (^)(bool (^)()))block;
- (void)execute;
- (void)cancel;
@end
@@ -0,0 +1,53 @@
#import "SThreadPoolTask.h"
@interface SThreadPoolTaskState : NSObject
{
@public
bool _cancelled;
}
@end
@implementation SThreadPoolTaskState
@end
@interface SThreadPoolTask ()
{
void (^_block)(bool (^)());
SThreadPoolTaskState *_state;
}
@end
@implementation SThreadPoolTask
- (instancetype)initWithBlock:(void (^)(bool (^)()))block
{
self = [super init];
if (self != nil)
{
_block = [block copy];
_state = [[SThreadPoolTaskState alloc] init];
}
return self;
}
- (void)execute
{
if (_state->_cancelled)
return;
SThreadPoolTaskState *state = _state;
_block(^bool
{
return state->_cancelled;
});
}
- (void)cancel
{
_state->_cancelled = true;
}
@end
@@ -0,0 +1,14 @@
#import <Foundation/Foundation.h>
@class SQueue;
@interface STimer : NSObject
- (id)initWithTimeout:(NSTimeInterval)timeout repeat:(bool)repeat completion:(dispatch_block_t)completion queue:(SQueue *)queue;
- (id)initWithTimeout:(NSTimeInterval)timeout repeat:(bool)repeat completion:(dispatch_block_t)completion nativeQueue:(dispatch_queue_t)nativeQueue;
- (void)start;
- (void)invalidate;
- (void)fireAndInvalidate;
@end
@@ -0,0 +1,83 @@
#import "STimer.h"
#import "SQueue.h"
@interface STimer ()
{
dispatch_source_t _timer;
NSTimeInterval _timeout;
NSTimeInterval _timeoutDate;
bool _repeat;
dispatch_block_t _completion;
dispatch_queue_t _nativeQueue;
}
@end
@implementation STimer
- (id)initWithTimeout:(NSTimeInterval)timeout repeat:(bool)repeat completion:(dispatch_block_t)completion queue:(SQueue *)queue {
return [self initWithTimeout:timeout repeat:repeat completion:completion nativeQueue:queue._dispatch_queue];
}
- (id)initWithTimeout:(NSTimeInterval)timeout repeat:(bool)repeat completion:(dispatch_block_t)completion nativeQueue:(dispatch_queue_t)nativeQueue
{
self = [super init];
if (self != nil)
{
_timeoutDate = INT_MAX;
_timeout = timeout;
_repeat = repeat;
_completion = [completion copy];
_nativeQueue = nativeQueue;
}
return self;
}
- (void)dealloc
{
if (_timer != nil)
{
dispatch_source_cancel(_timer);
_timer = nil;
}
}
- (void)start
{
_timeoutDate = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970 + _timeout;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _nativeQueue);
dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_timeout * NSEC_PER_SEC)), _repeat ? (int64_t)(_timeout * NSEC_PER_SEC) : DISPATCH_TIME_FOREVER, 0);
dispatch_source_set_event_handler(_timer, ^
{
if (_completion)
_completion();
if (!_repeat)
[self invalidate];
});
dispatch_resume(_timer);
}
- (void)fireAndInvalidate
{
if (_completion)
_completion();
[self invalidate];
}
- (void)invalidate
{
_timeoutDate = 0;
if (_timer != nil)
{
dispatch_source_cancel(_timer);
_timer = nil;
}
}
@end
@@ -0,0 +1,12 @@
#import <Foundation/Foundation.h>
@class SSignal;
@interface SVariable : NSObject
- (instancetype)init;
- (void)set:(SSignal *)signal;
- (SSignal *)signal;
@end
@@ -0,0 +1,93 @@
#import "SVariable.h"
#import <libkern/OSAtomic.h>
#import "SSignal.h"
#import "SBag.h"
#import "SBlockDisposable.h"
#import "SMetaDisposable.h"
@interface SVariable ()
{
OSSpinLock _lock;
id _value;
bool _hasValue;
SBag *_subscribers;
SMetaDisposable *_disposable;
}
@end
@implementation SVariable
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_subscribers = [[SBag alloc] init];
_disposable = [[SMetaDisposable alloc] init];
}
return self;
}
- (void)dealloc
{
[_disposable dispose];
}
- (SSignal *)signal
{
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
{
OSSpinLockLock(&self->_lock);
id currentValue = _value;
bool hasValue = _hasValue;
NSInteger index = [self->_subscribers addItem:[^(id value)
{
[subscriber putNext:value];
} copy]];
OSSpinLockUnlock(&self->_lock);
if (hasValue)
{
[subscriber putNext:currentValue];
}
return [[SBlockDisposable alloc] initWithBlock:^
{
OSSpinLockLock(&self->_lock);
[self->_subscribers removeItem:index];
OSSpinLockUnlock(&self->_lock);
}];
}];
}
- (void)set:(SSignal *)signal
{
OSSpinLockLock(&_lock);
_hasValue = false;
OSSpinLockUnlock(&_lock);
__weak SVariable *weakSelf = self;
[_disposable setDisposable:[signal startWithNext:^(id next)
{
__strong SVariable *strongSelf = weakSelf;
if (strongSelf != nil)
{
NSArray *subscribers = nil;
OSSpinLockLock(&strongSelf->_lock);
strongSelf->_value = next;
strongSelf->_hasValue = true;
subscribers = [strongSelf->_subscribers copyItems];
OSSpinLockUnlock(&strongSelf->_lock);
for (void (^subscriber)(id) in subscribers)
{
subscriber(next);
}
}
}]];
}
@end
@@ -0,0 +1,7 @@
#import "TGInterfaceController.h"
@interface TGAudioMicAlertController : TGInterfaceController
@property (nonatomic, weak) IBOutlet WKInterfaceLabel *alertLabel;
@end
@@ -0,0 +1,18 @@
#import "TGAudioMicAlertController.h"
#import "TGWatchCommon.h"
NSString *const TGAudioMicAlertControllerIdentifier = @"TGAudioMicAlertController";
@implementation TGAudioMicAlertController
- (void)configureWithContext:(id<TGInterfaceContext>)context
{
self.alertLabel.text = TGLocalized(@"Watch.Microphone.Access");
}
+ (NSString *)identifier
{
return TGAudioMicAlertControllerIdentifier;
}
@end
@@ -0,0 +1,17 @@
#import <WatchKit/WatchKit.h>
@class TGBridgeContext;
@class TGBridgeUser;
@class TGBridgeChat;
@interface TGAvatarViewModel : NSObject
@property (nonatomic, weak) WKInterfaceGroup *group;
@property (nonatomic, weak) WKInterfaceLabel *label;
- (void)updateWithUser:(TGBridgeUser *)user context:(TGBridgeContext *)context isVisible:(bool (^)(void))isVisible;
- (void)updateWithChat:(TGBridgeChat *)chat isVisible:(bool (^)(void))isVisible;
- (void)updateIfNeeded;
@end
@@ -0,0 +1,107 @@
#import "TGAvatarViewModel.h"
#import <WatchCommonWatch/WatchCommonWatch.h>
#import "TGStringUtils.h"
#import "TGWatchColor.h"
#import "WKInterfaceGroup+Signals.h"
#import "TGBridgeMediaSignals.h"
@interface TGAvatarViewModel ()
{
TGBridgeUser *_currentUser;
TGBridgeChat *_currentChat;
}
@end
@implementation TGAvatarViewModel
- (void)updateWithUser:(TGBridgeUser *)user context:(TGBridgeContext *)context isVisible:(bool (^)(void))isVisible
{
TGBridgeUser *oldUser = _currentUser;
_currentUser = user;
if (_currentUser.identifier == context.userId)
{
self.label.hidden = true;
self.group.backgroundColor = [UIColor hexColor:0x222223];
[self.group setBackgroundImageSignal:[SSignal single:@"SavedMessagesAvatar"] isVisible:isVisible];
}
else if (_currentUser.photoSmall.length > 0)
{
if (![_currentUser.photoSmall isEqualToString:oldUser.photoSmall])
{
self.label.hidden = true;
self.group.backgroundColor = [UIColor hexColor:0x222223];
__block bool completed = false;
__weak TGAvatarViewModel *weakSelf = self;
[self.group setBackgroundImageSignal:[[[TGBridgeMediaSignals avatarWithPeerId:_currentUser.identifier url:_currentUser.photoSmall type:TGBridgeMediaAvatarTypeSmall] onNext:^(id next)
{
completed = true;
}] onDispose:^
{
__strong TGAvatarViewModel *strongSelf = weakSelf;
if (strongSelf != nil && !completed)
strongSelf->_currentUser = nil;
}] isVisible:isVisible];
}
}
else
{
if (oldUser.photoSmall.length > 0 || ![[oldUser displayName] isEqualToString:[_currentUser displayName]])
{
self.label.hidden = false;
self.label.text = [TGStringUtils initialsForFirstName:_currentUser.firstName lastName:_currentUser.lastName single:true];
self.group.backgroundColor = [TGColor colorForUserId:(int32_t)user.identifier myUserId:context.userId];
}
}
}
- (void)updateWithChat:(TGBridgeChat *)chat isVisible:(bool (^)(void))isVisible
{
TGBridgeChat *oldChat = _currentChat;
_currentChat = chat;
if (_currentChat.groupPhotoSmall.length > 0)
{
if (![_currentChat.groupPhotoSmall isEqualToString:oldChat.groupPhotoSmall])
{
self.label.hidden = true;
self.group.backgroundColor = [UIColor hexColor:0x222223];
__block bool completed = false;
__weak TGAvatarViewModel *weakSelf = self;
[self.group setBackgroundImageSignal:[[[TGBridgeMediaSignals avatarWithPeerId:_currentChat.identifier url:_currentChat.groupPhotoSmall type:TGBridgeMediaAvatarTypeSmall] onNext:^(id next)
{
completed = true;
}] onDispose:^
{
__strong TGAvatarViewModel *strongSelf = weakSelf;
if (strongSelf != nil && !completed)
strongSelf->_currentChat = nil;
}] isVisible:isVisible];
}
}
else
{
if (oldChat.groupPhotoSmall.length > 0 || ![[oldChat groupTitle] isEqualToString:[_currentChat groupTitle]])
{
self.label.hidden = false;
self.label.text = [TGStringUtils initialForGroupName:_currentChat.groupTitle];
self.group.backgroundColor = [TGColor colorForGroupId:_currentChat.identifier];
}
}
}
- (void)updateIfNeeded
{
[self.group updateIfNeeded];
}
@end
@@ -0,0 +1,22 @@
#import "TGInterfaceController.h"
@class SSignal;
@class TGBridgeContext;
@interface TGBotCommandControllerContext : NSObject <TGInterfaceContext>
@property (nonatomic, strong) TGBridgeContext *context;
@property (nonatomic, strong) SSignal *commandListSignal;
@property (nonatomic, copy) void (^completionBlock)(NSString *command);
@end
@interface TGBotCommandController : TGInterfaceController
@property (nonatomic, weak) IBOutlet WKInterfaceTable *table;
@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator;
@end
extern NSString *const TGBotCommandUserKey;
extern NSString *const TGBotCommandListKey;
@@ -0,0 +1,149 @@
#import "TGBotCommandController.h"
#import <WatchCommonWatch/WatchCommonWatch.h>
#import "TGWatchCommon.h"
#import <SSignalKit/SSignalKit.h>
#import "WKInterfaceTable+TGDataDrivenTable.h"
#import "TGUserRowController.h"
NSString *const TGBotCommandControllerIdentifier = @"TGBotCommandController";
NSString *const TGBotCommandKey = @"command";
NSString *const TGBotCommandUserKey = @"user";
NSString *const TGBotCommandListKey = @"list";
@implementation TGBotCommandControllerContext
@end
@interface TGBotCommandController () <TGTableDataSource>
{
SMetaDisposable *_commandDisposable;
NSArray *_commandList;
TGBotCommandControllerContext *_context;
}
@end
@implementation TGBotCommandController
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_commandDisposable = [[SMetaDisposable alloc] init];
[self.table _setInitialHidden:true];
self.table.tableDataSource = self;
}
return self;
}
- (void)dealloc
{
[_commandDisposable dispose];
}
- (void)configureWithContext:(TGBotCommandControllerContext *)context
{
_context = context;
__weak TGBotCommandController *weakSelf = self;
[_commandDisposable setDisposable:[[context.commandListSignal deliverOn:[SQueue mainQueue]] startWithNext:^(NSArray *next)
{
__strong TGBotCommandController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_commandList = next;
[strongSelf performInterfaceUpdate:^(bool animated)
{
__strong TGBotCommandController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf.activityIndicator.hidden = true;
[strongSelf.table reloadData];
strongSelf.table.hidden = false;
}];
}]];
}
#pragma mark -
- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(TGIndexPath *)indexPath
{
return [TGUserRowController class];
}
- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section
{
return [self numberOfAvailableCommands];
}
- (void)table:(WKInterfaceTable *)table updateRowController:(TGUserRowController *)controller forIndexPath:(TGIndexPath *)indexPath
{
NSDictionary *dict = [self dictionaryForRow:indexPath.row];
TGBridgeBotCommandInfo *commandInfo = dict[TGBotCommandKey];
TGBridgeUser *botUser = dict[TGBotCommandUserKey];
[controller updateWithBotCommandInfo:commandInfo botUser:botUser context:_context.context];
}
- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath
{
[self dismissController];
NSDictionary *dict = [self dictionaryForRow:indexPath.row];
TGBridgeBotCommandInfo *commandInfo = dict[TGBotCommandKey];
TGBridgeUser *botUser = dict[TGBotCommandUserKey];
bool isSingleBot = (_commandList.count == 1);
NSString *mention = isSingleBot ? @"" : [NSString stringWithFormat:@"@%@", botUser.userName];
NSString *command = [NSString stringWithFormat:@"/%@%@", commandInfo.command, mention];
if (_context.completionBlock != nil)
_context.completionBlock(command);
}
- (NSDictionary *)dictionaryForRow:(NSUInteger)row
{
NSRange currentRange = NSMakeRange(0, 0);
for (NSDictionary *dict in _commandList)
{
NSArray *commandList = dict[TGBotCommandListKey];
currentRange = NSMakeRange(currentRange.location + currentRange.length, commandList.count);
NSInteger transposedRow = row - currentRange.location;
if (transposedRow >= 0 && transposedRow < currentRange.length)
return @{ TGBotCommandUserKey: dict[TGBotCommandUserKey], TGBotCommandKey: commandList[transposedRow]};
}
return nil;
}
- (NSUInteger)numberOfAvailableCommands
{
NSUInteger count = 0;
for (NSDictionary *dict in _commandList)
{
id commandList = dict[TGBotCommandListKey];
if ([commandList isKindOfClass:[NSArray class]])
count += [commandList count];
}
return count;
}
#pragma mark -
+ (NSString *)identifier
{
return TGBotCommandControllerIdentifier;
}
@end
@@ -0,0 +1,11 @@
#import "WKInterfaceTable+TGDataDrivenTable.h"
@class TGBridgeBotReplyMarkupButton;
@interface TGBotKeyboardButtonController : TGTableRowController
@property (nonatomic, weak) IBOutlet WKInterfaceLabel *textLabel;
- (void)updateWithButton:(TGBridgeBotReplyMarkupButton *)button;
@end
@@ -0,0 +1,18 @@
#import "TGBotKeyboardButtonController.h"
#import "TGBridgeBotReplyMarkup.h"
NSString *const TGBotKeyboardButtonRowIdentifier = @"TGBotKeyboardButton";
@implementation TGBotKeyboardButtonController
- (void)updateWithButton:(TGBridgeBotReplyMarkupButton *)button
{
self.textLabel.text = button.text;
}
+ (NSString *)identifier
{
return TGBotKeyboardButtonRowIdentifier;
}
@end
@@ -0,0 +1,17 @@
#import "TGInterfaceController.h"
@class TGBridgeBotReplyMarkup;
@interface TGBotKeyboardControllerContext : NSObject <TGInterfaceContext>
@property (nonatomic, strong) TGBridgeBotReplyMarkup *replyMarkup;
@property (nonatomic, copy) void (^completionBlock)(NSString *command);
@end
@interface TGBotKeyboardController : TGInterfaceController
@property (nonatomic, weak) IBOutlet WKInterfaceTable *table;
@end
@@ -0,0 +1,106 @@
#import "TGBotKeyboardController.h"
#import "TGWatchCommon.h"
#import "TGBridgeBotReplyMarkup.h"
#import "WKInterfaceTable+TGDataDrivenTable.h"
#import "TGBotKeyboardButtonController.h"
NSString *const TGBotKeyboardControllerIdentifier = @"TGBotKeyboardController";
@implementation TGBotKeyboardControllerContext
@end
@interface TGBotKeyboardController () <TGTableDataSource>
{
TGBotKeyboardControllerContext *_context;
TGBridgeBotReplyMarkup *_replyMarkup;
}
@end
@implementation TGBotKeyboardController
- (instancetype)init
{
self = [super init];
if (self != nil)
{
[self.table _setInitialHidden:true];
self.table.tableDataSource = self;
}
return self;
}
- (void)configureWithContext:(TGBotKeyboardControllerContext *)context
{
_context = context;
_replyMarkup = context.replyMarkup;
[self.table reloadData];
}
#pragma mark -
- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section
{
return [self numberOfAvailableButtons];
}
- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(TGIndexPath *)indexPath
{
return [TGBotKeyboardButtonController class];
}
- (void)table:(WKInterfaceTable *)table updateRowController:(TGBotKeyboardButtonController *)controller forIndexPath:(TGIndexPath *)indexPath
{
TGBridgeBotReplyMarkupButton *button = [self buttonForRow:indexPath.row];
[controller updateWithButton:button];
}
- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath
{
[self dismissController];
TGBridgeBotReplyMarkupButton *button = [self buttonForRow:indexPath.row];
if (_context.completionBlock != nil)
_context.completionBlock(button.text);
}
#pragma mark -
- (TGBridgeBotReplyMarkupButton *)buttonForRow:(NSUInteger)row
{
NSRange currentRange = NSMakeRange(0, 0);
for (TGBridgeBotReplyMarkupRow *markupRow in _replyMarkup.rows)
{
NSArray *buttons = markupRow.buttons;
currentRange = NSMakeRange(currentRange.location + currentRange.length, buttons.count);
NSInteger transposedRow = row - currentRange.location;
if (transposedRow >= 0 && transposedRow < currentRange.length)
return buttons[transposedRow];
}
return nil;
}
- (NSUInteger)numberOfAvailableButtons
{
NSUInteger count = 0;
for (TGBridgeBotReplyMarkupRow *row in _replyMarkup.rows)
count += row.buttons.count;
return count;
}
#pragma mark -
+ (NSString *)identifier
{
return TGBotKeyboardControllerIdentifier;
}
@end
@@ -0,0 +1,19 @@
#import <Foundation/Foundation.h>
@class TGBridgeUser;
@class TGBridgeBotInfo;
@interface TGBridgeUserCache : NSObject
- (TGBridgeUser *)userWithId:(int64_t)userId;
- (NSDictionary *)usersWithIds:(NSArray<NSNumber *> *)indexSet;
- (void)storeUser:(TGBridgeUser *)user;
- (void)storeUsers:(NSArray *)users;
- (NSArray *)applyUserChanges:(NSArray *)userChanges;
- (TGBridgeBotInfo *)botInfoForUserId:(int32_t)userId;
- (void)storeBotInfo:(TGBridgeBotInfo *)botInfo forUserId:(int32_t)userId;
+ (instancetype)instance;
@end
@@ -0,0 +1,173 @@
#import "TGBridgeUserCache.h"
#import <WatchCommonWatch/WatchCommonWatch.h>
#import "TGFileCache.h"
#import <libkern/OSAtomic.h>
@interface TGBridgeUserCache ()
{
NSMutableDictionary *_userByUid;
OSSpinLock _userByUidLock;
NSMutableDictionary *_botInfoByUid;
OSSpinLock _botInfoByUidLock;
TGFileCache *_fileCache;
}
@end
@implementation TGBridgeUserCache
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_userByUid = [[NSMutableDictionary alloc] init];
_botInfoByUid = [[NSMutableDictionary alloc] init];
_fileCache = [[TGFileCache alloc] initWithName:@"users" useMemoryCache:false];
}
return self;
}
- (TGBridgeUser *)userWithId:(int64_t)userId
{
__block TGBridgeUser *user = nil;
OSSpinLockLock(&_userByUidLock);
user = _userByUid[@(userId)];
OSSpinLockUnlock(&_userByUidLock);
return user;
}
- (NSDictionary *)usersWithIds:(NSArray<NSNumber *> *)indexSet
{
NSMutableDictionary *users = [[NSMutableDictionary alloc] init];
NSMutableSet<NSNumber *> *neededUsers = [indexSet mutableCopy];
NSMutableSet<NSNumber *> *foundUsers = [[NSMutableSet alloc] init];
OSSpinLockLock(&_userByUidLock);
for (NSNumber *nId in neededUsers) {
int64_t index = [nId longLongValue];
TGBridgeUser *user = _userByUid[@(index)];
if (user != nil)
{
users[@(index)] = user;
[foundUsers addObject:@(index)];
}
}
OSSpinLockUnlock(&_userByUidLock);
for (NSNumber *nId in foundUsers) {
[neededUsers removeObject:nId];
}
return users;
}
- (void)storeUser:(TGBridgeUser *)user
{
if (user == nil)
return;
[self storeUsers:@[ user ]];
}
- (void)storeUsers:(NSArray *)users
{
OSSpinLockLock(&_userByUidLock);
for (id peer in users)
{
if ([peer isKindOfClass:[TGBridgeUser class]])
_userByUid[@(((TGBridgeUser *)peer).identifier)] = peer;
}
OSSpinLockUnlock(&_userByUidLock);
}
- (NSArray *)applyUserChanges:(NSArray *)userChanges
{
NSMutableArray *missedUserIds = [[NSMutableArray alloc] init];
NSMutableArray *updatedUsers = [[NSMutableArray alloc] init];
for (TGBridgeUserChange *change in userChanges)
{
TGBridgeUser *user = [self userWithId:change.userIdentifier];
if (user != nil)
{
TGBridgeUser *updatedUser = [user userByApplyingChange:change];
[updatedUsers addObject:updatedUser];
}
else
{
[missedUserIds addObject:@(change.userIdentifier)];
}
}
[self storeUsers:updatedUsers];
if (missedUserIds.count == 0)
return nil;
return missedUserIds;
}
- (TGBridgeBotInfo *)botInfoForUserId:(int32_t)userId
{
__block TGBridgeBotInfo *botInfo = nil;
OSSpinLockLock(&_botInfoByUidLock);
botInfo = _botInfoByUid[@(userId)];
OSSpinLockUnlock(&_botInfoByUidLock);
if (botInfo == nil)
{
[_fileCache fetchDataForKey:[NSString stringWithFormat:@"botInfo-%d", userId] synchronous:true unserializeBlock:^id(NSData *data)
{
id object = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if ([object isKindOfClass:[TGBridgeBotInfo class]])
return object;
return nil;
} completion:^(TGBridgeBotInfo *result)
{
if (result != nil)
{
botInfo = result;
OSSpinLockLock(&_botInfoByUidLock);
_botInfoByUid[@(userId)] = botInfo;
OSSpinLockUnlock(&_botInfoByUidLock);
}
}];
}
return botInfo;
}
- (void)storeBotInfo:(TGBridgeBotInfo *)botInfo forUserId:(int32_t)userId
{
OSSpinLockLock(&_botInfoByUidLock);
_botInfoByUid[@(userId)] = botInfo;
[_fileCache cacheData:botInfo key:[NSString stringWithFormat:@"botInfo-%d", userId] synchronous:true serializeBlock:^NSData *(NSObject<NSCoding> *object)
{
return [NSKeyedArchiver archivedDataWithRootObject:object];
} completion:nil];
OSSpinLockUnlock(&_botInfoByUidLock);
}
+ (instancetype)instance
{
static dispatch_once_t onceToken;
static TGBridgeUserCache *userCache;
dispatch_once(&onceToken, ^
{
userCache = [[TGBridgeUserCache alloc] init];
});
return userCache;
}
@end
+8
View File
@@ -0,0 +1,8 @@
#import "WKInterfaceTable+TGDataDrivenTable.h"
@interface TGChatInfo : NSObject <TGTableItem>
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSString *text;
@end
+10
View File
@@ -0,0 +1,10 @@
#import "TGChatInfo.h"
@implementation TGChatInfo
- (NSString *)uniqueIdentifier
{
return @"chatInfo";
}
@end
@@ -0,0 +1,10 @@
#import "WKInterfaceTable+TGDataDrivenTable.h"
@interface TGChatTimestamp : NSObject <TGTableItem>
@property (nonatomic, readonly) NSTimeInterval date;
@property (nonatomic, readonly) NSString *string;
- (instancetype)initWithDate:(NSTimeInterval)date string:(NSString *)string;
@end
@@ -0,0 +1,30 @@
#import "TGChatTimestamp.h"
@interface TGChatTimestamp ()
{
NSString *_cachedIdentifier;
}
@end
@implementation TGChatTimestamp
- (instancetype)initWithDate:(NSTimeInterval)date string:(NSString *)string
{
self = [super init];
if (self != nil)
{
_date = date;
_string = string;
}
return self;
}
- (NSString *)uniqueIdentifier
{
if (_cachedIdentifier == nil)
_cachedIdentifier = [NSString stringWithFormat:@"ts-%ld", (long)_date];
return _cachedIdentifier;
}
@end
@@ -0,0 +1,5 @@
#import <ClockKit/ClockKit.h>
@interface TGComplicationController : NSObject <CLKComplicationDataSource>
@end
@@ -0,0 +1,78 @@
#import "TGComplicationController.h"
#import "TGWatchCommon.h"
#import "TGStringUtils.h"
@implementation TGComplicationController
- (void)getSupportedTimeTravelDirectionsForComplication:(CLKComplication *)complication withHandler:(void (^)(CLKComplicationTimeTravelDirections))handler
{
handler(CLKComplicationTimeTravelDirectionNone);
}
- (void)getPrivacyBehaviorForComplication:(CLKComplication *)complication withHandler:(void (^)(CLKComplicationPrivacyBehavior))handler
{
handler(CLKComplicationPrivacyBehaviorShowOnLockScreen);
}
- (void)getPlaceholderTemplateForComplication:(CLKComplication *)complication withHandler:(void (^)(CLKComplicationTemplate * _Nullable))handler
{
CLKComplicationTemplate *result = nil;
switch (complication.family)
{
case CLKComplicationFamilyModularLarge:
{
}
break;
case CLKComplicationFamilyUtilitarianSmall:
{
}
break;
case CLKComplicationFamilyUtilitarianLarge:
{
CLKComplicationTemplateUtilitarianLargeFlat *template = [[CLKComplicationTemplateUtilitarianLargeFlat alloc] init];
CLKSimpleTextProvider *textProvider = [[CLKSimpleTextProvider alloc] init];
textProvider.text = TGLocalized(@"Complication.LongNone");
template.textProvider = textProvider;
result = template;
}
break;
default:
break;
}
handler(result);
}
- (void)getCurrentTimelineEntryForComplication:(CLKComplication *)complication withHandler:(void (^)(CLKComplicationTimelineEntry * _Nullable))handler
{
CLKComplicationTemplate *result = nil;
switch (complication.family)
{
case CLKComplicationFamilyUtilitarianLarge:
{
CLKComplicationTemplateUtilitarianLargeFlat *template = [[CLKComplicationTemplateUtilitarianLargeFlat alloc] init];
CLKSimpleTextProvider *textProvider = [[CLKSimpleTextProvider alloc] init];
textProvider.text = TGLocalized(@"Complication.LongNone");
template.textProvider = textProvider;
result = template;
}
break;
default:
break;
}
CLKComplicationTimelineEntry *entry = [CLKComplicationTimelineEntry entryWithDate:[NSDate date] complicationTemplate:result];
handler(entry);
}
@end
@@ -0,0 +1,24 @@
#import "TGInterfaceController.h"
@interface TGComposeController : TGInterfaceController
@property (nonatomic, weak) IBOutlet WKInterfaceLabel *recipientLabel;
@property (nonatomic, weak) IBOutlet WKInterfaceLabel *messageLabel;
@property (nonatomic, weak) IBOutlet WKInterfaceButton *stickerButton;
@property (nonatomic, weak) IBOutlet WKInterfaceButton *locationButton;
@property (nonatomic, weak) IBOutlet WKInterfaceButton *addContactButton;
@property (nonatomic, weak) IBOutlet WKInterfaceButton *createMessageButton;
@property (nonatomic, weak) IBOutlet WKInterfaceButton *sendButton;
@property (nonatomic, weak) IBOutlet WKInterfaceGroup *bottomGroup;
@property (nonatomic, weak) IBOutlet WKInterfaceGroup *stickerGroup;
@property (nonatomic, weak) IBOutlet WKInterfaceImage *locationIcon;
- (IBAction)addContactPressedAction;
- (IBAction)createMessagePressedAction;
- (IBAction)stickerPressedAction;
- (IBAction)locationPressedAction;
- (IBAction)sendPressedAction;
@end
@@ -0,0 +1,292 @@
#import "TGComposeController.h"
#import <WatchCommonWatch/WatchCommonWatch.h>
#import "TGWatchCommon.h"
#import "TGBridgeSendMessageSignals.h"
#import "WKInterfaceGroup+Signals.h"
#import "TGBridgeMediaSignals.h"
#import "TGContactsController.h"
#import "TGInputController.h"
#import "TGStickersController.h"
#import "TGLocationController.h"
NSString *const TGComposeControllerIdentifier = @"TGComposeController";
@interface TGComposeController ()
{
TGBridgeUser *_recipient;
NSString *_messageText;
TGBridgeDocumentMediaAttachment *_messageSticker;
TGBridgeLocationMediaAttachment *_messageLocation;
SMetaDisposable *_sendMessageDisposable;
}
@end
@implementation TGComposeController
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_sendMessageDisposable = [[SMetaDisposable alloc] init];
[self.locationIcon _setInitialHidden:true];
[self.stickerGroup _setInitialHidden:true];
}
return self;
}
- (void)dealloc
{
[_sendMessageDisposable dispose];
}
- (void)configureWithContext:(id<TGInterfaceContext>)__unused context
{
self.recipientLabel.text = TGLocalized(@"Watch.Compose.AddContact");
self.messageLabel.text = TGLocalized(@"Watch.Compose.CreateMessage");
[self setSendButtonEnabled:false];
}
- (void)willActivate
{
[super willActivate];
[self.stickerGroup updateIfNeeded];
}
- (void)didDeactivate
{
[super didDeactivate];
}
- (IBAction)addContactPressedAction
{
[TGInputController presentPlainInputControllerForInterfaceController:self completion:^(NSString *text)
{
__weak TGComposeController *weakSelf = self;
TGContactsControllerContext *context = [[TGContactsControllerContext alloc] initWithQuery:text];
context.completionBlock = ^(TGBridgeUser *contact)
{
__strong TGComposeController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf setRecipient:contact];
};
[self presentControllerWithClass:[TGContactsController class] context:context];
}];
}
- (IBAction)createMessagePressedAction
{
[TGInputController presentInputControllerForInterfaceController:self suggestionsForText:nil completion:^(NSString *text)
{
[self setMessageText:text];
}];
}
- (IBAction)stickerPressedAction
{
__weak TGComposeController *weakSelf = self;
TGStickersControllerContext *context = [[TGStickersControllerContext alloc] init];
context.completionBlock = ^(TGBridgeDocumentMediaAttachment *sticker)
{
__strong TGComposeController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf setMessageSticker:sticker];
};
[self presentControllerWithClass:[TGStickersController class] context:context];
}
- (IBAction)locationPressedAction
{
__weak TGComposeController *weakSelf = self;
TGLocationControllerContext *context = [[TGLocationControllerContext alloc] init];
context.completionBlock = ^(TGBridgeLocationMediaAttachment *location)
{
__strong TGComposeController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf setMessageLocation:location];
};
[self presentControllerWithClass:[TGLocationController class] context:context];
}
- (IBAction)sendPressedAction
{
__weak TGComposeController *weakSelf = self;
if (_messageSticker != nil)
{
[_sendMessageDisposable setDisposable:[[TGBridgeSendMessageSignals sendMessageWithPeerId:_recipient.identifier sticker:_messageSticker replyToMid:0] startWithNext:^(id next)
{
__strong TGComposeController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf dismissController];
} completed:^
{
}]];
}
else if (_messageLocation != nil)
{
[_sendMessageDisposable setDisposable:[[TGBridgeSendMessageSignals sendMessageWithPeerId:_recipient.identifier location:_messageLocation replyToMid:0] startWithNext:^(id next)
{
__strong TGComposeController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf dismissController];
} completed:^
{
}]];
}
else if (_messageText != nil)
{
[_sendMessageDisposable setDisposable:[[TGBridgeSendMessageSignals sendMessageWithPeerId:_recipient.identifier text:_messageText replyToMid:0] startWithNext:^(id next)
{
__strong TGComposeController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf dismissController];
} completed:^{
}]];
}
}
- (void)setRecipient:(TGBridgeUser *)recipient
{
_recipient = recipient;
[self performInterfaceUpdate:^(bool animated)
{
if (recipient != nil)
{
self.recipientLabel.text = [recipient displayName];
self.recipientLabel.textColor = [UIColor whiteColor];
}
else
{
self.recipientLabel.text = TGLocalized(@"Watch.Compose.AddContact");
self.recipientLabel.textColor = [UIColor hexColor:0xaeb4bf];
}
[self updateSendButtonEnabled];
}];
}
- (void)setMessageText:(NSString *)messageText
{
_messageSticker = nil;
_messageLocation = nil;
_messageText = messageText;
[self performInterfaceUpdate:^(bool animated)
{
self.stickerGroup.hidden = true;
self.locationIcon.hidden = true;
self.messageLabel.hidden = false;
if (messageText.length > 0)
{
self.messageLabel.text = messageText;
self.messageLabel.textColor = [UIColor whiteColor];
}
else
{
self.messageLabel.text = TGLocalized(@"Watch.Compose.CreateMessage");
self.messageLabel.textColor = [UIColor hexColor:0xaeb4bf];
}
[self updateSendButtonEnabled];
}];
}
- (void)setMessageSticker:(TGBridgeDocumentMediaAttachment *)messageSticker
{
_messageText = nil;
_messageLocation = nil;
_messageSticker = messageSticker;
[self performInterfaceUpdate:^(bool animated)
{
self.stickerGroup.hidden = false;
self.locationIcon.hidden = true;
self.messageLabel.hidden = true;
self.messageLabel.text = @"";
__weak TGComposeController *weakSelf = self;
[self.stickerGroup setBackgroundImageSignal:[TGBridgeMediaSignals stickerWithDocumentId:messageSticker.documentId packId:messageSticker.stickerPackId accessHash:messageSticker.stickerPackAccessHash type:TGMediaStickerImageTypeInput] isVisible:^bool
{
__strong TGComposeController *strongSelf = weakSelf;
if (strongSelf == nil)
return false;
return strongSelf.isVisible;
}];
[self updateSendButtonEnabled];
}];
}
- (void)setMessageLocation:(TGBridgeLocationMediaAttachment *)messageLocation
{
_messageText = nil;
_messageSticker = nil;
_messageLocation = messageLocation;
[self performInterfaceUpdate:^(bool animated)
{
self.stickerGroup.hidden = true;
self.locationIcon.hidden = false;
self.messageLabel.hidden = false;
if (messageLocation.venue != nil)
self.messageLabel.text = messageLocation.venue.title;
else
self.messageLabel.text = TGLocalized(@"Watch.Compose.CurrentLocation");
self.messageLabel.textColor = [UIColor hexColor:0xaeb4bf];
[self updateSendButtonEnabled];
}];
}
- (void)setSendButtonEnabled:(bool)enabled
{
NSAttributedString *buttonTitle = [[NSAttributedString alloc] initWithString:TGLocalized(@"Watch.Compose.Send") attributes:@{ NSForegroundColorAttributeName:enabled ? [UIColor hexColor:0x2094fa] : [UIColor hexColor:0xaeb4bf], NSFontAttributeName: [UIFont systemFontOfSize:15] }];
self.sendButton.enabled = enabled;
self.sendButton.attributedTitle = buttonTitle;
}
- (void)updateSendButtonEnabled
{
bool hasRecipient = (_recipient != nil);
bool hasContent = (_messageText.length > 0 || _messageSticker != nil || _messageLocation != nil);
[self setSendButtonEnabled:hasRecipient && hasContent];
}
+ (NSString *)identifier
{
return TGComposeControllerIdentifier;
}
@end
@@ -0,0 +1,23 @@
#import "TGInterfaceController.h"
@class TGBridgeContext;
@class TGBridgeUser;
@interface TGContactsControllerContext : NSObject <TGInterfaceContext>
@property (nonatomic, strong) TGBridgeContext *context;
@property (nonatomic, readonly) NSString *query;
@property (nonatomic, copy) void (^completionBlock)(TGBridgeUser *user);
- (instancetype)initWithQuery:(NSString *)query;
@end
@interface TGContactsController : TGInterfaceController
@property (nonatomic, weak) IBOutlet WKInterfaceTable *table;
@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator;
@property (nonatomic, weak) IBOutlet WKInterfaceLabel *alertLabel;
@end
@@ -0,0 +1,154 @@
#import "TGContactsController.h"
#import "TGWatchCommon.h"
#import "TGBridgeContactsSignals.h"
#import "WKInterfaceTable+TGDataDrivenTable.h"
#import "TGInputController.h"
#import "TGUserRowController.h"
NSString *const TGContactsControllerIdentifier = @"TGContactsController";
const NSUInteger TGContactsControllerBatchCount = 15;
@implementation TGContactsControllerContext
- (instancetype)initWithQuery:(NSString *)query
{
self = [super init];
if (self != nil)
{
_query = query;
}
return self;
}
@end
@interface TGContactsController () <TGTableDataSource>
{
SMetaDisposable *_contactsDisposable;
TGContactsControllerContext *_context;
NSArray *_userModels;
}
@end
@implementation TGContactsController
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_contactsDisposable = [[SMetaDisposable alloc] init];
[self.alertLabel _setInitialHidden:true];
[self.table _setInitialHidden:true];
self.table.tableDataSource = self;
}
return self;
}
- (void)dealloc
{
[_contactsDisposable dispose];
}
- (void)configureWithContext:(TGContactsControllerContext *)context
{
_context = context;
__weak TGContactsController *weakSelf = self;
[_contactsDisposable setDisposable:[[[TGBridgeContactsSignals searchContactsWithQuery:_context.query] deliverOn:[SQueue mainQueue]] startWithNext:^(NSArray *users)
{
__strong TGContactsController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
if (users.count > TGContactsControllerBatchCount)
strongSelf->_userModels = [users subarrayWithRange:NSMakeRange(0, TGContactsControllerBatchCount)];
else
strongSelf->_userModels = users;
[strongSelf performInterfaceUpdate:^(bool animated)
{
__strong TGContactsController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf.activityIndicator.hidden = true;
if (strongSelf->_userModels.count > 0)
{
strongSelf.table.hidden = false;
[strongSelf.table reloadData];
}
else
{
strongSelf->_alertLabel.hidden = false;
strongSelf->_alertLabel.text = TGLocalized(@"Watch.Contacts.NoResults");
}
}];
} error:^(id error)
{
} completed:^
{
}]];
}
- (void)willActivate
{
[super willActivate];
[self.table notifyVisiblityChange];
}
- (void)didDeactivate
{
[super didDeactivate];
}
#pragma mark -
- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section
{
return _userModels.count;
}
- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(NSIndexPath *)indexPath
{
return [TGUserRowController class];
}
- (void)table:(WKInterfaceTable *)table updateRowController:(TGUserRowController *)controller forIndexPath:(TGIndexPath *)indexPath
{
__weak TGContactsController *weakSelf = self;
controller.isVisible = ^bool
{
__strong TGContactsController *strongSelf = weakSelf;
if (strongSelf == nil)
return false;
return strongSelf.isVisible;
};
[controller updateWithUser:_userModels[indexPath.row] context:_context.context];
}
- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath
{
[self dismissController];
if (_context.completionBlock != nil)
_context.completionBlock(_userModels[indexPath.row]);
}
+ (NSString *)identifier
{
return TGContactsControllerIdentifier;
}
@end
@@ -0,0 +1,44 @@
#import "WKInterfaceTable+TGDataDrivenTable.h"
typedef NS_OPTIONS(NSUInteger, TGConversationFooterOptions) {
TGConversationFooterOptionsSendMessage = 1 << 0,
TGConversationFooterOptionsUnblock = 1 << 1,
TGConversationFooterOptionsStartBot = 1 << 2,
TGConversationFooterOptionsRestartBot = 1 << 3,
TGConversationFooterOptionsInactive = 1 << 4,
TGConversationFooterOptionsBotCommands = 1 << 5,
TGConversationFooterOptionsBotKeyboard = 1 << 6,
TGConversationFooterOptionsVoice = 1 << 7
};
@interface TGConversationFooterController : TGTableRowController
@property (nonatomic, weak) IBOutlet WKInterfaceGroup *attachmentsGroup;
@property (nonatomic, weak) IBOutlet WKInterfaceImage *commandsIcon;
@property (nonatomic, weak) IBOutlet WKInterfaceButton *commandsButton;
@property (nonatomic, weak) IBOutlet WKInterfaceButton *stickerButton;
@property (nonatomic, weak) IBOutlet WKInterfaceButton *locationButton;
@property (nonatomic, weak) IBOutlet WKInterfaceButton *voiceButton;
@property (nonatomic, weak) IBOutlet WKInterfaceButton *bottomButton;
- (IBAction)commandsButtonPressedAction;
- (IBAction)stickerButtonPressedAction;
- (IBAction)locationButtonPressedAction;
- (IBAction)voiceButtonPressedAction;
- (IBAction)bottomButtonPressedAction;
@property (nonatomic, assign) TGConversationFooterOptions options;
- (void)setOptions:(TGConversationFooterOptions)options animated:(bool)animated;
@property (nonatomic, copy) void (^commandsPressed)(void);
@property (nonatomic, copy) void (^stickerPressed)(void);
@property (nonatomic, copy) void (^locationPressed)(void);
@property (nonatomic, copy) void (^voicePressed)(void);
@property (nonatomic, copy) void (^replyPressed)(void);
@property (nonatomic, copy) void (^unblockPressed)(void);
@property (nonatomic, copy) void (^startPressed)(void);
@property (nonatomic, copy) void (^restartPressed)(void);
@property (nonatomic, copy) void (^animate)(void (^)(void));
@end
@@ -0,0 +1,150 @@
#import "TGConversationFooterController.h"
#import "TGWatchCommon.h"
NSString *const TGConversationFooterIdentifier = @"TGConversationFooter";
@implementation TGConversationFooterController
- (void)setOptions:(TGConversationFooterOptions)options
{
[self setOptions:options animated:false];
}
- (void)setOptions:(TGConversationFooterOptions)options animated:(bool)animated
{
void (^changeBlock)() = ^
{
if (options == _options)
return;
_options = options;
bool isSendMessage = options & TGConversationFooterOptionsSendMessage;
bool isStartBot = options & TGConversationFooterOptionsStartBot;
bool isRestartBot = options & TGConversationFooterOptionsRestartBot;
bool isUnblock = options & TGConversationFooterOptionsUnblock;
bool isInactive = options & TGConversationFooterOptionsInactive;
bool hasCommandsButton = options & TGConversationFooterOptionsBotCommands;
bool hasKeyboardButton = options & TGConversationFooterOptionsBotKeyboard;
bool hasVoiceButton = options & TGConversationFooterOptionsVoice;
if (isSendMessage)
{
self.attachmentsGroup.hidden = false;
self.bottomButton.hidden = false;
self.bottomButton.title = TGLocalized(@"Watch.Conversation.Reply");
NSInteger buttonCount = 2;
CGFloat buttonWidth = 0.5f;
if (hasCommandsButton || hasKeyboardButton)
buttonCount += 1;
if (hasVoiceButton)
buttonCount += 1;
buttonWidth = 1.0f / buttonCount;
bool commandButtonHidden = (!hasCommandsButton && !hasKeyboardButton);
[self.commandsButton setHidden:commandButtonHidden];
[self.voiceButton setHidden:!hasVoiceButton];
if (!commandButtonHidden)
[self.commandsIcon setImageNamed:hasCommandsButton ? @"BotCommandIcon": @"BotKeyboardIcon"];
[self.commandsButton setRelativeWidth:commandButtonHidden ? 0.0 : buttonWidth withAdjustment:0];
[self.voiceButton setRelativeWidth:!hasVoiceButton ? 0.0 : buttonWidth withAdjustment:0];
[self.stickerButton setRelativeWidth:buttonWidth withAdjustment:0];
[self.locationButton setRelativeWidth:buttonWidth withAdjustment:0];
}
else if (isStartBot)
{
self.attachmentsGroup.hidden = true;
self.bottomButton.hidden = false;
self.bottomButton.title = TGLocalized(@"Bot.Start");
}
else if (isRestartBot)
{
self.attachmentsGroup.hidden = true;
self.bottomButton.hidden = false;
self.bottomButton.title = TGLocalized(@"Watch.Bot.Restart");
}
else if (isUnblock)
{
self.attachmentsGroup.hidden = true;
self.bottomButton.hidden = false;
self.bottomButton.title = TGLocalized(@"Watch.Conversation.Unblock");
}
else if (isInactive)
{
self.attachmentsGroup.hidden = true;
self.bottomButton.hidden = true;
}
};
if (animated)
self.animate(changeBlock);
else
changeBlock();
}
- (IBAction)commandsButtonPressedAction
{
if (self.commandsPressed != nil)
self.commandsPressed();
}
- (IBAction)stickerButtonPressedAction
{
if (self.stickerPressed != nil)
self.stickerPressed();
}
- (IBAction)locationButtonPressedAction
{
if (self.locationPressed != nil)
self.locationPressed();
}
- (IBAction)voiceButtonPressedAction
{
if (self.voicePressed != nil)
self.voicePressed();
}
- (IBAction)bottomButtonPressedAction
{
bool isSendMessage = _options & TGConversationFooterOptionsSendMessage;
bool isStartBot = _options & TGConversationFooterOptionsStartBot;
bool isRestartBot = _options & TGConversationFooterOptionsRestartBot;
bool isUnblock = _options & TGConversationFooterOptionsUnblock;
if (isSendMessage)
{
if (self.replyPressed != nil)
self.replyPressed();
}
else if (isStartBot)
{
if (self.startPressed != nil)
self.startPressed();
}
else if (isRestartBot)
{
if (self.restartPressed != nil)
self.restartPressed();
}
else if (isUnblock)
{
if (self.unblockPressed != nil)
self.unblockPressed();
}
}
#pragma mark -
+ (NSString *)identifier
{
return TGConversationFooterIdentifier;
}
@end
+29
View File
@@ -0,0 +1,29 @@
#import <Foundation/Foundation.h>
#import "TGChatTimestamp.h"
typedef enum
{
TGDateRelativeSpanLately = -2,
TGDateRelativeSpanWithinAWeek = -3,
TGDateRelativeSpanWithinAMonth = -4,
TGDateRelativeSpanALongTimeAgo = -5
} TGDateRelativeSpan;
@interface TGDateUtils : NSObject
+ (void)reset;
+ (NSString *)stringForFullDate:(int)date;
+ (NSString *)stringForShortTime:(int)time;
+ (NSString *)stringForShortTime:(int)time daytimeVariant:(int *)daytimeVariant;
+ (NSString *)stringForDialogTime:(int)time;
+ (NSString *)stringForDayOfWeek:(int)date;
+ (NSString *)stringForMonthOfYear:(int)date;
+ (NSString *)stringForPreciseDate:(int)date;
+ (NSString *)stringForApproximateDate:(int)date;
+ (NSString *)stringForRelativeLastSeen:(int)date;
+ (NSString *)stringForMessageListDate:(int)date;
+ (TGChatTimestamp *)timestampForDateIfNeeded:(int)date previousDate:(NSNumber *)previousDate;
@end
+547
View File
@@ -0,0 +1,547 @@
#import "TGDateUtils.h"
#import "TGStringUtils.h"
#import "TGWatchCommon.h"
#import <time.h>
static bool value_dateHas12hFormat = false;
static __strong NSString *value_monthNamesGenShort[] = {
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil
};
static __strong NSString *value_monthNamesGenFull[] = {
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil
};
static __strong NSString *value_weekdayNamesShort[] = {
nil, nil, nil, nil, nil, nil, nil
};
static __strong NSString *value_weekdayNamesFull[] = {
nil, nil, nil, nil, nil, nil, nil
};
static NSString *value_dialogTimeFormat = nil;
static NSString *value_date_separator = @".";
static bool value_monthFirst = false;
static bool isArabic = false;
static bool isKorean = false;
static bool TGDateUtilsInitialized = false;
static void initializeTGDateUtils()
{
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[NSLocale currentLocale]];
[dateFormatter setDateStyle:NSDateFormatterNoStyle];
[dateFormatter setTimeStyle:NSDateFormatterMediumStyle];
NSTimeZone *timeZone = [NSTimeZone localTimeZone];
[dateFormatter setTimeZone:timeZone];
NSString *dateString = [dateFormatter stringFromDate:[NSDate date]];
NSRange amRange = [dateString rangeOfString:[dateFormatter AMSymbol]];
NSRange pmRange = [dateString rangeOfString:[dateFormatter PMSymbol]];
value_dateHas12hFormat = !(amRange.location == NSNotFound && pmRange.location == NSNotFound);
dateString = [NSDateFormatter dateFormatFromTemplate:@"MdY" options:0 locale:[NSLocale currentLocale]];
if ([dateString rangeOfString:@"."].location != NSNotFound)
{
value_date_separator = @".";
}
else if ([dateString rangeOfString:@"/"].location != NSNotFound)
{
value_date_separator = @"/";
}
else if ([dateString rangeOfString:@"-"].location != NSNotFound)
{
value_date_separator = @"-";
}
if ([dateString rangeOfString:[NSString stringWithFormat:@"M%@d", value_date_separator]].location != NSNotFound)
{
value_monthFirst = true;
}
NSString *identifier = [[NSLocale currentLocale] localeIdentifier];
if ([identifier isEqualToString:@"ar"] || [identifier hasPrefix:@"ar_"])
{
isArabic = true;
value_date_separator = @"\u060d";
}
else if ([identifier isEqualToString:@"ko"] || [identifier hasPrefix:@"ko-"])
{
isKorean = true;
}
value_monthNamesGenShort[0] = TGLocalized(@"Month.ShortJanuary");
value_monthNamesGenShort[1] = TGLocalized(@"Month.ShortFebruary");
value_monthNamesGenShort[2] = TGLocalized(@"Month.ShortMarch");
value_monthNamesGenShort[3] = TGLocalized(@"Month.ShortApril");
value_monthNamesGenShort[4] = TGLocalized(@"Month.ShortMay");
value_monthNamesGenShort[5] = TGLocalized(@"Month.ShortJune");
value_monthNamesGenShort[6] = TGLocalized(@"Month.ShortJuly");
value_monthNamesGenShort[7] = TGLocalized(@"Month.ShortAugust");
value_monthNamesGenShort[8] = TGLocalized(@"Month.ShortSeptember");
value_monthNamesGenShort[9] = TGLocalized(@"Month.ShortOctober");
value_monthNamesGenShort[10] = TGLocalized(@"Month.ShortNovember");
value_monthNamesGenShort[11] = TGLocalized(@"Month.ShortDecember");
value_monthNamesGenFull[0] = TGLocalized(@"Month.GenJanuary");
value_monthNamesGenFull[1] = TGLocalized(@"Month.GenFebruary");
value_monthNamesGenFull[2] = TGLocalized(@"Month.GenMarch");
value_monthNamesGenFull[3] = TGLocalized(@"Month.GenApril");
value_monthNamesGenFull[4] = TGLocalized(@"Month.GenMay");
value_monthNamesGenFull[5] = TGLocalized(@"Month.GenJune");
value_monthNamesGenFull[6] = TGLocalized(@"Month.GenJuly");
value_monthNamesGenFull[7] = TGLocalized(@"Month.GenAugust");
value_monthNamesGenFull[8] = TGLocalized(@"Month.GenSeptember");
value_monthNamesGenFull[9] = TGLocalized(@"Month.GenOctober");
value_monthNamesGenFull[10] = TGLocalized(@"Month.GenNovember");
value_monthNamesGenFull[11] = TGLocalized(@"Month.GenDecember");
value_weekdayNamesShort[0] = TGLocalized(@"Weekday.ShortMonday");
value_weekdayNamesShort[1] = TGLocalized(@"Weekday.ShortTuesday");
value_weekdayNamesShort[2] = TGLocalized(@"Weekday.ShortWednesday");
value_weekdayNamesShort[3] = TGLocalized(@"Weekday.ShortThursday");
value_weekdayNamesShort[4] = TGLocalized(@"Weekday.ShortFriday");
value_weekdayNamesShort[5] = TGLocalized(@"Weekday.ShortSaturday");
value_weekdayNamesShort[6] = TGLocalized(@"Weekday.ShortSunday");
value_weekdayNamesFull[0] = TGLocalized(@"Weekday.Monday");
value_weekdayNamesFull[1] = TGLocalized(@"Weekday.Tuesday");
value_weekdayNamesFull[2] = TGLocalized(@"Weekday.Wednesday");
value_weekdayNamesFull[3] = TGLocalized(@"Weekday.Thursday");
value_weekdayNamesFull[4] = TGLocalized(@"Weekday.Friday");
value_weekdayNamesFull[5] = TGLocalized(@"Weekday.Saturday");
value_weekdayNamesFull[6] = TGLocalized(@"Weekday.Sunday");
value_dialogTimeFormat = [[TGLocalized(@"Date.DialogDateFormat") stringByReplacingOccurrencesOfString:@"{month}" withString:@"%1$@"] stringByReplacingOccurrencesOfString:@"{day}" withString:@"%2$@"];
TGDateUtilsInitialized = true;
}
static inline bool dateHas12hFormat()
{
if (!TGDateUtilsInitialized)
initializeTGDateUtils();
return value_dateHas12hFormat;
}
bool TGUse12hDateFormat()
{
if (!TGDateUtilsInitialized)
initializeTGDateUtils();
return value_dateHas12hFormat;
}
static inline NSString *weekdayNameShort(int number)
{
if (!TGDateUtilsInitialized)
initializeTGDateUtils();
if (number < 0)
number = 0;
if (number > 6)
number = 6;
if (number == 0)
number = 6;
else
number--;
return value_weekdayNamesShort[number];
}
static inline NSString *weekdayNameFull(int number)
{
if (!TGDateUtilsInitialized)
initializeTGDateUtils();
if (number < 0)
number = 0;
if (number > 6)
number = 6;
if (number == 0)
number = 6;
else
number--;
return value_weekdayNamesFull[number];
}
static inline NSString *monthNameGenFull(int number)
{
if (!TGDateUtilsInitialized)
initializeTGDateUtils();
if (number < 0)
number = 0;
if (number > 11)
number = 11;
return value_monthNamesGenFull[number];
}
static inline NSString *dialogTimeFormat()
{
if (!TGDateUtilsInitialized)
initializeTGDateUtils();
return value_dialogTimeFormat;
}
@implementation TGDateUtils
+ (void)reset
{
TGDateUtilsInitialized = false;
}
+ (NSString *)stringForShortTime:(int)time
{
time_t t = time;
struct tm timeinfo;
localtime_r(&t, &timeinfo);
return [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min];
}
+ (NSString *)stringForShortTimeWithHours:(int)hours minutes:(int)minutes
{
if (!TGDateUtilsInitialized)
initializeTGDateUtils();
if (isArabic)
{
if (dateHas12hFormat())
{
if (hours < 12)
return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%d:%02d ص", hours == 0 ? 12 : hours, minutes]];
else
return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%d:%02d م", (hours - 12 == 0) ? 12 : (hours - 12), minutes]];
}
else
return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%02d:%02d", hours, minutes]];
}
else if (isKorean)
{
return [[NSString alloc] initWithFormat:@"%02d:%02d", hours, minutes];
}
else
{
if (dateHas12hFormat())
{
if (hours < 12)
return [[NSString alloc] initWithFormat:@"%d:%02d AM", hours == 0 ? 12 : hours, minutes];
else
return [[NSString alloc] initWithFormat:@"%d:%02d PM", (hours - 12 == 0) ? 12 : (hours - 12), minutes];
}
else
return [[NSString alloc] initWithFormat:@"%02d:%02d", hours, minutes];
}
}
+ (NSString *)stringForShortTime:(int)time daytimeVariant:(int *)__unused daytimeVariant
{
return [self stringForShortTime:time];
}
+ (NSString *)stringForDialogTime:(int)time
{
time_t t = time;
struct tm timeinfo;
gmtime_r(&t, &timeinfo);
return [[NSString alloc] initWithFormat:dialogTimeFormat(), monthNameGenFull(timeinfo.tm_mon), [TGStringUtils stringWithLocalizedNumber:timeinfo.tm_mday]];
}
+ (NSString *)stringForDayOfWeek:(int)date
{
time_t t = date;
struct tm timeinfo;
localtime_r(&t, &timeinfo);
return weekdayNameFull(timeinfo.tm_wday);
}
+ (NSString *)stringForMonthOfYear:(int)date
{
time_t t = date;
struct tm timeinfo;
localtime_r(&t, &timeinfo);
NSString *format = TGLocalized(([[NSString alloc] initWithFormat:@"Time.MonthOfYear_m%d", (int)timeinfo.tm_mon + 1]));
return [[NSString alloc] initWithFormat:format, [[NSString alloc] initWithFormat:@"%d", 2000 + timeinfo.tm_year - 100]];
}
+ (NSString *)stringForFullDateWithDay:(int)day month:(int)month year:(int)year
{
if (isArabic)
{
return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%d%@%d%@%02d", day, value_date_separator, month, value_date_separator, year - 100]];
}
else if (isKorean)
{
return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%04d년 %d월 %d일", year - 100 + 2000, month, day]];
}
else
{
if (value_monthFirst)
{
return [[NSString alloc] initWithFormat:@"%d%@%d%@%02d", month, value_date_separator, day, value_date_separator, year - 100];
}
else
{
return [[NSString alloc] initWithFormat:@"%d%@%02d%@%02d", day, value_date_separator, month, value_date_separator, year - 100];
}
}
}
+ (NSString *)stringForPreciseDate:(int)date
{
time_t t = date;
struct tm timeinfo;
localtime_r(&t, &timeinfo);
NSString *format = TGLocalized(([[NSString alloc] initWithFormat:@"Time.PreciseDate_m%d", (int)timeinfo.tm_mon + 1]));
return [[NSString alloc] initWithFormat:format, [[NSString alloc] initWithFormat:@"%d", timeinfo.tm_mday], [[NSString alloc] initWithFormat:@"%d", (int)(2000 + timeinfo.tm_year - 100)], [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]];
}
+ (NSString *)stringForMessageListDate:(int)date
{
time_t t = date;
struct tm timeinfo;
localtime_r(&t, &timeinfo);
time_t t_now;
time(&t_now);
struct tm timeinfo_now;
localtime_r(&t_now, &timeinfo_now);
if (timeinfo.tm_year != timeinfo_now.tm_year)
{
return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year];
}
else
{
int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday;
if(dayDiff == 0)
return [self stringForShortTime:date];
else if(dayDiff == -1)
return weekdayNameFull(timeinfo.tm_wday);
else if(dayDiff == -2)
return weekdayNameFull(timeinfo.tm_wday);
else if(dayDiff > -7 && dayDiff <= -2)
return weekdayNameFull(timeinfo.tm_wday);
else
return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year];
}
return nil;
}
+ (NSString *)stringForApproximateDate:(int)date
{
time_t t = date;
struct tm timeinfo;
localtime_r(&t, &timeinfo);
time_t t_now;
time(&t_now);
struct tm timeinfo_now;
localtime_r(&t_now, &timeinfo_now);
if (timeinfo.tm_year != timeinfo_now.tm_year)
return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year];
else
{
int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday;
if(dayDiff == 0 || dayDiff == -1)
return [self stringForTodayOrYesterday:dayDiff == 0 hours:timeinfo.tm_hour minutes:timeinfo.tm_min];
else if (false && dayDiff > -7 && dayDiff <= -2)
return weekdayNameShort(timeinfo.tm_wday);
else
return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year];
}
return nil;
}
+ (NSString *)stringForFullDate:(int)date
{
time_t t = date;
struct tm timeinfo;
localtime_r(&t, &timeinfo);
time_t t_now;
time(&t_now);
struct tm timeinfo_now;
localtime_r(&t_now, &timeinfo_now);
return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year];
}
+ (NSString *)stringForTodayOrYesterday:(bool)today hours:(int)hours minutes:(int)minutes
{
NSString *timeString = [self stringForShortTimeWithHours:hours minutes:minutes];
return [[NSString alloc] initWithFormat:today ? TGLocalized(@"Time.TodayAt") : TGLocalized(@"Time.YesterdayAt"), timeString];
}
+ (NSString *)stringForRelativeLastSeen:(int)date
{
if (date == -1)
return TGLocalized(@"Presence.invisible");
else if (date == TGDateRelativeSpanLately)
return TGLocalized(@"Watch.LastSeen.Lately");
else if (date == TGDateRelativeSpanWithinAWeek)
return TGLocalized(@"Watch.LastSeen.WithinAWeek");
else if (date == TGDateRelativeSpanWithinAMonth)
return TGLocalized(@"Watch.LastSeen.WithinAMonth");
else if (date == TGDateRelativeSpanALongTimeAgo)
return TGLocalized(@"Watch.LastSeen.ALongTimeAgo");
else if (date <= 0)
return @" ";
time_t t = date;
struct tm timeinfo;
localtime_r(&t, &timeinfo);
time_t t_now;
time(&t_now);
struct tm timeinfo_now;
localtime_r(&t_now, &timeinfo_now);
if (timeinfo.tm_year != timeinfo_now.tm_year)
return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.AtDate"), [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]];
else
{
int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday;
int minutesDiff = (int)((t_now - date) / 60);
int hoursDiff = (int)((t_now - date) / (60 * 60));
if (dayDiff == 0 && hoursDiff <= 23)
{
if (minutesDiff < 1)
return TGLocalized(@"Watch.LastSeen.JustNow");
else if (minutesDiff < 60)
{
if (minutesDiff == 1)
return TGLocalized(@"Watch.LastSeen.MinutesAgo_1");
else if (minutesDiff == 2)
return TGLocalized(@"Watch.LastSeen.MinutesAgo_2");
else if (minutesDiff >= 3 && minutesDiff <= 10)
return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.MinutesAgo_3_10"), [TGStringUtils stringWithLocalizedNumber:minutesDiff]];
else
return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.MinutesAgo_any"), [TGStringUtils stringWithLocalizedNumber:minutesDiff]];
}
else
{
if (hoursDiff == 1)
return TGLocalized(@"Watch.LastSeen.HoursAgo_1");
else if (hoursDiff == 2)
return TGLocalized(@"Watch.LastSeen.HoursAgo_2");
else if (hoursDiff >= 3 && hoursDiff <= 10)
return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.HoursAgo_3_10"), [TGStringUtils stringWithLocalizedNumber:hoursDiff]];
else
return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.HoursAgo_any"), [TGStringUtils stringWithLocalizedNumber:hoursDiff]];
}
}
return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.AtDate"), [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]];
}
return nil;
}
+ (TGChatTimestamp *)timestampForDateIfNeeded:(int)date previousDate:(NSNumber *)previousDate
{
if (previousDate == nil)
return [self timestampForDate:date];
TGChatTimestamp *timestamp = nil;
time_t t = date;
struct tm timeinfo;
localtime_r(&t, &timeinfo);
time_t t_prev = previousDate.integerValue;
struct tm timeinfo_prev;
localtime_r(&t_prev, &timeinfo_prev);
time_t t_now;
time(&t_now);
struct tm timeinfo_now;
localtime_r(&t_now, &timeinfo_now);
bool timestampNeeded = false;
if (timeinfo.tm_yday != timeinfo_prev.tm_yday)
{
timestampNeeded = true;
}
else
{
if (timeinfo.tm_year != timeinfo_prev.tm_year)
{
timestampNeeded = true;
}
else
{
if (timeinfo.tm_year == timeinfo_now.tm_year)
{
if (timeinfo_now.tm_yday - timeinfo.tm_yday < 7)
{
if (abs(t - t_prev) > 3600)
timestampNeeded = true;
}
}
}
}
if (timestampNeeded)
timestamp = [self timestampForDate:date];
return timestamp;
}
+ (TGChatTimestamp *)timestampForDate:(int)date
{
time_t t = date;
struct tm timeinfo;
localtime_r(&t, &timeinfo);
time_t t_now;
time(&t_now);
struct tm timeinfo_now;
localtime_r(&t_now, &timeinfo_now);
NSString *string = nil;
if (timeinfo.tm_year != timeinfo_now.tm_year)
{
string = [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year];
}
else
{
int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday;
if (dayDiff == 0 || dayDiff == -1)
{
string = [NSString stringWithFormat:dayDiff == 0 ? TGLocalized(@"Watch.Time.ShortTodayAt") : TGLocalized(@"Watch.Time.ShortYesterdayAt"), [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]];
}
else if (dayDiff > -7)
{
string = [NSString stringWithFormat:TGLocalized(@"Watch.Time.ShortWeekdayAt"), [self stringForDayOfWeek:timeinfo.tm_wday], [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]];
}
else
{
string = [NSString stringWithFormat:TGLocalized(@"Watch.Time.ShortFullAt"), [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year], [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]];
}
}
return [[TGChatTimestamp alloc] initWithDate:date string:string];
}
@end
@@ -0,0 +1,32 @@
#import <WatchKit/WatchKit.h>
@class TGNeoChatsController;
@class TGFileCache;
typedef enum
{
TGContentSizeCategoryXS,
TGContentSizeCategoryS,
TGContentSizeCategoryM,
TGContentSizeCategoryL,
TGContentSizeCategoryXL,
TGContentSizeCategoryXXL,
TGContentSizeCategoryXXXL
} TGContentSizeCategory;
@interface TGExtensionDelegate : NSObject <WKExtensionDelegate>
@property (nonatomic, readonly) TGFileCache *audioCache;
@property (nonatomic, readonly) TGFileCache *imageCache;
@property (nonatomic, readonly) TGNeoChatsController *chatsController;
@property (nonatomic, readonly) TGContentSizeCategory contentSizeCategory;
- (void)setCustomLocalizationFile:(NSURL *)fileUrl;
+ (NSString *)documentsPath;
+ (instancetype)instance;
@end
@@ -0,0 +1,117 @@
#import "TGExtensionDelegate.h"
#import "TGWatchCommon.h"
#import "TGFileCache.h"
#import "TGBridgeClient.h"
#import "TGDateUtils.h"
#import "TGNeoChatsController.h"
@interface TGExtensionDelegate ()
{
NSString *_cachedContentSize;
TGContentSizeCategory _sizeCategory;
}
@end
@implementation TGExtensionDelegate
- (instancetype)init
{
self = [super init];
if (self != nil)
{
TGLog(@"Extension initialization start");
[TGBridgeClient instance];
_audioCache = [[TGFileCache alloc] initWithName:@"audio" useMemoryCache:false];
_audioCache.defaultFileExtension = @"m4a";
_imageCache = [[TGFileCache alloc] initWithName:@"images" useMemoryCache:true];
}
return self;
}
- (TGNeoChatsController *)chatsController
{
return (TGNeoChatsController *)[WKExtension sharedExtension].rootInterfaceController;
}
- (void)applicationDidBecomeActive
{
[[TGBridgeClient instance] handleDidBecomeActive];
}
- (void)applicationWillResignActive
{
[[TGBridgeClient instance] handleWillResignActive];
}
- (void)didReceiveRemoteNotification:(NSDictionary *)userInfo
{
}
- (void)didReceiveLocalNotification:(UILocalNotification *)notification
{
}
- (void)setCustomLocalizationFile:(NSURL *)fileUrl
{
if (fileUrl == nil)
TGResetLocalization();
else
TGSetLocalizationFromFile(fileUrl);
[TGDateUtils reset];
[[self chatsController] resetLocalization];
}
- (TGContentSizeCategory)contentSizeCategory
{
NSString *contentSize = [WKInterfaceDevice currentDevice].preferredContentSizeCategory;
if (![_cachedContentSize isEqualToString:contentSize])
{
_cachedContentSize = contentSize;
_sizeCategory = [TGExtensionDelegate contentSizeCategoryForString:contentSize];
}
return _sizeCategory;
}
+ (TGContentSizeCategory)contentSizeCategoryForString:(NSString *)string
{
if ([string isEqualToString:@"UICTContentSizeCategoryXS"])
return TGContentSizeCategoryXS;
else if ([string isEqualToString:@"UICTContentSizeCategoryS"])
return TGContentSizeCategoryS;
else if ([string isEqualToString:@"UICTContentSizeCategoryM"])
return TGContentSizeCategoryM;
else if ([string isEqualToString:@"UICTContentSizeCategoryL"])
return TGContentSizeCategoryL;
else if ([string isEqualToString:@"UICTContentSizeCategoryXL"])
return TGContentSizeCategoryXL;
else if ([string isEqualToString:@"UICTContentSizeCategoryXXL"])
return TGContentSizeCategoryXXL;
else if ([string isEqualToString:@"UICTContentSizeCategoryXXXL"])
return TGContentSizeCategoryXXXL;
return TGContentSizeCategoryL;
}
+ (NSString *)documentsPath
{
static dispatch_once_t onceToken;
static NSString *path;
dispatch_once(&onceToken, ^
{
path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0];
});
return path;
}
+ (instancetype)instance
{
return (TGExtensionDelegate *)[[WKExtension sharedExtension] delegate];
}
@end
+22
View File
@@ -0,0 +1,22 @@
#import <Foundation/Foundation.h>
@interface TGFileCache : NSObject
@property (nonatomic, strong) NSString *defaultFileExtension;
- (instancetype)initWithName:(NSString *)name useMemoryCache:(bool)useMemoryCache;
- (void)fetchDataForKey:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(id))completion;
- (void)fetchDataForKey:(NSString *)key memoryOnly:(bool)memoryOnly synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(id))completion;
- (void)cacheData:(NSData *)data key:(NSString *)key synchronous:(bool)synchronous completion:(void (^)(NSURL *))completion;
- (void)cacheData:(NSObject<NSCoding> *)data key:(NSString *)key synchronous:(bool)synchronous serializeBlock:(NSData *(^)(NSObject<NSCoding> *))serializeBlock completion:(void (^)(NSURL *))completion;
- (void)cacheFileAtURL:(NSURL *)url key:(NSString *)key synchronous:(bool)synchronous completion:(void (^)(NSURL *))completion;
- (void)cacheFileAtURL:(NSURL *)url key:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(NSURL *))completion;
- (void)cacheData:(NSData *)data key:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(NSURL *))completion;
- (void)clearCacheSynchronous:(bool)synchronous;
- (bool)hasDataForKey:(NSString *)key;
- (NSURL *)urlForKey:(NSString *)key;
@end
+208
View File
@@ -0,0 +1,208 @@
#import "TGFileCache.h"
#import "TGStringUtils.h"
NSString *const TGFileCacheDomain = @"com.telegram.FileCache";
@interface TGFileCache ()
{
NSCache *_memoryCache;
dispatch_queue_t _queue;
NSURL *_url;
}
@end
@implementation TGFileCache
- (instancetype)init
{
return [self initWithName:nil useMemoryCache:true];
}
- (instancetype)initWithName:(NSString *)name useMemoryCache:(bool)useMemoryCache
{
self = [super init];
if (self != nil)
{
if (useMemoryCache)
_memoryCache = [[NSCache alloc] init];
_queue = dispatch_queue_create(TGFileCacheDomain.UTF8String, nil);
_url = [NSURL fileURLWithPath:name relativeToURL:[TGFileCache baseURL]];
dispatch_async(_queue, ^
{
if (![[NSFileManager defaultManager] fileExistsAtPath:_url.path])
{
NSError *error;
[[NSFileManager defaultManager] createDirectoryAtURL:_url withIntermediateDirectories:true attributes:nil error:&error];
}
});
}
return self;
}
- (void)fetchDataForKey:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(id))completion
{
[self fetchDataForKey:key memoryOnly:false synchronous:synchronous unserializeBlock:unserializeBlock completion:completion];
}
- (void)fetchDataForKey:(NSString *)key memoryOnly:(bool)memoryOnly synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(id))completion
{
if (completion == nil)
return;
void (^block)(void) = ^
{
id cachedObject = [_memoryCache objectForKey:key];
if (cachedObject != nil)
{
completion(cachedObject);
return;
}
if (!memoryOnly)
{
NSData *data = [[NSData alloc] initWithContentsOfURL:[self urlForKey:key] options:kNilOptions error:nil];
if (data.length > 0)
{
id result = data;
if (unserializeBlock != nil)
{
result = unserializeBlock(data);
[_memoryCache setObject:result forKey:key];
}
completion(result);
return;
}
}
completion(nil);
};
if (synchronous)
dispatch_sync(_queue, block);
else
dispatch_async(_queue, block);
}
- (void)cacheData:(NSData *)data key:(NSString *)key synchronous:(bool)synchronous completion:(void (^)(NSURL *))completion
{
[self cacheData:data key:key synchronous:synchronous serializeBlock:nil completion:completion];
}
- (void)cacheData:(NSObject<NSCoding> *)data key:(NSString *)key synchronous:(bool)synchronous serializeBlock:(NSData *(^)(NSObject<NSCoding> *))serializeBlock completion:(void (^)(NSURL *))completion
{
void (^block)(void) = ^
{
NSURL *url = [self urlForKey:key];
NSData *serializedData = nil;
if (serializeBlock != nil)
serializedData = serializeBlock(data);
else if ([data isKindOfClass:[NSData class]])
serializedData = (NSData *)data;
[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
[serializedData writeToURL:url atomically:true];
if (completion != nil)
completion(url);
};
if (synchronous)
dispatch_sync(_queue, block);
else
dispatch_async(_queue, block);
}
- (void)cacheFileAtURL:(NSURL *)url key:(NSString *)key synchronous:(bool)synchronous completion:(void (^)(NSURL *))completion
{
[self cacheFileAtURL:url key:key synchronous:synchronous unserializeBlock:nil completion:completion];
}
- (void)cacheFileAtURL:(NSURL *)url key:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(NSURL *))completion
{
void (^block)(void) = ^
{
NSURL *newUrl = [self urlForKey:key];
[[NSFileManager defaultManager] copyItemAtURL:url toURL:newUrl error:NULL];
if (completion != nil)
completion(newUrl);
if (unserializeBlock != nil && _memoryCache != nil)
{
NSData *data = [NSData dataWithContentsOfURL:url];
id result = unserializeBlock(data);
[_memoryCache setObject:result forKey:key];
}
};
if (synchronous)
dispatch_sync(_queue, block);
else
dispatch_async(_queue, block);
}
- (void)cacheData:(NSData *)data key:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(NSURL *))completion
{
void (^block)(void) = ^
{
NSURL *newUrl = [self urlForKey:key];
[data writeToURL:newUrl atomically:true];
if (completion != nil)
completion(newUrl);
if (unserializeBlock != nil && _memoryCache != nil)
{
id result = unserializeBlock(data);
[_memoryCache setObject:result forKey:key];
}
};
if (synchronous)
dispatch_sync(_queue, block);
else
dispatch_async(_queue, block);
}
- (void)clearCacheSynchronous:(bool)synchronous
{
void (^block)(void) = ^
{
NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:_url includingPropertiesForKeys:nil options:kNilOptions error:NULL];
for (NSURL *url in contents)
[[NSFileManager defaultManager] removeItemAtURL:url error:NULL];
};
if (synchronous)
dispatch_sync(_queue, block);
else
dispatch_async(_queue, block);
}
- (bool)hasDataForKey:(NSString *)key
{
return [[NSFileManager defaultManager] fileExistsAtPath:[self urlForKey:key].path];
}
- (NSURL *)urlForKey:(NSString *)key
{
NSString *fileName = [TGStringUtils md5WithString:key];
if (self.defaultFileExtension != nil)
fileName = [fileName stringByAppendingPathExtension:self.defaultFileExtension];
return [NSURL fileURLWithPath:[_url.path stringByAppendingPathComponent:fileName]];
}
+ (NSURL *)baseURL
{
static dispatch_once_t onceToken;
static NSURL *baseURL;
dispatch_once(&onceToken, ^
{
NSString *cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, true)[0];
baseURL = [[NSURL alloc] initFileURLWithPath:[cachesPath stringByAppendingPathComponent:TGFileCacheDomain]];
});
return baseURL;
}
@end
+10
View File
@@ -0,0 +1,10 @@
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
@interface TGGeometry : NSObject
CGSize TGFitSize(CGSize size, CGSize maxSize);
CGSize TGFillSize(CGSize size, CGSize maxSize);
CGSize TGScaleToFill(CGSize size, CGSize boundsSize);
@end
+56
View File
@@ -0,0 +1,56 @@
#import "TGGeometry.h"
@implementation TGGeometry
CGSize TGFitSize(CGSize size, CGSize maxSize)
{
if (size.width < 1.0f)
return CGSizeZero;
if (size.height < 1.0f)
return CGSizeZero;
if (size.width > maxSize.width)
{
size.height = (CGFloat)floor((size.height * maxSize.width / size.width));
size.width = maxSize.width;
}
if (size.height > maxSize.height)
{
size.width = (CGFloat)floor((size.width * maxSize.height / size.height));
size.height = maxSize.height;
}
return size;
}
CGSize TGFillSize(CGSize size, CGSize maxSize)
{
if (size.width < 1)
size.width = 1;
if (size.height < 1)
size.height = 1;
if (/*size.width >= size.height && */size.width < maxSize.width)
{
size.height = floor(maxSize.width * size.height / MAX(1.0f, size.width));
size.width = maxSize.width;
}
if (/*size.width <= size.height &&*/ size.height < maxSize.height)
{
size.width = floor(maxSize.height * size.width / MAX(1.0f, size.height));
size.height = maxSize.height;
}
return size;
}
CGSize TGScaleToFill(CGSize size, CGSize boundsSize)
{
if (size.width < 1.0f || size.height < 1.0f)
return CGSizeMake(1.0f, 1.0f);
CGFloat scale = MAX(boundsSize.width / size.width, boundsSize.height / size.height);
return CGSizeMake(floor(size.width * scale), floor(size.height * scale));
}
@end
@@ -0,0 +1,20 @@
#import "TGInterfaceController.h"
@class TGBridgeContext;
@class TGBridgeChat;
@interface TGGroupInfoControllerContext : NSObject <TGInterfaceContext>
@property (nonatomic, strong) TGBridgeContext *context;
@property (nonatomic, readonly) TGBridgeChat *groupChat;
- (instancetype)initWithGroupChat:(TGBridgeChat *)groupChat;
@end
@interface TGGroupInfoController : TGInterfaceController
@property (nonatomic, weak) IBOutlet WKInterfaceTable *table;
@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator;
@end
@@ -0,0 +1,335 @@
#import "TGGroupInfoController.h"
#import <WatchCommonWatch/WatchCommonWatch.h>
#import "TGWatchCommon.h"
#import "TGStringUtils.h"
#import "TGBridgeConversationSignals.h"
#import "TGBridgePeerSettingsSignals.h"
#import "WKInterfaceTable+TGDataDrivenTable.h"
#import "TGTableDeltaUpdater.h"
#import "TGInterfaceMenu.h"
#import "TGGroupInfoHeaderController.h"
#import "TGGroupInfoFooterController.h"
#import "TGUserRowController.h"
#import "TGInputController.h"
#import "TGUserInfoController.h"
#import "TGContactsController.h"
#import "TGProfilePhotoController.h"
NSString *const TGGroupInfoControllerIdentifier = @"TGGroupInfoController";
@implementation TGGroupInfoControllerContext
- (instancetype)initWithGroupChat:(TGBridgeChat *)groupChat
{
self = [super init];
if (self != nil)
{
_groupChat = groupChat;
}
return self;
}
@end
@interface TGGroupInfoController () <TGTableDataSource>
{
SMetaDisposable *_chatDisposable;
SMetaDisposable *_peerSettingsDisposable;
SMetaDisposable *_updateSettingsDisposable;
TGInterfaceMenu *_menu;
TGGroupInfoControllerContext *_context;
TGBridgeChat *_chatModel;
NSDictionary *_userModels;
NSArray *_participantsModels;
NSArray *_currentParticipantsModels;
bool _muted;
NSDictionary *_preferredParticipantsOrder;
}
@end
@implementation TGGroupInfoController
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_chatDisposable = [[SMetaDisposable alloc] init];
_peerSettingsDisposable = [[SMetaDisposable alloc] init];
_updateSettingsDisposable = [[SMetaDisposable alloc] init];
[self.table _setInitialHidden:true];
self.table.tableDataSource = self;
}
return self;
}
- (void)dealloc
{
[_chatDisposable dispose];
[_peerSettingsDisposable dispose];
[_updateSettingsDisposable dispose];
}
- (void)configureWithContext:(TGGroupInfoControllerContext *)context
{
_context = context;
self.title = TGLocalized(@"Watch.GroupInfo.Title");
__weak TGGroupInfoController *weakSelf = self;
[_chatDisposable setDisposable:[[[TGBridgeConversationSignals conversationWithPeerId:_context.groupChat.identifier] deliverOn:[SQueue mainQueue]] startWithNext:^(NSDictionary *models)
{
__strong TGGroupInfoController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_chatModel = models[TGBridgeChatKey];
strongSelf->_userModels = models[TGBridgeUsersDictionaryKey];
NSMutableArray *participantsModels = [[NSMutableArray alloc] init];
for (NSNumber *uid in strongSelf->_chatModel.participants)
{
TGBridgeUser *user = strongSelf->_userModels[uid];
if (user != nil)
[participantsModels addObject:user];
}
participantsModels = [TGGroupInfoController sortedParticipantsList:participantsModels preferredOrder:strongSelf->_preferredParticipantsOrder ownUid:strongSelf->_context.context.userId];
strongSelf->_preferredParticipantsOrder = [TGGroupInfoController participantsOrderForList:participantsModels];
strongSelf->_participantsModels = participantsModels;
[strongSelf performInterfaceUpdate:^(bool animated)
{
strongSelf.activityIndicator.hidden = true;
strongSelf.table.hidden = false;
NSArray *currentParticipantsModels = strongSelf->_currentParticipantsModels;
bool initial = (currentParticipantsModels == 0);
strongSelf->_currentParticipantsModels = strongSelf->_participantsModels;
if (animated && !initial)
{
[TGTableDeltaUpdater updateTable:strongSelf.table oldData:currentParticipantsModels newData:strongSelf->_currentParticipantsModels controllerClassForIndexPath:^Class(TGIndexPath *indexPath)
{
return [strongSelf table:strongSelf->_table rowControllerClassAtIndexPath:indexPath];
}];
[strongSelf.table reloadHeader];
[strongSelf.table reloadFooter];
}
else
{
[strongSelf.table reloadData];
}
}];
} error:^(id error)
{
} completed:^
{
}]];
[_peerSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals peerSettingsWithPeerId:_context.groupChat.identifier] deliverOn:[SQueue mainQueue]] startWithNext:^(NSDictionary *next)
{
__strong TGGroupInfoController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
bool muted = [next[@"muted"] boolValue];
if (strongSelf->_menu == nil || muted != strongSelf->_muted)
{
strongSelf->_muted = muted;
[strongSelf performInterfaceUpdate:^(bool animated)
{
[strongSelf updateMenuItemsMuted:strongSelf->_muted];
}];
}
}]];
}
- (void)willActivate
{
[super willActivate];
[self.table notifyVisiblityChange];
}
- (void)didDeactivate
{
[super didDeactivate];
}
#pragma mark -
- (void)updateMenuItemsMuted:(bool)muted
{
[_menu clearItems];
if (_menu == nil)
_menu = [[TGInterfaceMenu alloc] initForInterfaceController:self];
__weak TGGroupInfoController *weakSelf = self;
NSMutableArray *menuItems = [[NSMutableArray alloc] init];
bool muteForever = true;
int32_t muteFor = muteForever ? INT_MAX : 1;
NSString *muteTitle = muteForever ? TGLocalized(@"Watch.UserInfo.MuteTitle") : [NSString stringWithFormat:TGLocalized([TGStringUtils integerValueFormat:@"Watch.UserInfo.Mute_" value:muteFor]), muteFor];
TGInterfaceMenuItem *muteItem = [[TGInterfaceMenuItem alloc] initWithItemIcon:muted ? WKMenuItemIconSpeaker : WKMenuItemIconMute title:muted ? TGLocalized(@"Watch.UserInfo.Unmute") : muteTitle actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender)
{
__strong TGGroupInfoController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
[strongSelf->_updateSettingsDisposable setDisposable:[[TGBridgePeerSettingsSignals toggleMutedWithPeerId:strongSelf->_context.groupChat.identifier] startWithNext:nil completed:^
{
__strong TGGroupInfoController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
strongSelf->_muted = !muted;
[strongSelf updateMenuItemsMuted:strongSelf->_muted];
}]];
}];
[menuItems addObject:muteItem];
[_menu addItems:menuItems];
}
#pragma mark -
- (Class)headerControllerClassForTable:(WKInterfaceTable *)table
{
return [TGGroupInfoHeaderController class];
}
- (void)table:(WKInterfaceTable *)table updateHeaderController:(TGGroupInfoHeaderController *)controller
{
__weak TGGroupInfoController *weakSelf = self;
controller.isVisible = ^bool
{
__strong TGGroupInfoController *strongSelf = weakSelf;
if (strongSelf == nil)
return false;
return strongSelf.isVisible;
};
controller.avatarPressed = ^
{
__strong TGGroupInfoController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
TGProfilePhotoControllerContext *context = [[TGProfilePhotoControllerContext alloc] initWithIdentifier:strongSelf->_chatModel.identifier imageUrl:strongSelf->_chatModel.groupPhotoSmall];
[strongSelf pushControllerWithClass:[TGProfilePhotoController class] context:context];
};
[controller updateWithGroupChat:_chatModel users:_userModels context:_context.context];
}
- (void)table:(WKInterfaceTable *)table updateFooterController:(TGGroupInfoFooterController *)controller
{
}
- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(NSIndexPath *)indexPath
{
return [TGUserRowController class];
}
- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section
{
return _currentParticipantsModels.count;
}
- (void)table:(WKInterfaceTable *)table updateRowController:(TGUserRowController *)controller forIndexPath:(TGIndexPath *)indexPath
{
__weak TGGroupInfoController *weakSelf = self;
controller.isVisible = ^bool
{
__strong TGGroupInfoController *strongSelf = weakSelf;
if (strongSelf == nil)
return false;
return strongSelf.isVisible;
};
[controller updateWithUser:_currentParticipantsModels[indexPath.row] context:_context.context];
}
- (id<TGInterfaceContext>)contextForSegueWithIdentifer:(NSString *)segueIdentifier table:(WKInterfaceTable *)table indexPath:(TGIndexPath *)indexPath
{
return [[TGUserInfoControllerContext alloc] initWithUser:_currentParticipantsModels[indexPath.row]];
}
+ (NSMutableArray *)sortedParticipantsList:(NSMutableArray *)list preferredOrder:(NSDictionary *)preferredOrder ownUid:(int32_t)ownUid
{
NSMutableArray *resultList = [list mutableCopy];
[resultList sortUsingComparator:^NSComparisonResult(TGBridgeUser *user1, TGBridgeUser *user2)
{
if (user1.identifier == ownUid)
return NSOrderedAscending;
else if (user2.identifier == ownUid)
return NSOrderedDescending;
NSNumber *order1 = preferredOrder[@(user1.identifier)];
NSNumber *order2 = preferredOrder[@(user2.identifier)];
if (order1 != nil && order2 != nil)
return order1.integerValue < order2.integerValue ? NSOrderedAscending : NSOrderedDescending;
if (user1.online != user2.online)
return user1.online ? NSOrderedAscending : NSOrderedDescending;
if ((user1.lastSeen < 0) != (user2.lastSeen < 0))
return user1.lastSeen >= 0 ? NSOrderedAscending : NSOrderedDescending;
if (user1.online || user1.lastSeen < 0)
return user1.identifier < user2.identifier ? NSOrderedAscending : NSOrderedDescending;
return user1.lastSeen > user2.lastSeen ? NSOrderedAscending : NSOrderedDescending;
}];
return resultList;
}
+ (NSDictionary *)participantsOrderForList:(NSArray *)list
{
NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init];
NSInteger i = 0;
for (TGBridgeUser *user in list)
{
dictionary[@(user.identifier)] = @(i);
i++;
}
return dictionary;
}
#pragma mark -
+ (NSString *)identifier
{
return TGGroupInfoControllerIdentifier;
}
@end
@@ -0,0 +1,10 @@
#import <WatchKit/WatchKit.h>
@interface TGGroupInfoFooterController : NSObject
@property (nonatomic, weak) IBOutlet WKInterfaceButton *button;
- (IBAction)buttonPressedAction;
@property (nonatomic, copy) void (^buttonPressed)(void);
@end

Some files were not shown because too many files have changed in this diff Show More