|
| 1 | +import Foundation |
| 2 | +protocol AccountContextProtocol: AnyObject { var engine: Engine { get }; var account: Account { get } } |
| 3 | +enum NoError: Error {} |
| 4 | +struct Signal<T, E> { let value: T; static func single(_ value: T) -> Self { Self(value: value) }; func start() {} } |
| 5 | +precedencegroup PipePrecedence { associativity: left } |
| 6 | +infix operator |> : PipePrecedence |
| 7 | +func |> <A, B>(value: A, transform: (A) -> B) -> B { transform(value) } |
| 8 | +func map<A, B, E>(_ f: @escaping (A) -> B) -> (Signal<A, E>) -> Signal<B, E> { { .single(f($0.value)) } } |
| 9 | +func mapToSignal<A, B, E>(_ f: @escaping (A) -> Signal<B, E>) -> (Signal<A, E>) -> Signal<B, E> { { f($0.value) } } |
| 10 | +func deliverOnMainQueue<A, E>(_ signal: Signal<A, E>) -> Signal<A, E> { signal } |
| 11 | +struct PeerId: Equatable { let namespace: Int; let id: Int } |
| 12 | +enum Namespaces { enum Peer { static let SecretChat = 3 } } |
| 13 | +class Peer { let id: PeerId; var isForumOrMonoForum: Bool; init(_ id: PeerId, forum: Bool = false) { self.id=id; isForumOrMonoForum=forum } } |
| 14 | +struct ChannelFlags: OptionSet { let rawValue: Int; static let displayForumAsTabs = ChannelFlags(rawValue: 1) } |
| 15 | +class TelegramChannel: Peer { var flags: ChannelFlags = [] } |
| 16 | +struct KnownBool { var knownValue: Bool? } |
| 17 | +struct CachedChannelData { var viewForumAsMessages = KnownBool() } |
| 18 | +struct CachedItem { let id: PeerId } |
| 19 | +struct EnginePeer { let peer: Peer; func _asPeer() -> Peer { peer } } |
| 20 | +struct ReadState { var count=0; var marked=false; var isUnread: Bool { count>0 || marked } } |
| 21 | +enum Group { case root, archive } |
| 22 | +enum SearchControllerTarget { case common(Group), forum(PeerId), savedMessages(PeerId), community(PeerId) } |
| 23 | +struct ReadItem { let id: PeerId } |
| 24 | +struct GroupItem { let id: PeerId } |
| 25 | +struct PeerItem { let id: PeerId } |
| 26 | +enum TelegramEngine { enum EngineData { enum Item { enum Messages { typealias PeerReadCounters = ReadItem; typealias ChatListGroup = GroupItem }; enum Peer { typealias Peer = PeerItem; typealias CachedData = CachedItem } } } } |
| 27 | +final class DataEngine { |
| 28 | + var cached: Any?; var state=ReadState(); var group: Group? = .root; var peer: EnginePeer?; var requests: [PeerId]=[] |
| 29 | + func get(_ read: ReadItem, _ group: GroupItem, _ peer: PeerItem, _ cached: CachedItem) -> Signal<(ReadState, Group?, EnginePeer?, Any?), NoError> { requests += [read.id,group.id,peer.id,cached.id]; return .single((state,self.group,self.peer,self.cached)) } |
| 30 | + func get(_ read: ReadItem, _ group: GroupItem, _ peer: PeerItem) -> Signal<(ReadState, Group?, EnginePeer?), NoError> { requests += [read.id,group.id,peer.id]; return .single((state,self.group,self.peer)) } |
| 31 | +} |
| 32 | +final class Peers { var removed: [PeerId]=[]; func removeRecentPeer(peerId: PeerId) -> Signal<Int, NoError> { removed.append(peerId); return .single(0) } } |
| 33 | +final class Engine { let data=DataEngine(); let peers=Peers() } |
| 34 | +final class Transaction { |
| 35 | + var rawState = ReadState() |
| 36 | + func getCombinedPeerReadState(_ id: PeerId) -> ReadState? { rawState } |
| 37 | +} |
| 38 | +final class Postbox { |
| 39 | + let data = Transaction() |
| 40 | + func transaction<T>(_ body: (Transaction) -> T) -> Signal<T, NoError> { .single(body(data)) } |
| 41 | +} |
| 42 | +final class Account { let postbox = Postbox() } |
| 43 | +final class Context: AccountContextProtocol { let account = Account(); let engine=Engine(); var calls: [(PeerId,Bool)]=[] } |
| 44 | +struct Labels { let chatListContextMaskAsRead="Read"; let chatListContextMaskAsUnread="Unread"; let searchPopularDelete="Remove" } |
| 45 | +func strings() -> Labels { Labels() } |
| 46 | +enum MenuAnimation { case menu_read, menu_unread, menu_delete; var value: Int { switch self { case .menu_read:0;case .menu_unread:1;case .menu_delete:2 } } } |
| 47 | +enum ItemMode { case destruct } |
| 48 | +class ContextMenuItem { let title: String; let handler:(()->Void)?; let image: Int?; init(_ title:String,handler:(()->Void)?=nil,itemMode: ItemMode?=nil,itemImage:Int?=nil) { self.title=title;self.handler=handler;image=itemImage } } |
| 49 | +final class ContextSeparatorItem: ContextMenuItem { init() { super.init("separator") } } |
| 50 | +enum ChatListRowItem { |
| 51 | + static func markAsRead(context:any AccountContextProtocol,peerId:PeerId) { (context as! Context).calls.append((peerId,false)); context.account.postbox.data.rawState.marked = false } |
| 52 | + static func markAsUnread(context:any AccountContextProtocol,peerId:PeerId) { (context as! Context).calls.append((peerId,true)); context.account.postbox.data.rawState.marked = true } |
| 53 | +} |
| 54 | +enum PopularItemType { case peer(Peer,Int?,Bool), savedMessages(Peer), articles(Int32) } |
| 55 | +final class Popular { |
| 56 | + let context: Context; let type: PopularItemType |
| 57 | + init(_ context:Context,_ type:PopularItemType) { self.context=context;self.type=type } |
| 58 | + func menuItems() -> Signal<[ContextMenuItem],NoError> { __POPULAR_BODY__ } |
| 59 | +} |
| 60 | +__HELPER__ |
| 61 | +__LEASE__ |
| 62 | +var count=0, failures=0 |
| 63 | +func check(_ name:String,_ pass:Bool) {count+=1;if !pass {failures+=1;print("FAIL: "+name)} } |
| 64 | +let context=Context(); let id=PeerId(namespace:0,id:42);let peer=Peer(id);context.engine.data.peer=EnginePeer(peer:peer) |
| 65 | +for group in [Group.root,.archive] { |
| 66 | + context.engine.data.group=group |
| 67 | + for state in [ReadState(count:0,marked:false),ReadState(count:3,marked:false),ReadState(count:0,marked:true)] { |
| 68 | + context.engine.data.state=state;context.calls=[] |
| 69 | + let item=searchPeerReadMenuItem(context:context,peerId:id).value |
| 70 | + check("current read state determines title",item?.title == (state.isUnread ? "Read":"Unread")) |
| 71 | + check("standard read/unread icon",item?.image == (state.isUnread ? 0:1)) |
| 72 | + item?.handler?() |
| 73 | + check("explicit action targets original chat",context.calls.count == 1 && context.calls[0].0 == id && context.calls[0].1 == !state.isUnread) |
| 74 | + } |
| 75 | +} |
| 76 | +context.engine.data.group=nil |
| 77 | +check("global peer outside chat list has no read action",searchPeerReadMenuItem(context:context,peerId:id).value == nil) |
| 78 | +context.engine.data.group = .root;context.engine.data.peer=nil |
| 79 | +check("missing peer has no read action",searchPeerReadMenuItem(context:context,peerId:id).value == nil) |
| 80 | +context.engine.data.peer=EnginePeer(peer:peer);context.engine.data.requests=[] |
| 81 | +check("secret chat has no unsupported unread action",searchPeerReadMenuItem(context:context,peerId:PeerId(namespace:3,id:99)).value == nil) |
| 82 | +check("secret chat does not look up the visible user's read state",context.engine.data.requests.isEmpty) |
| 83 | +peer.isForumOrMonoForum=true;context.engine.data.state=ReadState() |
| 84 | +check("read forum does not offer an action that would clear topics",searchPeerReadMenuItem(context:context,peerId:id).value == nil) |
| 85 | +context.engine.data.state=ReadState(count:2) |
| 86 | +check("unread forum can be marked read",searchPeerReadMenuItem(context:context,peerId:id).value?.title == "Read") |
| 87 | +peer.isForumOrMonoForum=false;context.engine.data.state=ReadState() |
| 88 | +let top=Popular(context,.peer(peer,nil,false)).menuItems().value |
| 89 | +check("top peer has unread action",top.first?.title == "Unread") |
| 90 | +check("top peer retains remove action",top.last?.title == "Remove") |
| 91 | +check("top peer does not remove on menu construction",context.engine.peers.removed.isEmpty) |
| 92 | +check("Saved Messages has read action",Popular(context,.savedMessages(peer)).menuItems().value.first?.title == "Unread") |
| 93 | +check("articles do not acquire chat read action",Popular(context,.articles(2)).menuItems().value.isEmpty) |
| 94 | +context.engine.data.state=ReadState(marked:true) |
| 95 | +check("reopened top peer uses live manual unread mark",Popular(context,.peer(peer,nil,false)).menuItems().value.first?.title == "Read") |
| 96 | +peer.isForumOrMonoForum = true |
| 97 | +context.engine.data.state = ReadState() |
| 98 | +context.engine.data.cached = CachedChannelData(viewForumAsMessages: KnownBool(knownValue: true)) |
| 99 | +check("forum displayed as messages retains Mark as Unread", searchPeerReadMenuItem(context: context, peerId: id).value?.title == "Unread") |
| 100 | +context.engine.data.cached = nil |
| 101 | +let tabsForum = TelegramChannel(id, forum: true) |
| 102 | +tabsForum.flags = [.displayForumAsTabs] |
| 103 | +context.engine.data.peer = EnginePeer(peer: tabsForum) |
| 104 | +check("forum displayed as tabs retains Mark as Unread", searchPeerReadMenuItem(context: context, peerId: id).value?.title == "Unread") |
| 105 | +context.engine.data.state = ReadState() |
| 106 | +context.account.postbox.data.rawState = ReadState(marked: true) |
| 107 | +let markedForum = searchPeerReadMenuItem(context: context, peerId: id).value |
| 108 | +check("regular-chat forum reads its peer-level manual unread mark", markedForum?.title == "Read") |
| 109 | +markedForum?.handler?() |
| 110 | +check("Read action clears the forum peer-level mark", !context.account.postbox.data.rawState.marked) |
| 111 | +check("reopened forum menu reflects cleared mark", searchPeerReadMenuItem(context: context, peerId: id).value?.title == "Unread") |
| 112 | +searchPeerReadMenuItem(context: context, peerId: id).value?.handler?() |
| 113 | +check("Unread action can set the peer-level mark again", context.account.postbox.data.rawState.marked) |
| 114 | +check("forum round trip returns Mark as Read", searchPeerReadMenuItem(context: context, peerId: id).value?.title == "Read") |
| 115 | +context.engine.data.requests=[]; context.calls=[] |
| 116 | +check("Saved Messages sub-chat search omits the real peer read action",searchPeerReadMenuItem(context:context,peerId:id,target:.savedMessages(id)).value == nil) |
| 117 | +check("Saved Messages sub-chat search never reads private-chat counters",context.engine.data.requests.isEmpty) |
| 118 | +check("Saved Messages sub-chat menu cannot send read receipts",context.calls.isEmpty) |
| 119 | +var firstWindow: NSObject?=NSObject() |
| 120 | +weak var retainedRun=firstWindow |
| 121 | +check("first diagnostic window acquires the run",SearchReadRunLease.begin(firstWindow!)) |
| 122 | +firstWindow=nil |
| 123 | +check("closing the owner window preserves the active restore operation",retainedRun != nil) |
| 124 | +let reopenedWindow=NSObject() |
| 125 | +check("reopened window cannot overlap the restore run",!SearchReadRunLease.begin(reopenedWindow)) |
| 126 | +SearchReadRunLease.end(reopenedWindow) |
| 127 | +check("another window cannot release the active lease",!SearchReadRunLease.begin(reopenedWindow)) |
| 128 | +SearchReadRunLease.end(retainedRun!) |
| 129 | +check("finishing releases the old run owner",retainedRun == nil) |
| 130 | +check("a later window can start after restoration",SearchReadRunLease.begin(reopenedWindow)) |
| 131 | +SearchReadRunLease.end(reopenedWindow) |
| 132 | +print("\(count-failures)/\(count) search unread checks passed") |
| 133 | +exit(failures==0 ? 0:1) |
0 commit comments