From 65f34358655565955f1886089d24c86a5174c858 Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Fri, 3 Sep 2021 15:54:02 -0400 Subject: [PATCH 01/16] Added pre-stream waitroom --- ios/Views/StreamView/StreamView.swift | 114 ++++++++++++++------------ 1 file changed, 62 insertions(+), 52 deletions(-) diff --git a/ios/Views/StreamView/StreamView.swift b/ios/Views/StreamView/StreamView.swift index 5c7e25f..45489f9 100644 --- a/ios/Views/StreamView/StreamView.swift +++ b/ios/Views/StreamView/StreamView.swift @@ -26,6 +26,7 @@ class StreamView: BaseController { var waitRoom = false let waitTimeText = UILabel() let waitTimeScheduled = UILabel() + var videoLoaded: Bool = false let chatTable = ChatTable(frame: .zero, style: .plain) let chatControl: UISegmentedControl @@ -92,6 +93,7 @@ class StreamView: BaseController { } } let playerItem = AVPlayerItem(url: streamURL!) + videoLoaded = true player?.replaceCurrentItem(with: playerItem) videoPlayer.player = player @@ -199,57 +201,65 @@ class StreamView: BaseController { override func handle(_ error: Error) { let nserror = error as NSError -// if nserror.code == -2, waitRoom == false { -// waitRoom = true -// waitRoomView.kf.setImage(with: URL(string: "https://i.ytimg.com/vi/\(videoID)/maxresdefault.jpg"), options: [.cacheOriginalImage]) { result in -// switch result { -// case .failure: do { -// self.waitRoomView.kf.setImage(with: URL(string: "https://i.ytimg.com/vi/\(self.videoID)/mqdefault.jpg"), options: [.cacheOriginalImage]) -// } -// case .success: -// break -// } -// } -// // Get Timestamp -// let startStringTimestamp = nserror.userInfo["startTimestamp"] as! String -// let startTimestamp = Date(timeIntervalSince1970: Double(startStringTimestamp)!) -// let interval = DateInterval(start: Date(), end: startTimestamp) -// -// // Create Countdown Timer -// let countDown = Int(interval.duration) -// Observable.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance) -// .take(countDown + 1) -// .subscribe(onNext: { timePassed in -// let count = countDown - timePassed -// let h = String(format: "%02d", count / 3600) -// let m = String(format: "%02d", (count % 3600) / 60) -// let s = String(format: "%02d", (count % 3600) % 60) -// var timer = "\(m):\(s)" -// if h != "00" { -// timer = "\(h):\(timer)" -// } -// self.waitTimeText.text = "Live in " + timer -// -// }, onCompleted: { -// self.waitTimeText.text = "Waiting on stream to start" -// }) -// .disposed(by: bag) -// waitTimeText.font = .systemFont(ofSize: 19) -// waitTimeText.textColor = .white -// -// let dateFormatter = DateFormatter() -// dateFormatter.dateStyle = .long -// dateFormatter.timeStyle = .medium -// dateFormatter.timeZone = .current -// dateFormatter.locale = .current -// waitTimeScheduled.text = dateFormatter.string(from: startTimestamp) -// waitTimeScheduled.font = .systemFont(ofSize: 15) -// waitTimeScheduled.textColor = .white -// -// waitRoomTextView.addSubview(waitTimeText) -// waitRoomTextView.addSubview(waitTimeScheduled) -// model.input.loadPreviewChat(videoID, duration: 0) -// } + if nserror.code == -2, waitRoom == false { + waitRoom = true + waitRoomView.kf.setImage(with: URL(string: "https://i.ytimg.com/vi/\(videoID)/maxresdefault.jpg"), options: [.cacheOriginalImage]) { result in + switch result { + case .failure: do { + self.waitRoomView.kf.setImage(with: URL(string: "https://i.ytimg.com/vi/\(self.videoID)/mqdefault.jpg"), options: [.cacheOriginalImage]) + } + case .success: + break + } + } + // Get Timestamp + let startStringTimestamp = nserror.userInfo["startTimestamp"] as! String + let startTimestamp = Date(timeIntervalSince1970: Double(startStringTimestamp)!) + let interval = DateInterval(start: Date(), end: startTimestamp) + + // try to load the video until it loads + Observable.timer(.seconds(0), period: .seconds(10), scheduler: MainScheduler.instance) + .take(until: { _ in self.videoLoaded }) + .subscribe(onNext: {_ in self.load(self.videoID)}, onCompleted: { + print("Video loaded") + + }) + .disposed(by: bag) + + // Create Countdown Timer + let countDown = Int(interval.duration) + Observable.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance) + .take(countDown + 1) + .subscribe(onNext: { timePassed in + let count = countDown - timePassed + let h = String(format: "%02d", count / 3600) + let m = String(format: "%02d", (count % 3600) / 60) + let s = String(format: "%02d", (count % 3600) % 60) + var timer = "\(m):\(s)" + if h != "00" { + timer = "\(h):\(timer)" + } + self.waitTimeText.text = "Live in " + timer + }, onCompleted: { + self.waitTimeText.text = "Waiting on stream to start..." + }) + .disposed(by: bag) + waitTimeText.font = .systemFont(ofSize: 19) + waitTimeText.textColor = .white + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .long + dateFormatter.timeStyle = .medium + dateFormatter.timeZone = .current + dateFormatter.locale = .current + waitTimeScheduled.text = dateFormatter.string(from: startTimestamp) + waitTimeScheduled.font = .systemFont(ofSize: 15) + waitTimeScheduled.textColor = .white + + waitRoomTextView.addSubview(waitTimeText) + waitRoomTextView.addSubview(waitTimeScheduled) + model.input.loadPreviewChat(videoID, duration: 0) + } if nserror.code == -6, nserror.userInfo["consentHtmlData"] as? String != nil { closeStream() @@ -272,7 +282,7 @@ class StreamView: BaseController { } alert.showError(Bundle.main.localizedString(forKey: "An Error Occurred", value: "An Error Occurred", table: "Localizeable"), subTitle: error.localizedDescription + " You'll need to join the channel from youtube.com or the YouTube app. If this is in error, try logging out and logging in again.") } - } else { //if !nserror.localizedDescription.starts(with: "This live event will begin in") { + } else if !nserror.localizedDescription.starts(with: "This live event will begin in") { let alert = SCLAlertView() alert.addButton(Bundle.main.localizedString(forKey: "Go Back", value: "Go Back", table: "Localizeable")) { self.closeStream() From 7b4568b6cfe6e9975da6cec91fe883d7a805b98e Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Fri, 3 Sep 2021 15:54:53 -0400 Subject: [PATCH 02/16] timed messages WIP --- ios/Core/Models/StreamModel.swift | 32 ++++++++++++++++++------- ios/Core/Types/DisplayableMessage.swift | 2 +- ios/Core/Types/InjectedMessage.swift | 2 +- ios/Core/Types/TranslatedMessage.swift | 2 +- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/ios/Core/Models/StreamModel.swift b/ios/Core/Models/StreamModel.swift index 1804443..8ac0239 100644 --- a/ios/Core/Models/StreamModel.swift +++ b/ios/Core/Models/StreamModel.swift @@ -48,6 +48,7 @@ class StreamModel: BaseModel { private let controlRelay = BehaviorRelay(value: .allChat) + private let rawChatRelay = BehaviorRelay<[DisplayableMessage]>(value: []) private let chatRelay = BehaviorRelay<[DisplayableMessage]>(value: []) private let liveRelay = BehaviorRelay<[DisplayableMessage]>(value: []) private let translatedRelay = BehaviorRelay<[DisplayableMessage]>(value: []) @@ -91,14 +92,27 @@ class StreamModel: BaseModel { self.liveRelay.accept([]) self.translatedRelay.accept([]) self.chatRelay.accept([]) + self.rawChatRelay.accept([]) }).disposed(by: bag) Observable.combineLatest(controlRelay, liveRelay, translatedRelay).map { control, live, translated in control == .allChat ? live : translated } .map { $0.sorted { $0.sortTimestamp > $1.sortTimestamp } } - .bind(to: chatRelay) + .bind(to: rawChatRelay) .disposed(by: bag) - + + Observable.timer(.milliseconds(0), period: .milliseconds(500), scheduler: MainScheduler.asyncInstance) + .subscribe { _ in + var messageQueue = self.rawChatRelay.value + var sendMessages: [DisplayableMessage] = self.chatRelay.value + print("\(messageQueue.first?.showTimestamp) <= \(Date())") + while messageQueue.first?.showTimestamp ?? Date.distantPast >= Date() { + sendMessages.append(messageQueue.first!) + messageQueue.removeFirst() + + self.chatRelay.accept(sendMessages) + } + }.disposed(by: bag) playerRelay.compactMap { $0 } .map { (id: $0.identifier, duration: $0.duration) } .subscribe(onNext: loadChat) @@ -180,12 +194,12 @@ class StreamModel: BaseModel { }) .disposed(by: bag) -// mchad.map { $0.element?.first } -// .bind(to: mchadRoomRelay) -// .disposed(by: bag) -// mchad.map { $0.error } -// .bind(to: errorRelay) -// .disposed(by: bag) + mchad.map { $0.element?.first } + .bind(to: mchadRoomRelay) + .disposed(by: bag) + mchad.map { $0.error } + .bind(to: errorRelay) + .disposed(by: bag) request.map { $0.element } .bind(to: chatURLRelay) @@ -348,9 +362,11 @@ extension StreamModel: StreamModelInput { func load(_ id: String) { loadVideoPlayer(id) } + func loadPreviewChat(_ id: String, duration: Double) { loadChat(id, duration: duration) } + func getMetadata(_ id: String) { getVideoMeta(id) } diff --git a/ios/Core/Types/DisplayableMessage.swift b/ios/Core/Types/DisplayableMessage.swift index 4b45181..071a16b 100644 --- a/ios/Core/Types/DisplayableMessage.swift +++ b/ios/Core/Types/DisplayableMessage.swift @@ -16,7 +16,7 @@ protocol DisplayableMessage { var superchatData : Superchat? { get } var sortTimestamp: Date { get } - var showTimestamp: Double { get } + var showTimestamp: Date { get } } extension DisplayableMessage { diff --git a/ios/Core/Types/InjectedMessage.swift b/ios/Core/Types/InjectedMessage.swift index 2141a06..dbf6cd2 100644 --- a/ios/Core/Types/InjectedMessage.swift +++ b/ios/Core/Types/InjectedMessage.swift @@ -48,5 +48,5 @@ extension InjectedMessage: DisplayableMessage { var superchatData: Superchat? { superchat } var sortTimestamp: Date { timestamp } - var showTimestamp: Double { showtime } + var showTimestamp: Date { Date(timeIntervalSinceNow: showtime/1000) } } diff --git a/ios/Core/Types/TranslatedMessage.swift b/ios/Core/Types/TranslatedMessage.swift index fe1df8d..b45873c 100644 --- a/ios/Core/Types/TranslatedMessage.swift +++ b/ios/Core/Types/TranslatedMessage.swift @@ -134,7 +134,7 @@ extension TranslatedMessage: DisplayableMessage { var superchatData: Superchat? { superchat } var sortTimestamp: Date { timestamp } - var showTimestamp: Double { show } + var showTimestamp: Date { Date(timeIntervalSinceNow: show/1000) } } extension String { From d2cca345599b7f7e4747dac491904d8541fa1e06 Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Thu, 9 Sep 2021 12:19:57 -0400 Subject: [PATCH 03/16] use 3 requests to gaurentee all catagories exist in HomeView --- ios/Core/Models/HomeModel.swift | 35 ++++++++++++++++++++----- ios/Core/Services/HoloDexServices.swift | 4 +-- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/ios/Core/Models/HomeModel.swift b/ios/Core/Models/HomeModel.swift index 3f46fb5..0005f5f 100644 --- a/ios/Core/Models/HomeModel.swift +++ b/ios/Core/Models/HomeModel.swift @@ -38,14 +38,17 @@ protocol HomeModelOutput { class HomeModel: BaseModel { let refresh = BehaviorRelay(value: ()) - private let streamers = BehaviorRelay(value: nil) + private var streamers = BehaviorRelay(value: nil) + private let liveStreamers = BehaviorRelay(value: nil) + private let upStreamers = BehaviorRelay(value: nil) + private let pastStreamers = BehaviorRelay(value: nil) private let refreshState = BehaviorRelay(value: false) override init(_ services: AppServices) { super.init(services) refresh.subscribe(onNext: { _ in self.loadStreamers(services.settings.orgFilter) }).disposed(by: bag) - streamers.compactMap { $0 }.distinctUntilChanged() + liveStreamers.compactMap { $0 }.distinctUntilChanged() .map { _ in false } .bind(to: refreshState) .disposed(by: bag) @@ -53,8 +56,28 @@ class HomeModel: BaseModel { func loadStreamers(_ org: Organization) { refreshState.accept(true) - services.holodex.streamers(org.description) + services.holodex.streamers(org.description, status: "live") .asObservable() + .bind(to: liveStreamers) + .disposed(by: bag) + services.holodex.streamers(org.description, status: "upcoming") + .asObservable() + .bind(to: upStreamers) + .disposed(by: bag) + services.holodex.streamers(org.description, status: "past") + .asObservable() + .bind(to: pastStreamers) + .disposed(by: bag) + + Observable.combineLatest(liveStreamers, upStreamers, pastStreamers) + .asObservable() + .map({ live, upcoming, past -> HoloDexResponse in + var combined: [HoloDexResponse.Streamer] = [] + combined.append(contentsOf: live?.items ?? []) + combined.append(contentsOf: upcoming?.items ?? []) + combined.append(contentsOf: past?.items ?? []) + return HoloDexResponse(items: combined) + }) .bind(to: streamers) .disposed(by: bag) } @@ -88,7 +111,7 @@ extension HomeModel: HomeModelOutput { func video(for section: Int, and index: Int) -> String { let r = streamers.value!.sections() - //return "x1EZYh8aGwA" + //return "kWTVKNhRmfg" return r[section].items[index].id } func thumbnail(for section: Int, and index: Int) -> URL? { @@ -123,14 +146,14 @@ extension HoloDexResponse { let l = items.filter { $0.status == .live }.sorted { $0.start_scheduled > $1.start_scheduled } let u = items.filter { $0.status == .upcoming }.sorted { $0.start_scheduled < $1.start_scheduled } - let e = items.filter { $0.status == .past }.sorted { $0.start_scheduled > $1.start_scheduled } + let e = items.filter { $0.status == .past && $0.start_scheduled <= Date() }.sorted { $0.start_scheduled > $1.start_scheduled } var rtr: [StreamerItemModel] = [] if !l.isEmpty { rtr.append(StreamerItemModel(title: Bundle.main.localizedString(forKey: "Live", value: "Live", table: "Localizeable"), items: l)) } if !u.isEmpty { rtr.append(StreamerItemModel(title: Bundle.main.localizedString(forKey: "Upcoming", value: "Upcoming", table: "Localizeable"), items: u)) } if !e.isEmpty { rtr.append(StreamerItemModel(title: Bundle.main.localizedString(forKey: "Ended", value: "Ended", table: "Localizeable"), items: e))} - rtr.append(StreamerItemModel(title: Bundle.main.localizedString(forKey: "Stream data provided by Holodex. Results capped at 50.", value: "Stream data provided by Holodex. Results capped at 50.", table: "Localizeable"), items: [])) + rtr.append(StreamerItemModel(title: Bundle.main.localizedString(forKey: "Stream data provided by Holodex.", value: "Stream data provided by Holodex.", table: "Localizeable"), items: [])) return rtr } diff --git a/ios/Core/Services/HoloDexServices.swift b/ios/Core/Services/HoloDexServices.swift index 6a8d823..5e1eda8 100644 --- a/ios/Core/Services/HoloDexServices.swift +++ b/ios/Core/Services/HoloDexServices.swift @@ -12,9 +12,9 @@ import RxSwift struct HoloDexServices { init() {} - func streamers(_ org: String) -> Single { + func streamers(_ org: String, status: String) -> Single { return Single.create { observer in - let url = URL(string: "https://holodex.net/api/v2/videos?status=live%2Cupcoming%2Cpast&lang=all&type=stream&include=description%2Clive_info&org=\(org.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "Hololive")&sort=start_scheduled&order=desc&limit=50&offset=0&paginated=%3Cempty%3E&max_upcoming_hours=48")! + let url = URL(string: "https://holodex.net/api/v2/videos?status=\(status)&lang=all&type=stream&include=description%2Clive_info&org=\(org.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "Hololive")&sort=start_scheduled&order=desc&limit=50&offset=0&paginated=%3Cempty%3E&max_upcoming_hours=48")! let task = URLSession.shared.dataTask(with: url) { response, _, error in if let response = response { do { From 53efbe14b51530011ac4e8ebda42f46de34ffb8a Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Thu, 9 Sep 2021 14:43:48 -0400 Subject: [PATCH 04/16] fix horizontal homeView popout preview --- ios/Views/Home/HomeView.swift | 38 +++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/ios/Views/Home/HomeView.swift b/ios/Views/Home/HomeView.swift index 29df522..8496fa3 100644 --- a/ios/Views/Home/HomeView.swift +++ b/ios/Views/Home/HomeView.swift @@ -18,6 +18,9 @@ import UIKit import FontAwesome_swift class HomeView: BaseController { + var popoutWidth: CGFloat = 333 + var popoutImageHeight: CGFloat = 187 + var rightButton: UIBarButtonItem { let b = UIBarButtonItem(title: "cogs", style: .plain, target: self, action: #selector(settings)) b.setTitleTextAttributes([.font: UIFont(name: "FontAwesome5Pro-Solid", size: 20)!], for: .normal) @@ -148,6 +151,13 @@ class HomeView: BaseController { override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() + switch UIDevice.current.model { + case "iPhone": view.width < view.height ? iPhoneLayoutPortrait() : iPhoneLayoutLandscape() + case "iPad": view.width < view.height ? iPadLayoutPortrait() : iPadLayoutLandscape() + + default: break + } + table.fillSuperview(left: 5, right: 5, top: 15, bottom: 5) } } @@ -190,7 +200,7 @@ extension HomeView: UITableViewDelegate { let viewController = UIViewController() let popoutView = UIView() let imageView = UIImageView() - popoutView.frame = CGRect(x: 0, y: 0, width: 333, height: 999) + popoutView.frame = CGRect(x: 0, y: 0, width: popoutWidth, height: 999) // popoutView.clipsToBounds = true imageView.kf.indicatorType = .activity @@ -204,7 +214,7 @@ extension HomeView: UITableViewDelegate { } } popoutView.addSubview(imageView) - imageView.anchorToEdge(.top, padding: 0, width: 333, height: 187) + imageView.anchorToEdge(.top, padding: 0, width: popoutWidth, height: popoutImageHeight) let titleText = model.output.title(for: indexPath.section, and: indexPath.row) let nsText = titleText as NSString? @@ -218,13 +228,13 @@ extension HomeView: UITableViewDelegate { popoutView.addSubview(title) title.sizeToFit() - title.align(.underCentered, relativeTo: imageView, padding: 10, width: 300, height: textSize?.height ?? 0) + title.align(.underCentered, relativeTo: imageView, padding: 10, width: popoutWidth - 30, height: textSize?.height ?? 0) title.leadingAnchor.constraint(equalTo: popoutView.safeAreaLayoutGuide.leadingAnchor, constant: 100).isActive = true title.trailingAnchor.constraint(equalTo: popoutView.safeAreaLayoutGuide.trailingAnchor, constant: -100).isActive = true title.layoutIfNeeded() let popoutHeight = title.height + imageView.height + 20 - popoutView.frame = CGRect(x: 0, y: 0, width: 333, height: popoutHeight) + popoutView.frame = CGRect(x: 0, y: 0, width: popoutWidth, height: popoutHeight) viewController.view = popoutView viewController.preferredContentSize = popoutView.frame.size @@ -258,4 +268,24 @@ extension HomeView: UITableViewDelegate { return UIMenu(title: "", image: nil, children: [shareAction, youtubeAction]) } } + + func iPhoneLayoutPortrait() { + popoutWidth = 333 + popoutImageHeight = 187 + } + + func iPhoneLayoutLandscape() { + popoutWidth = 262 + popoutImageHeight = 147 + } + + func iPadLayoutPortrait() { + popoutWidth = 340 + popoutImageHeight = 191 + } + + func iPadLayoutLandscape() { + popoutWidth = 343 + popoutImageHeight = 192 + } } From 751c4dfa15315c3c6c96aa0d13ab539ea1ad0edb Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Thu, 9 Sep 2021 14:45:16 -0400 Subject: [PATCH 05/16] timed messages a little better now... --- ios/Core/Models/StreamModel.swift | 21 +++++++++------------ ios/Core/Types/DisplayableMessage.swift | 4 +--- ios/Core/Types/InjectedMessage.swift | 2 +- ios/WindowInjector.js | 8 ++++++-- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/ios/Core/Models/StreamModel.swift b/ios/Core/Models/StreamModel.swift index 8ac0239..3e247fd 100644 --- a/ios/Core/Models/StreamModel.swift +++ b/ios/Core/Models/StreamModel.swift @@ -101,18 +101,15 @@ class StreamModel: BaseModel { .bind(to: rawChatRelay) .disposed(by: bag) - Observable.timer(.milliseconds(0), period: .milliseconds(500), scheduler: MainScheduler.asyncInstance) - .subscribe { _ in - var messageQueue = self.rawChatRelay.value - var sendMessages: [DisplayableMessage] = self.chatRelay.value - print("\(messageQueue.first?.showTimestamp) <= \(Date())") - while messageQueue.first?.showTimestamp ?? Date.distantPast >= Date() { - sendMessages.append(messageQueue.first!) - messageQueue.removeFirst() - - self.chatRelay.accept(sendMessages) - } - }.disposed(by: bag) + Observable.combineLatest(Observable.timer(.milliseconds(500), scheduler: MainScheduler.instance), rawChatRelay) + .map { _, chat -> [DisplayableMessage] in + var f = chat.filter { $0.showTimestamp <= Date() } + f.append(contentsOf: self.chatRelay.value) + return f + } + .map { $0.sorted { $0.showTimestamp > $1.showTimestamp } } + .bind(to: chatRelay).disposed(by: bag) + playerRelay.compactMap { $0 } .map { (id: $0.identifier, duration: $0.duration) } .subscribe(onNext: loadChat) diff --git a/ios/Core/Types/DisplayableMessage.swift b/ios/Core/Types/DisplayableMessage.swift index 071a16b..e115902 100644 --- a/ios/Core/Types/DisplayableMessage.swift +++ b/ios/Core/Types/DisplayableMessage.swift @@ -55,6 +55,4 @@ enum Message: Decodable { } } -extension Message: Equatable { - -} +extension Message: Equatable {} diff --git a/ios/Core/Types/InjectedMessage.swift b/ios/Core/Types/InjectedMessage.swift index dbf6cd2..e38ece1 100644 --- a/ios/Core/Types/InjectedMessage.swift +++ b/ios/Core/Types/InjectedMessage.swift @@ -48,5 +48,5 @@ extension InjectedMessage: DisplayableMessage { var superchatData: Superchat? { superchat } var sortTimestamp: Date { timestamp } - var showTimestamp: Date { Date(timeIntervalSinceNow: showtime/1000) } + var showTimestamp: Date { Date(timeIntervalSince1970: showtime / 1000) } } diff --git a/ios/WindowInjector.js b/ios/WindowInjector.js index c6e6e09..cfc2efc 100644 --- a/ios/WindowInjector.js +++ b/ios/WindowInjector.js @@ -57,14 +57,19 @@ const messageReceiveCallback = async(response) => { console.debug('Response was invalid', response); return; } + console.log("Hello world"); + ( response.continuationContents.liveChatContinuation.actions || [] ).forEach((action, i) => { try { let currentElement = action.addChatItemAction; + const offsetMs = response.continuationContents?.liveChatContinuation.continuations[0].timedContinuationData?.timeoutMs || response.continuationContents?.liveChatContinuation.continuations[0].invalidationContinuationData?.timeoutMs; + if (action.replayChatItemAction != null) { const thisAction = action.replayChatItemAction.actions[0]; currentElement = thisAction.addChatItemAction; + offsetMs = action.replayChatItemAction.videoOffsetTimeMsec; } currentElement = (currentElement || {}).item; if (!currentElement) { @@ -120,8 +125,7 @@ const messageReceiveCallback = async(response) => { index: i, messages: runs, timestamp: Math.round(parseInt(timestampUsec) / 1000), - showtime: isReplay ? getMillis(timestampText, timestampUsec) - : date.getTime() - (timestampUsec / 1000) + showtime: isReplay ? offsetMs : (timestampUsec / 1000) + offsetMs + 2000 }; if (currentElement.liveChatPaidMessageRenderer) { item.superchat = { From 92533341c50ef049b15cd57b497df475f8e7d14a Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Mon, 13 Sep 2021 11:16:48 -0400 Subject: [PATCH 06/16] going back home now uses the back animation --- ios/Core/Models/StreamModel.swift | 19 ++++++++++--------- ios/Core/Routing/AppFlow.swift | 15 ++++++++++++--- ios/Core/Routing/AppStep.swift | 1 + ios/Views/StreamView/StreamView.swift | 8 +++++--- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/ios/Core/Models/StreamModel.swift b/ios/Core/Models/StreamModel.swift index 3e247fd..92914ea 100644 --- a/ios/Core/Models/StreamModel.swift +++ b/ios/Core/Models/StreamModel.swift @@ -98,17 +98,18 @@ class StreamModel: BaseModel { control == .allChat ? live : translated } .map { $0.sorted { $0.sortTimestamp > $1.sortTimestamp } } - .bind(to: rawChatRelay) + //.bind(to: rawChatRelay) + .bind(to: chatRelay) .disposed(by: bag) - Observable.combineLatest(Observable.timer(.milliseconds(500), scheduler: MainScheduler.instance), rawChatRelay) - .map { _, chat -> [DisplayableMessage] in - var f = chat.filter { $0.showTimestamp <= Date() } - f.append(contentsOf: self.chatRelay.value) - return f - } - .map { $0.sorted { $0.showTimestamp > $1.showTimestamp } } - .bind(to: chatRelay).disposed(by: bag) +// Observable.combineLatest(Observable.timer(.milliseconds(500), scheduler: MainScheduler.instance), rawChatRelay) +// .map { _, chat -> [DisplayableMessage] in +// var f = chat.filter { $0.showTimestamp <= Date() } +// f.append(contentsOf: self.chatRelay.value) +// return f +// } +// .map { $0.sorted { $0.showTimestamp > $1.showTimestamp } } +// .bind(to: chatRelay).disposed(by: bag) playerRelay.compactMap { $0 } .map { (id: $0.identifier, duration: $0.duration) } diff --git a/ios/Core/Routing/AppFlow.swift b/ios/Core/Routing/AppFlow.swift index e460528..54e9aa1 100644 --- a/ios/Core/Routing/AppFlow.swift +++ b/ios/Core/Routing/AppFlow.swift @@ -8,6 +8,7 @@ import UIKit import RxCocoa import RxFlow +import Kingfisher class AppFlow: Flow { var root: Presentable { @@ -24,6 +25,7 @@ class AppFlow: Flow { switch step { case .home : return toHome() case .view(let id) : return toStreamView(id) + case .streamDone : return streamViewDone() case .settings : return toSettings() case .settingsDone : return settingsDone() case .toConsent(let showAlert): return toConsent(showAlert) @@ -35,14 +37,21 @@ class AppFlow: Flow { private func toHome() -> FlowContributors { let controller = HomeView(stepper, services) - rootViewController.setViewControllers([controller], animated: true) - + rootViewController.pushViewController(controller, animated: true) + //rootViewController.setViewControllers([controller], animated: true) + return .none } private func toStreamView(_ id: String) -> FlowContributors { let controller = StreamView(stepper, services) controller.load(id) - rootViewController.setViewControllers([controller], animated: true) + KingfisherManager.shared.cache.clearMemoryCache() + rootViewController.pushViewController(controller, animated: true) + + return .none + } + private func streamViewDone() -> FlowContributors { + rootViewController.popViewController(animated: true) return .none } diff --git a/ios/Core/Routing/AppStep.swift b/ios/Core/Routing/AppStep.swift index 4cd89b8..f5db283 100644 --- a/ios/Core/Routing/AppStep.swift +++ b/ios/Core/Routing/AppStep.swift @@ -11,6 +11,7 @@ import RxFlow enum AppStep: Step { case home case view(_ id: String) + case streamDone case settings, settingsDone case filter, filterDone case toConsent(_ showAlert: Bool), consentDone diff --git a/ios/Views/StreamView/StreamView.swift b/ios/Views/StreamView/StreamView.swift index 45489f9..f71f319 100644 --- a/ios/Views/StreamView/StreamView.swift +++ b/ios/Views/StreamView/StreamView.swift @@ -191,7 +191,7 @@ class StreamView: BaseController { videoPlayer.player = nil player = nil settingsService.spotlightUser = nil - stepper.steps.accept(AppStep.home) + stepper.steps.accept(AppStep.streamDone) } @objc func settings() { @@ -201,7 +201,7 @@ class StreamView: BaseController { override func handle(_ error: Error) { let nserror = error as NSError - if nserror.code == -2, waitRoom == false { + if nserror.code == -2, let startStringTimestamp = nserror.userInfo["startTimestamp"] as? String, waitRoom == false { waitRoom = true waitRoomView.kf.setImage(with: URL(string: "https://i.ytimg.com/vi/\(videoID)/maxresdefault.jpg"), options: [.cacheOriginalImage]) { result in switch result { @@ -213,7 +213,7 @@ class StreamView: BaseController { } } // Get Timestamp - let startStringTimestamp = nserror.userInfo["startTimestamp"] as! String + //let startStringTimestamp = nserror.userInfo["startTimestamp"] as! String let startTimestamp = Date(timeIntervalSince1970: Double(startStringTimestamp)!) let interval = DateInterval(start: Date(), end: startTimestamp) @@ -363,3 +363,5 @@ class StreamView: BaseController { chatControl.align(.aboveCentered, relativeTo: chatTable, padding: 2, width: view.width * 0.3, height: 35) } } + + From ff2cbd48748a324fafe7ccd55cb6261fa7c7d741 Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Mon, 13 Sep 2021 11:17:04 -0400 Subject: [PATCH 07/16] Pull to refresh --- ios/Core/Models/HomeModel.swift | 2 +- ios/Views/Home/HomeView.swift | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ios/Core/Models/HomeModel.swift b/ios/Core/Models/HomeModel.swift index 0005f5f..1b79d93 100644 --- a/ios/Core/Models/HomeModel.swift +++ b/ios/Core/Models/HomeModel.swift @@ -48,7 +48,7 @@ class HomeModel: BaseModel { super.init(services) refresh.subscribe(onNext: { _ in self.loadStreamers(services.settings.orgFilter) }).disposed(by: bag) - liveStreamers.compactMap { $0 }.distinctUntilChanged() + liveStreamers.compactMap { $0 }//.distinctUntilChanged() .map { _ in false } .bind(to: refreshState) .disposed(by: bag) diff --git a/ios/Views/Home/HomeView.swift b/ios/Views/Home/HomeView.swift index 8496fa3..cb5c981 100644 --- a/ios/Views/Home/HomeView.swift +++ b/ios/Views/Home/HomeView.swift @@ -64,7 +64,6 @@ class HomeView: BaseController { selector: #selector(checkPasteboard), name: UIApplication.willEnterForegroundNotification, object: nil) - view.backgroundColor = .systemBackground navigationItem.rightBarButtonItem = rightButton navigationItem.leftBarButtonItem = leftButton @@ -86,6 +85,7 @@ class HomeView: BaseController { let engNameObserver = Defaults.observe(\.englishNames) { _ in self.reload() } observers.append(contentsOf: [orgObserver, thumbnailsObserver, blurObserver, darkenObserver, engNameObserver]) + refresh.addTarget(self, action: #selector(reload), for: .valueChanged) refresh.rx.controlEvent(.valueChanged).bind(to: model.input.refresh).disposed(by: bag) model.output.refreshDoneDriver.drive(refresh.rx.isRefreshing).disposed(by: bag) @@ -95,6 +95,7 @@ class HomeView: BaseController { .map { $0.sections() } .drive(table.rx.items(dataSource: dataSource)) .disposed(by: bag) + table.addSubview(refresh) view.addSubview(table) model.input.loadStreamers(services.settings.orgFilter) @@ -141,7 +142,7 @@ class HomeView: BaseController { stepper.steps.accept(AppStep.filter) } - private func reload() { + @objc private func reload() { model.input.refresh.accept(()) DispatchQueue.main.async { self.navigationItem.title = "\(self.services.settings.orgFilter.short)Dex" @@ -289,3 +290,9 @@ extension HomeView: UITableViewDelegate { popoutImageHeight = 192 } } + +extension HomeView: UIGestureRecognizerDelegate { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } +} From 48f723dcfd85ed0da111d1c4e1b4bf8d3d781615 Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Fri, 5 Nov 2021 15:17:18 -0400 Subject: [PATCH 08/16] remove archive mchad and start work on live mchad --- ios.xcodeproj/project.pbxproj | 16 ++-- ios/Core/Models/HomeModel.swift | 2 +- ios/Core/Models/StreamModel.swift | 59 ++++++------- ios/Core/Services/HoloDexServices.swift | 2 + ios/Core/Services/MchadServices.swift | 110 +++++++++++++++--------- ios/Core/Types/DisplayableMessage.swift | 2 +- ios/Core/Types/InjectedMessage.swift | 4 +- ios/Core/Types/MchadTypes.swift | 6 ++ ios/Core/Types/TranslatedMessage.swift | 28 +++--- ios/WindowInjector.js | 5 +- 10 files changed, 128 insertions(+), 106 deletions(-) diff --git a/ios.xcodeproj/project.pbxproj b/ios.xcodeproj/project.pbxproj index 272cd64..c932c6b 100644 --- a/ios.xcodeproj/project.pbxproj +++ b/ios.xcodeproj/project.pbxproj @@ -842,7 +842,7 @@ CODE_SIGN_ENTITLEMENTS = "yt-extension/yt-extension.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; - DEVELOPMENT_TEAM = 6S4L29QT59; + DEVELOPMENT_TEAM = RJNC97Y8QD; INFOPLIST_FILE = "yt-extension/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -851,7 +851,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.1.0; - PRODUCT_BUNDLE_IDENTIFIER = "app.livetl.ios.yt-extension"; + PRODUCT_BUNDLE_IDENTIFIER = "app.livetl-dev.ios.yt-extension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; @@ -866,7 +866,7 @@ CODE_SIGN_ENTITLEMENTS = "yt-extension/yt-extension.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; - DEVELOPMENT_TEAM = 6S4L29QT59; + DEVELOPMENT_TEAM = RJNC97Y8QD; INFOPLIST_FILE = "yt-extension/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -875,7 +875,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.1.0; - PRODUCT_BUNDLE_IDENTIFIER = "app.livetl.ios.yt-extension"; + PRODUCT_BUNDLE_IDENTIFIER = "app.livetl-dev.ios.yt-extension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; @@ -1012,7 +1012,7 @@ CODE_SIGN_ENTITLEMENTS = ios/ios.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; - DEVELOPMENT_TEAM = 6S4L29QT59; + DEVELOPMENT_TEAM = RJNC97Y8QD; INFOPLIST_FILE = ios/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -1020,7 +1020,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.1.0; - PRODUCT_BUNDLE_IDENTIFIER = app.livetl.ios; + PRODUCT_BUNDLE_IDENTIFIER = "app.livetl-dev.ios"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -1040,7 +1040,7 @@ CODE_SIGN_ENTITLEMENTS = ios/ios.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 3; - DEVELOPMENT_TEAM = 6S4L29QT59; + DEVELOPMENT_TEAM = RJNC97Y8QD; INFOPLIST_FILE = ios/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -1048,7 +1048,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.1.0; - PRODUCT_BUNDLE_IDENTIFIER = app.livetl.ios; + PRODUCT_BUNDLE_IDENTIFIER = "app.livetl-dev.ios"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; diff --git a/ios/Core/Models/HomeModel.swift b/ios/Core/Models/HomeModel.swift index 1b79d93..39c8448 100644 --- a/ios/Core/Models/HomeModel.swift +++ b/ios/Core/Models/HomeModel.swift @@ -111,7 +111,7 @@ extension HomeModel: HomeModelOutput { func video(for section: Int, and index: Int) -> String { let r = streamers.value!.sections() - //return "kWTVKNhRmfg" + //return "sVCp2PhQBaE" return r[section].items[index].id } func thumbnail(for section: Int, and index: Int) -> URL? { diff --git a/ios/Core/Models/StreamModel.swift b/ios/Core/Models/StreamModel.swift index 92914ea..2dcc17a 100644 --- a/ios/Core/Models/StreamModel.swift +++ b/ios/Core/Models/StreamModel.swift @@ -48,7 +48,6 @@ class StreamModel: BaseModel { private let controlRelay = BehaviorRelay(value: .allChat) - private let rawChatRelay = BehaviorRelay<[DisplayableMessage]>(value: []) private let chatRelay = BehaviorRelay<[DisplayableMessage]>(value: []) private let liveRelay = BehaviorRelay<[DisplayableMessage]>(value: []) private let translatedRelay = BehaviorRelay<[DisplayableMessage]>(value: []) @@ -58,7 +57,7 @@ class StreamModel: BaseModel { private let chatURLRelay = BehaviorRelay(value: nil) private let mchadRoomRelay = BehaviorRelay(value: nil) - private let mchadScriptRelay = BehaviorRelay<[MchadScript?]>(value: []) + private let mchadScriptRelay = BehaviorRelay<[DisplayableMessage]>(value: []) private let metadataRelay = BehaviorRelay(value: nil) private let replayRelay = BehaviorRelay(value: false) @@ -92,25 +91,14 @@ class StreamModel: BaseModel { self.liveRelay.accept([]) self.translatedRelay.accept([]) self.chatRelay.accept([]) - self.rawChatRelay.accept([]) }).disposed(by: bag) Observable.combineLatest(controlRelay, liveRelay, translatedRelay).map { control, live, translated in control == .allChat ? live : translated } .map { $0.sorted { $0.sortTimestamp > $1.sortTimestamp } } - //.bind(to: rawChatRelay) .bind(to: chatRelay) .disposed(by: bag) -// Observable.combineLatest(Observable.timer(.milliseconds(500), scheduler: MainScheduler.instance), rawChatRelay) -// .map { _, chat -> [DisplayableMessage] in -// var f = chat.filter { $0.showTimestamp <= Date() } -// f.append(contentsOf: self.chatRelay.value) -// return f -// } -// .map { $0.sorted { $0.showTimestamp > $1.showTimestamp } } -// .bind(to: chatRelay).disposed(by: bag) - playerRelay.compactMap { $0 } .map { (id: $0.identifier, duration: $0.duration) } .subscribe(onNext: loadChat) @@ -174,30 +162,33 @@ class StreamModel: BaseModel { .asObservable() .materialize() - let mchad = services.mchad.getMchadRoom(id: id, duration: duration) +// let mchad = services.mchad.getMchadRoom(id: id, duration: duration) +// .asObservable() +// .materialize() +// .subscribe(onNext: { room in +// self.services.mchad.getMchadLiveTls(id, room: room.element!) +// .asObservable() +// .materialize() +// .subscribe(onNext: { messages in +// if messages.element != nil { +// let push = [messages.element] +// self.mchadScriptRelay.accept(push as! [DisplayableMessage]) +// } +// }) +// .disposed(by: self.bag) +// }) +// .disposed(by: bag) + + services.mchad.getMchadLiveTls(id, room: nil) .asObservable() .materialize() - - mchadRoomRelay.compactMap { $0 } - .subscribe(onNext: { room in - self.services.mchad.getMchadArchiveTls(id, room: room) - .asObservable() - .materialize() - .subscribe(onNext: { messages in - if messages.element != nil { - self.translatedRelay.accept(messages.element as! [DisplayableMessage]) - } - }) - .disposed(by: self.bag) + .subscribe(onNext: { messages in + if messages.element != nil { + let push = [messages.element] + self.mchadScriptRelay.accept(push as! [DisplayableMessage]) + } }) - .disposed(by: bag) - - mchad.map { $0.element?.first } - .bind(to: mchadRoomRelay) - .disposed(by: bag) - mchad.map { $0.error } - .bind(to: errorRelay) - .disposed(by: bag) + request.map { $0.element } .bind(to: chatURLRelay) diff --git a/ios/Core/Services/HoloDexServices.swift b/ios/Core/Services/HoloDexServices.swift index 5e1eda8..12d9fb3 100644 --- a/ios/Core/Services/HoloDexServices.swift +++ b/ios/Core/Services/HoloDexServices.swift @@ -89,3 +89,5 @@ extension JSONDecoder.DateDecodingStrategy { } } } + + diff --git a/ios/Core/Services/MchadServices.swift b/ios/Core/Services/MchadServices.swift index a94669c..9c7c350 100644 --- a/ios/Core/Services/MchadServices.swift +++ b/ios/Core/Services/MchadServices.swift @@ -12,7 +12,7 @@ import RxSwift struct MchadServices { init() {} - func getMchadRoom(id: String, duration: Double) -> Single<[MchadRoom]> { + func getMchadRoom(id: String, duration: Double) -> Single { return Single.create { observer in let request: URLRequest if duration > 0 { @@ -22,15 +22,18 @@ struct MchadServices { request = URLRequest(url: URL(string: "https://repo.mchatx.org/Room?link=YT_\(id)")!) } let task = URLSession.shared.dataTask(with: request) { data, _, error in - if let data = data, let room = String(data: data, encoding: .utf8) { + if let data = data { do { //print(room) let decoder = JSONDecoder() let json = try decoder.decode([MchadRoom].self, from: data) - observer(.success(json)) + if !json.isEmpty { + observer(.success(json.first!)) + } else { + print("No mchad room") + } } catch { print(error) - //observer(.failure(error)) } } else if let error = error { observer(.failure(error)) @@ -44,57 +47,82 @@ struct MchadServices { } } - func getMchadArchiveTls(_ id: String, room: MchadRoom) -> Single<[TranslatedMessage?]> { - Single.create { observer in - let bag = DisposeBag() - let meta = BehaviorRelay(value: nil) - let start = BehaviorRelay(value: nil) + func getMchadLiveTls(_ id: String, room: MchadRoom?) -> Observable { + return Observable.create { observer in + let request = URLRequest(url: URL(string: "https://repo.mchatx.org/Listener/?room=Testing")!) - var request = URLRequest(url: URL(string: "https://repo.mchatx.org/Archive")!) - request.httpMethod = "POST" - request.addValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept") - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - - let jsonObject = NSMutableDictionary() - jsonObject.setValue(room.Link, forKey: "link") - let jsonData: NSData - do { - jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions()) as NSData - request.httpBody = jsonData as Data? - } catch { + let task = URLSession.shared.dataTask(with: request) { data, responce, error in + print(data) print(error) - } - - let task = URLSession.shared.dataTask(with: request) { data, _, error in + print(responce) if let data = data { do { let decoder = JSONDecoder() - let json = try decoder.decode([MchadScript].self, from: data) - if let startTime = json.first(where: { $0.Stext == "--- Stream Starts ---"})?.Stime { - let messages = json.map { TranslatedMessage(from: $0, room: room) } - - observer(.success(messages)) - } else if let startTime = json.first?.Stime { - - } else { - let startTime = Date.distantPast - } - //observer(.success(json)) + let json = try decoder.decode(MchadIncoming.self, from: data) + + print(json) + + let room = MchadRoom(Room: "Testing", Link: nil, Nick: nil, EntryPass: nil, Empty: nil, Pass: nil, StreamLink: nil, Tags: "en", ExtShare: nil, Downloadable: nil) + observer.onNext(TranslatedMessage(from: json.content, room: room)) } catch { - //observer(.failure(error)) print(error) } - } else if let error = error { - print(error) - //observer(.failure(error)) } + } - task.resume() - return Disposables.create { + task.resume() + return Disposables.create() { task.cancel() } } } + + +// func getMchadArchiveTls(_ id: String, room: MchadRoom) -> Single<[TranslatedMessage?]> { +// Single.create { observer in +// var request = URLRequest(url: URL(string: "https://repo.mchatx.org/Archive")!) +// request.httpMethod = "POST" +// request.addValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept") +// request.addValue("application/json", forHTTPHeaderField: "Content-Type") +// +// let jsonObject = NSMutableDictionary() +// jsonObject.setValue(room.Link, forKey: "link") +// let jsonData: NSData +// do { +// jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions()) as NSData +// request.httpBody = jsonData as Data? +// } catch { +// print(error) +// } +// +// let task = URLSession.shared.dataTask(with: request) { data, _, error in +// if let data = data { +// do { +// let decoder = JSONDecoder() +// +// let json = try decoder.decode([MchadScript].self, from: data) +// print(json) +// if (json.first(where: { $0.Stext == "--- Stream Starts ---"})?.Stime) != nil { +// let messages = json.map { TranslatedMessage(from: $0, room: room) } +// +// observer(.success(messages)) +// } +// } catch { +// observer(.failure(error)) +// print(error) +// } +// } else if let error = error { +// print(error) +// observer(.failure(error)) +// } +// } +// task.resume() +// +// return Disposables.create { +// task.cancel() +// } +// } +// } } diff --git a/ios/Core/Types/DisplayableMessage.swift b/ios/Core/Types/DisplayableMessage.swift index e115902..91eac60 100644 --- a/ios/Core/Types/DisplayableMessage.swift +++ b/ios/Core/Types/DisplayableMessage.swift @@ -16,7 +16,7 @@ protocol DisplayableMessage { var superchatData : Superchat? { get } var sortTimestamp: Date { get } - var showTimestamp: Date { get } + } extension DisplayableMessage { diff --git a/ios/Core/Types/InjectedMessage.swift b/ios/Core/Types/InjectedMessage.swift index e38ece1..fe1706b 100644 --- a/ios/Core/Types/InjectedMessage.swift +++ b/ios/Core/Types/InjectedMessage.swift @@ -17,7 +17,6 @@ struct MessageChunk: Decodable { struct InjectedMessage: Decodable { let author: Author let messages: [Message] - let showtime: Double let timestamp: Date let superchat: Superchat? @@ -45,8 +44,9 @@ extension InjectedMessage: DisplayableMessage { } return false } + var superchatData: Superchat? { superchat } var sortTimestamp: Date { timestamp } - var showTimestamp: Date { Date(timeIntervalSince1970: showtime / 1000) } + } diff --git a/ios/Core/Types/MchadTypes.swift b/ios/Core/Types/MchadTypes.swift index ea2eb8f..ba3eced 100644 --- a/ios/Core/Types/MchadTypes.swift +++ b/ios/Core/Types/MchadTypes.swift @@ -20,7 +20,13 @@ struct MchadRoom: Decodable { let Downloadable: Bool? } +struct MchadIncoming: Decodable { + let flag: String? + let content: MchadScript +} + struct MchadScript: Decodable { + let _id: String let Stime: Date let Stext: String let CC: String? diff --git a/ios/Core/Types/TranslatedMessage.swift b/ios/Core/Types/TranslatedMessage.swift index b45873c..fc8f81d 100644 --- a/ios/Core/Types/TranslatedMessage.swift +++ b/ios/Core/Types/TranslatedMessage.swift @@ -14,23 +14,23 @@ struct TranslatedMessage { let languages: [String] let timestamp: Date - let show : Double + let show: Double let superchat: Superchat? init?(from message: InjectedMessage) { self.author = Author(from: message.author) self.timestamp = message.timestamp - self.show = message.showtime + self.show = 0 self.superchat = message.superchat var m: [Message?] = [] - var l: [String]? = nil + var l: [String]? - if case let .text(s) = message.messages.first { + if case .text(let s) = message.messages.first { for token in tokens { guard let begin = s.firstIndex(of: token.start), - let end = s.firstIndex(of: token.end) + let end = s.firstIndex(of: token.end) else { continue } guard begin < end else { continue } @@ -43,15 +43,13 @@ struct TranslatedMessage { do { for splitLang in try lang.split(usingRegex: "\\W+") { - guard TranslatedLanguageTag.allCases.map({ $0.tag }).contains(splitLang) || TranslatedLanguageTag.allCases.map({ $0.description.lowercased().hasPrefix(splitLang) }).contains(Bool.init(true)) || TranslatedLanguageTag.allCases.map({ $0.tag.lowercased().hasPrefix(splitLang) }).contains(Bool.init(true)) else { continue } + guard TranslatedLanguageTag.allCases.map({ $0.tag }).contains(splitLang) || TranslatedLanguageTag.allCases.map({ $0.description.lowercased().hasPrefix(splitLang) }).contains(Bool(true)) || TranslatedLanguageTag.allCases.map({ $0.tag.lowercased().hasPrefix(splitLang) }).contains(Bool(true)) else { continue } finalLang.append(splitLang) } } catch { print("Whoops") } - - let mStart = s.index(after: end) m.append(Message.text(String(s[mStart.. [String] { - //### Crashes when you pass invalid `pattern` + // ### Crashes when you pass invalid `pattern` let regex = try NSRegularExpression(pattern: pattern) let matches = regex.matches(in: self, range: NSRange(0.. { console.debug('Response was invalid', response); return; } - console.log("Hello world"); ( response.continuationContents.liveChatContinuation.actions || [] ).forEach((action, i) => { try { let currentElement = action.addChatItemAction; - const offsetMs = response.continuationContents?.liveChatContinuation.continuations[0].timedContinuationData?.timeoutMs || response.continuationContents?.liveChatContinuation.continuations[0].invalidationContinuationData?.timeoutMs; if (action.replayChatItemAction != null) { const thisAction = action.replayChatItemAction.actions[0]; currentElement = thisAction.addChatItemAction; - offsetMs = action.replayChatItemAction.videoOffsetTimeMsec; } currentElement = (currentElement || {}).item; if (!currentElement) { @@ -125,7 +122,7 @@ const messageReceiveCallback = async(response) => { index: i, messages: runs, timestamp: Math.round(parseInt(timestampUsec) / 1000), - showtime: isReplay ? offsetMs : (timestampUsec / 1000) + offsetMs + 2000 + showtime: isReplay ? getMillis(timestampText, timestampUsec) : date.getTime() - (timestampUsec / 1000) }; if (currentElement.liveChatPaidMessageRenderer) { item.superchat = { From f57350745dd7c82aef04e520c3d581022b8cb42f Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Tue, 9 Nov 2021 10:21:45 -0500 Subject: [PATCH 09/16] fix chat performance --- Podfile.lock | 2 +- ios/Core/Models/StreamModel.swift | 18 +++++++++--------- ios/Views/StreamView/ChatCell/ChatCell.swift | 17 ++++++++++++++--- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 44f124d..c8aa384 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -110,4 +110,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: bd3025cc71699ad0300e7ce21de6683a7ce4a10a -COCOAPODS: 1.10.1 +COCOAPODS: 1.11.2 diff --git a/ios/Core/Models/StreamModel.swift b/ios/Core/Models/StreamModel.swift index 2dcc17a..e540cef 100644 --- a/ios/Core/Models/StreamModel.swift +++ b/ios/Core/Models/StreamModel.swift @@ -179,15 +179,15 @@ class StreamModel: BaseModel { // }) // .disposed(by: bag) - services.mchad.getMchadLiveTls(id, room: nil) - .asObservable() - .materialize() - .subscribe(onNext: { messages in - if messages.element != nil { - let push = [messages.element] - self.mchadScriptRelay.accept(push as! [DisplayableMessage]) - } - }) +// services.mchad.getMchadLiveTls(id, room: nil) +// .asObservable() +// .materialize() +// .subscribe(onNext: { messages in +// if messages.element != nil { +// let push = [messages.element] +// self.mchadScriptRelay.accept(push as! [DisplayableMessage]) +// } +// }) request.map { $0.element } diff --git a/ios/Views/StreamView/ChatCell/ChatCell.swift b/ios/Views/StreamView/ChatCell/ChatCell.swift index 1a7f854..ddb0427 100644 --- a/ios/Views/StreamView/ChatCell/ChatCell.swift +++ b/ios/Views/StreamView/ChatCell/ChatCell.swift @@ -22,9 +22,15 @@ class ChatCell: UITableViewCell { func configure(_ item: DisplayableMessage, useTimestamps: Bool) { author.text = item.displayAuthor timestamp.text = useTimestamps ? item.displayTimestamp : "" + + // This should reset the cell, so we aviod duplicate superchats and members + timestamp.font = .systemFont(ofSize: 17) + timestamp.textColor = .secondaryLabel + contentView.layer.cornerRadius = 0 + contentView.backgroundColor = .clear + author.textColor = .secondaryLabel if item.superchatData != nil { - // print(item.superchatData) timestamp.text = item.superchatData?.amount timestamp.font = .boldSystemFont(ofSize: 17) timestamp.textColor = .label @@ -47,13 +53,17 @@ class ChatCell: UITableViewCell { case .text(let s): let am = NSAttributedString(string: s) fullMessage.append(am) - case .emote(let u, let id): + case .emote(var u, let id): if id != nil { if id!.isSingleEmoji { let am = NSAttributedString(string: id!) fullMessage.append(am) } else { - let html = " " + if u.pathExtension == "svg" { + u.deletePathExtension() + u.appendPathExtension("png") + } + let html = " " let data = Data(html.utf8) do { @@ -69,6 +79,7 @@ class ChatCell: UITableViewCell { } } + // print("\(item.displayAuthor): \(item.displayMessage)") message.attributedText = fullMessage } } From 775080d73bc6738f5bd612199812304f316778aa Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Sat, 27 Nov 2021 14:03:51 -0500 Subject: [PATCH 10/16] Use an API key for holodex requests It uses the HOLODEX_API_KEY env var --- ios/Core/Services/HoloDexServices.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ios/Core/Services/HoloDexServices.swift b/ios/Core/Services/HoloDexServices.swift index 12d9fb3..ffd64f6 100644 --- a/ios/Core/Services/HoloDexServices.swift +++ b/ios/Core/Services/HoloDexServices.swift @@ -14,8 +14,11 @@ struct HoloDexServices { func streamers(_ org: String, status: String) -> Single { return Single.create { observer in - let url = URL(string: "https://holodex.net/api/v2/videos?status=\(status)&lang=all&type=stream&include=description%2Clive_info&org=\(org.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "Hololive")&sort=start_scheduled&order=desc&limit=50&offset=0&paginated=%3Cempty%3E&max_upcoming_hours=48")! - let task = URLSession.shared.dataTask(with: url) { response, _, error in + let url = URL(string: "https://staging.holodex.net/api/v2/videos?status=\(status)&lang=all&type=stream&include=description%2Clive_info&org=\(org.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "Hololive")&sort=start_scheduled&order=desc&limit=50&offset=0&paginated=%3Cempty%3E&max_upcoming_hours=48")! + + var request = URLRequest(url: url) + request.setValue(ProcessInfo.processInfo.environment["HOLODEX_API_KEY"]!, forHTTPHeaderField: "X-APIKEY") + let task = URLSession.shared.dataTask(with: request) { response, _, error in if let response = response { do { let decoder = JSONDecoder() From 360dd5a29c279eb91dcaf361cbed1eb381d0f5ea Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Mon, 29 Nov 2021 18:23:48 -0500 Subject: [PATCH 11/16] Fixed an issue where we would try to replace an element in an empty array --- ios/Core/Types/TranslatedMessage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Core/Types/TranslatedMessage.swift b/ios/Core/Types/TranslatedMessage.swift index fc8f81d..ceb4714 100644 --- a/ios/Core/Types/TranslatedMessage.swift +++ b/ios/Core/Types/TranslatedMessage.swift @@ -71,7 +71,7 @@ struct TranslatedMessage { guard TranslatedLanguageTag.allCases.map({ $0.tag }).contains(lang) else { continue } let mStart = s.index(after: end) - m[0] = Message.text(String(s[mStart.. Date: Mon, 29 Nov 2021 18:25:21 -0500 Subject: [PATCH 12/16] fixed a crash when we tried to force unwrap nil when converting a string to a double --- ios/Views/StreamView/StreamView.swift | 95 ++++++++++++++------------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/ios/Views/StreamView/StreamView.swift b/ios/Views/StreamView/StreamView.swift index f71f319..944a77e 100644 --- a/ios/Views/StreamView/StreamView.swift +++ b/ios/Views/StreamView/StreamView.swift @@ -213,52 +213,59 @@ class StreamView: BaseController { } } // Get Timestamp - //let startStringTimestamp = nserror.userInfo["startTimestamp"] as! String - let startTimestamp = Date(timeIntervalSince1970: Double(startStringTimestamp)!) - let interval = DateInterval(start: Date(), end: startTimestamp) - - // try to load the video until it loads - Observable.timer(.seconds(0), period: .seconds(10), scheduler: MainScheduler.instance) - .take(until: { _ in self.videoLoaded }) - .subscribe(onNext: {_ in self.load(self.videoID)}, onCompleted: { - print("Video loaded") - - }) - .disposed(by: bag) - - // Create Countdown Timer - let countDown = Int(interval.duration) - Observable.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance) - .take(countDown + 1) - .subscribe(onNext: { timePassed in - let count = countDown - timePassed - let h = String(format: "%02d", count / 3600) - let m = String(format: "%02d", (count % 3600) / 60) - let s = String(format: "%02d", (count % 3600) % 60) - var timer = "\(m):\(s)" - if h != "00" { - timer = "\(h):\(timer)" - } - self.waitTimeText.text = "Live in " + timer - }, onCompleted: { - self.waitTimeText.text = "Waiting on stream to start..." - }) - .disposed(by: bag) - waitTimeText.font = .systemFont(ofSize: 19) - waitTimeText.textColor = .white + if let startStreamTimeDouble = Double(startStringTimestamp) { + let startTimestamp = Date(timeIntervalSince1970: startStreamTimeDouble) + let interval = DateInterval(start: Date(), end: startTimestamp) + // try to load the video until it loads + Observable.timer(.seconds(0), period: .seconds(10), scheduler: MainScheduler.instance) + .take(until: { _ in self.videoLoaded }) + .subscribe(onNext: {_ in self.load(self.videoID)}, onCompleted: { + print("Video loaded") + + }) + .disposed(by: bag) + + // Create Countdown Timer + let countDown = Int(interval.duration) + Observable.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance) + .take(countDown + 1) + .subscribe(onNext: { timePassed in + let count = countDown - timePassed + let h = String(format: "%02d", count / 3600) + let m = String(format: "%02d", (count % 3600) / 60) + let s = String(format: "%02d", (count % 3600) % 60) + var timer = "\(m):\(s)" + if h != "00" { + timer = "\(h):\(timer)" + } + self.waitTimeText.text = "Live in " + timer + }, onCompleted: { + self.waitTimeText.text = "Waiting on stream to start..." + }) + .disposed(by: bag) + waitTimeText.font = .systemFont(ofSize: 19) + waitTimeText.textColor = .white - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .long - dateFormatter.timeStyle = .medium - dateFormatter.timeZone = .current - dateFormatter.locale = .current - waitTimeScheduled.text = dateFormatter.string(from: startTimestamp) - waitTimeScheduled.font = .systemFont(ofSize: 15) - waitTimeScheduled.textColor = .white + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .long + dateFormatter.timeStyle = .medium + dateFormatter.timeZone = .current + dateFormatter.locale = .current + waitTimeScheduled.text = dateFormatter.string(from: startTimestamp) + waitTimeScheduled.font = .systemFont(ofSize: 15) + waitTimeScheduled.textColor = .white - waitRoomTextView.addSubview(waitTimeText) - waitRoomTextView.addSubview(waitTimeScheduled) - model.input.loadPreviewChat(videoID, duration: 0) + waitRoomTextView.addSubview(waitTimeText) + waitRoomTextView.addSubview(waitTimeScheduled) + model.input.loadPreviewChat(videoID, duration: 0) + } else { + let alert = SCLAlertView() + alert.addButton(Bundle.main.localizedString(forKey: "Go Back", value: "Go Back", table: "Localizeable")) { + self.closeStream() + } + + alert.showError(Bundle.main.localizedString(forKey: "An Error Occurred", value: "An Error Occurred", table: "Localizeable"), subTitle: "An error occured, and we cannot open the waitroom.") + } } if nserror.code == -6, nserror.userInfo["consentHtmlData"] as? String != nil { From d6d8dd6606de5451ad19d4b9866dbcfd9a609e42 Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Mon, 29 Nov 2021 18:26:03 -0500 Subject: [PATCH 13/16] Fixed duplicate thumbnails in HomeView Hopefully --- ios/Views/Home/HomeView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ios/Views/Home/HomeView.swift b/ios/Views/Home/HomeView.swift index cb5c981..29cf386 100644 --- a/ios/Views/Home/HomeView.swift +++ b/ios/Views/Home/HomeView.swift @@ -172,6 +172,10 @@ extension HomeView: UITableViewDelegate { let vid = model.output.video(for: indexPath.section, and: indexPath.row) stepper.steps.accept(AppStep.view(vid)) } + + private func tableView(_ tableView: UITableView, didEndDisplaying cell: StreamerCell, forRowAt indexPath: IndexPath) { + cell.thumbnail.kf.cancelDownloadTask() + } func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let index = indexPath.row From 3cca2f84c7322594eef8ba20451c6ced2ae29d68 Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Mon, 29 Nov 2021 18:31:40 -0500 Subject: [PATCH 14/16] maybe actually fix duplicate thumbnails this time --- ios/Views/Home/StreamerCell/StreamerCell.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/Views/Home/StreamerCell/StreamerCell.swift b/ios/Views/Home/StreamerCell/StreamerCell.swift index b307aef..a3f963a 100644 --- a/ios/Views/Home/StreamerCell/StreamerCell.swift +++ b/ios/Views/Home/StreamerCell/StreamerCell.swift @@ -85,6 +85,8 @@ class StreamerCell: UITableViewCell { break } } + + self.setNeedsLayout() } override func layoutSubviews() { From 43ebc06f41a1e3fa98c1028d80c4ebb2bd7193a9 Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Mon, 29 Nov 2021 18:32:29 -0500 Subject: [PATCH 15/16] switch back to normal holodex, not staging --- ios/Core/Services/HoloDexServices.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Core/Services/HoloDexServices.swift b/ios/Core/Services/HoloDexServices.swift index ffd64f6..76c7b5e 100644 --- a/ios/Core/Services/HoloDexServices.swift +++ b/ios/Core/Services/HoloDexServices.swift @@ -14,7 +14,7 @@ struct HoloDexServices { func streamers(_ org: String, status: String) -> Single { return Single.create { observer in - let url = URL(string: "https://staging.holodex.net/api/v2/videos?status=\(status)&lang=all&type=stream&include=description%2Clive_info&org=\(org.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "Hololive")&sort=start_scheduled&order=desc&limit=50&offset=0&paginated=%3Cempty%3E&max_upcoming_hours=48")! + let url = URL(string: "https://holodex.net/api/v2/videos?status=\(status)&lang=all&type=stream&include=description%2Clive_info&org=\(org.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "Hololive")&sort=start_scheduled&order=desc&limit=50&offset=0&paginated=%3Cempty%3E&max_upcoming_hours=48")! var request = URLRequest(url: url) request.setValue(ProcessInfo.processInfo.environment["HOLODEX_API_KEY"]!, forHTTPHeaderField: "X-APIKEY") From 99d4e27fdfd881a8be68a2f5e9299eca4f58c112 Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Tue, 30 Nov 2021 10:45:19 -0500 Subject: [PATCH 16/16] Mchad Archive Support --- ios.xcodeproj/project.pbxproj | 6 + ios/Core/Models/StreamModel.swift | 108 +++++++++---- ios/Core/Services/AppServices.swift | 1 + ios/Core/Services/MchadServices.swift | 150 ++++++++++--------- ios/Core/Types/DisplayableMessage.swift | 1 + ios/Core/Types/InjectedMessage.swift | 1 + ios/Core/Types/MchadTypes.swift | 5 +- ios/Core/Types/TranslatedMessage.swift | 9 +- ios/Views/StreamView/ChatCell/ChatCell.swift | 4 + ios/en.lproj/Localizeable.strings | 3 + ios/ios.entitlements | 4 + 11 files changed, 189 insertions(+), 103 deletions(-) diff --git a/ios.xcodeproj/project.pbxproj b/ios.xcodeproj/project.pbxproj index c932c6b..42e2bed 100644 --- a/ios.xcodeproj/project.pbxproj +++ b/ios.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 365045EF26D5B03900AD6214 /* MchadTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365045EE26D5B03900AD6214 /* MchadTypes.swift */; }; 36C224DB26D197CD00982BB1 /* FilterViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C224DA26D197CD00982BB1 /* FilterViewSwiftUI.swift */; }; 36CB58DE26C57F5F0002B8AF /* Superchat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36CB58DD26C57F5F0002B8AF /* Superchat.swift */; }; + 36DA10A02755C2600093E393 /* LtlAPIServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36DA109F2755C2600093E393 /* LtlAPIServices.swift */; }; + 36DA10A12755C2600093E393 /* LtlAPIServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36DA109F2755C2600093E393 /* LtlAPIServices.swift */; }; 36E6C1FA26A71DF600C2B08C /* HoloDexResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36EF35F026A64C370005E6FC /* HoloDexResponse.swift */; }; 36E6C1FB26A71DF600C2B08C /* TranslatedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36EF35F226A64C370005E6FC /* TranslatedMessage.swift */; }; 36E6C1FC26A71DF600C2B08C /* YouTubeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36EF35FE26A64C370005E6FC /* YouTubeService.swift */; }; @@ -125,6 +127,7 @@ 3652A47E26C20E7C003DA76B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizeable.strings; sourceTree = ""; }; 36C224DA26D197CD00982BB1 /* FilterViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterViewSwiftUI.swift; sourceTree = ""; }; 36CB58DD26C57F5F0002B8AF /* Superchat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Superchat.swift; sourceTree = ""; }; + 36DA109F2755C2600093E393 /* LtlAPIServices.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LtlAPIServices.swift; sourceTree = ""; }; 36EF35EC26A64C370005E6FC /* Language.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Language.swift; sourceTree = ""; }; 36EF35ED26A64C370005E6FC /* Organizations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Organizations.swift; sourceTree = ""; }; 36EF35EE26A64C370005E6FC /* InjectedMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InjectedMessage.swift; sourceTree = ""; }; @@ -282,6 +285,7 @@ 36EF35FC26A64C370005E6FC /* Services */ = { isa = PBXGroup; children = ( + 36DA109F2755C2600093E393 /* LtlAPIServices.swift */, 36EF35FD26A64C370005E6FC /* HoloDexServices.swift */, 36EF35FE26A64C370005E6FC /* YouTubeService.swift */, 36EF35FF26A64C370005E6FC /* AppServices.swift */, @@ -693,6 +697,7 @@ 36E6C1FB26A71DF600C2B08C /* TranslatedMessage.swift in Sources */, 3600776326A73653004CB44B /* StreamView.swift in Sources */, 36E6C1FF26A71DF600C2B08C /* AppStep.swift in Sources */, + 36DA10A12755C2600093E393 /* LtlAPIServices.swift in Sources */, 36E6C1FE26A71DF600C2B08C /* DisplayableMessage.swift in Sources */, 36E6C1FC26A71DF600C2B08C /* YouTubeService.swift in Sources */, 36FD3ED326978CC200EAAF51 /* ChatCell.swift in Sources */, @@ -739,6 +744,7 @@ 36EF361026A64C370005E6FC /* AppServices.swift in Sources */, 6636808B261983B500125A76 /* ChatCell.swift in Sources */, 36EF360526A64C370005E6FC /* HoloDexResponse.swift in Sources */, + 36DA10A02755C2600093E393 /* LtlAPIServices.swift in Sources */, 665774AE260C6ECE00072F30 /* HomeView.swift in Sources */, 36EF360926A64C370005E6FC /* BaseView.swift in Sources */, 36EF360F26A64C370005E6FC /* YouTubeService.swift in Sources */, diff --git a/ios/Core/Models/StreamModel.swift b/ios/Core/Models/StreamModel.swift index e540cef..74dd8b1 100644 --- a/ios/Core/Models/StreamModel.swift +++ b/ios/Core/Models/StreamModel.swift @@ -57,7 +57,7 @@ class StreamModel: BaseModel { private let chatURLRelay = BehaviorRelay(value: nil) private let mchadRoomRelay = BehaviorRelay(value: nil) - private let mchadScriptRelay = BehaviorRelay<[DisplayableMessage]>(value: []) + private let mchadScriptRelay = BehaviorRelay<([TranslatedMessage], MchadRoom?)?>(value: ([], nil)) private let metadataRelay = BehaviorRelay(value: nil) private let replayRelay = BehaviorRelay(value: false) @@ -104,6 +104,11 @@ class StreamModel: BaseModel { .subscribe(onNext: loadChat) .disposed(by: bag) + playerRelay.compactMap { $0 } + .map { (id: $0.identifier, duration: $0.duration) } + .subscribe(onNext: loadMchadRooms) + .disposed(by: bag) + chatView.navigationDelegate = self chatURLRelay.compactMap { $0 } .map { URLRequest(url: $0) } @@ -113,6 +118,15 @@ class StreamModel: BaseModel { .map { $0.absoluteString.contains("live_chat_replay") } .bind(to: replayRelay) .disposed(by: bag) + mchadRoomRelay.compactMap { $0 } + .subscribe(onNext: { room in self.loadMchadArchiveScript(room) }) + .disposed(by: bag) + mchadScriptRelay.compactMap { $0 } + .subscribe(onNext: { script, room in + self.appendMchadScriptTls(script, room: room) + }) + .disposed(by: bag) + do { let path = Bundle.main.path(forResource: "WindowInjector", ofType: "js") ?? "" let js = try String(contentsOfFile: path, encoding: .utf8) @@ -156,40 +170,76 @@ class StreamModel: BaseModel { .bind(to: metadataRelay) .disposed(by: bag) } + + private func loadMchadRooms(_ id: String, duration: Double) { + let request = services.mchad.getMchadRoom(id: id, duration: duration) + .asObservable() + .materialize() + + request.map { $0.element } + .bind(to: mchadRoomRelay) + .disposed(by: bag) + request.map { $0.error } + .bind(to: errorRelay) + .disposed(by: bag) + } + + private func loadMchadArchiveScript(_ room: MchadRoom) { + let request = services.mchad.getMchadArchiveTls(room) + .asObservable() + .materialize() + + request.map { $0.element } + .bind(to: mchadScriptRelay) + .disposed(by: bag) + request.map { $0.error } + .subscribe(onError: { error in print(error) }) + .disposed(by: bag) + } + + private func appendMchadScriptTls(_ script: [TranslatedMessage], room: MchadRoom?) { + var mutableScript = script + var mchadOffset: Double = 0.0 + + for displayableMessage in script { + if displayableMessage.message.first == Message.text("--- Stream Starts ---") { + mchadOffset = displayableMessage.showTimestamp + break + } + } + + replayEventRelay.compactMap { $0.current } + .sample(Observable.interval(.milliseconds(500), scheduler: MainScheduler.instance), defaultValue: 0.0) + .subscribe(onNext: { time in + var currentRelay = self.translatedRelay.value + let tmpScript = mutableScript.filter { $0.showTimestamp - mchadOffset <= time * 1000 } + mutableScript.removeFirst(tmpScript.endIndex) + for message in tmpScript { + for lang in message.languages { + if self.services.settings.languages.map({ $0.tag }).contains(lang) || + self.services.settings.languages.map({ $0.description.lowercased().hasPrefix(lang) }).contains(Bool(true)) || + self.services.settings.languages.map({ $0.tag.lowercased().hasPrefix(lang) }).contains(Bool(true)), + !self.services.settings.neverUsers.contains(message.displayAuthor) + { + var mutableMessage = message + mutableMessage.timestamp = Date() + currentRelay.append(mutableMessage) + } + } + } + + if !tmpScript.isEmpty { + self.translatedRelay.accept(currentRelay) + } + }) + .disposed(by: bag) + } private func loadChat(_ id: String, duration: Double) { let request = services.youtube.getYTChatURL(id, videoDuration: duration) .asObservable() .materialize() - -// let mchad = services.mchad.getMchadRoom(id: id, duration: duration) -// .asObservable() -// .materialize() -// .subscribe(onNext: { room in -// self.services.mchad.getMchadLiveTls(id, room: room.element!) -// .asObservable() -// .materialize() -// .subscribe(onNext: { messages in -// if messages.element != nil { -// let push = [messages.element] -// self.mchadScriptRelay.accept(push as! [DisplayableMessage]) -// } -// }) -// .disposed(by: self.bag) -// }) -// .disposed(by: bag) - -// services.mchad.getMchadLiveTls(id, room: nil) -// .asObservable() -// .materialize() -// .subscribe(onNext: { messages in -// if messages.element != nil { -// let push = [messages.element] -// self.mchadScriptRelay.accept(push as! [DisplayableMessage]) -// } -// }) - request.map { $0.element } .bind(to: chatURLRelay) .disposed(by: bag) diff --git a/ios/Core/Services/AppServices.swift b/ios/Core/Services/AppServices.swift index 9a49ee0..dbaddd2 100644 --- a/ios/Core/Services/AppServices.swift +++ b/ios/Core/Services/AppServices.swift @@ -11,6 +11,7 @@ class AppServices { let holodex = HoloDexServices() let youtube = YouTubeService() let mchad = MchadServices() + //let ltl = LtlAPIServices() let settings = SettingsService() init() {} diff --git a/ios/Core/Services/MchadServices.swift b/ios/Core/Services/MchadServices.swift index 9c7c350..93aba10 100644 --- a/ios/Core/Services/MchadServices.swift +++ b/ios/Core/Services/MchadServices.swift @@ -12,8 +12,8 @@ import RxSwift struct MchadServices { init() {} - func getMchadRoom(id: String, duration: Double) -> Single { - return Single.create { observer in + func getMchadRoom(id: String, duration: Double) -> Observable { + return Observable.create { observer in let request: URLRequest if duration > 0 { // is replay @@ -24,19 +24,21 @@ struct MchadServices { let task = URLSession.shared.dataTask(with: request) { data, _, error in if let data = data { do { - //print(room) + let decoder = JSONDecoder() let json = try decoder.decode([MchadRoom].self, from: data) if !json.isEmpty { - observer(.success(json.first!)) + for room in json { + observer.onNext(room) + } } else { print("No mchad room") } } catch { - print(error) + observer.onError(error) } } else if let error = error { - observer(.failure(error)) + observer.onError(error) } } task.resume() @@ -47,82 +49,92 @@ struct MchadServices { } } - func getMchadLiveTls(_ id: String, room: MchadRoom?) -> Observable { - return Observable.create { observer in - let request = URLRequest(url: URL(string: "https://repo.mchatx.org/Listener/?room=Testing")!) - - let task = URLSession.shared.dataTask(with: request) { data, responce, error in - print(data) + func getMchadArchiveOffset(_ room: MchadRoom) -> Single { + Single.create { observer in + var request = URLRequest(url: URL(string: "https://repo.mchatx.org/Archive")!) + request.httpMethod = "POST" + request.addValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept") + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + let jsonObject = NSMutableDictionary() + jsonObject.setValue(room.Link, forKey: "link") + let jsonData: NSData + do { + jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions()) as NSData + request.httpBody = jsonData as Data? + } catch { print(error) - print(responce) + } + + let task = URLSession.shared.dataTask(with: request) { data, _, error in if let data = data { do { let decoder = JSONDecoder() - - let json = try decoder.decode(MchadIncoming.self, from: data) - - print(json) - - let room = MchadRoom(Room: "Testing", Link: nil, Nick: nil, EntryPass: nil, Empty: nil, Pass: nil, StreamLink: nil, Tags: "en", ExtShare: nil, Downloadable: nil) - observer.onNext(TranslatedMessage(from: json.content, room: room)) + + let json = try decoder.decode([MchadScript].self, from: data) + if (json.first(where: { $0.Stext == "--- Stream Starts ---" })?.Stime) != nil { + let offset = json.first(where: { $0.Stext == "--- Stream Starts ---" })!.Stime + + observer(.success(offset)) + } } catch { print(error) + observer(.failure(error)) } + } else if let error = error { + print(error) + observer(.failure(error)) } - } - task.resume() - return Disposables.create() { + + return Disposables.create { task.cancel() } } } - -// func getMchadArchiveTls(_ id: String, room: MchadRoom) -> Single<[TranslatedMessage?]> { -// Single.create { observer in -// var request = URLRequest(url: URL(string: "https://repo.mchatx.org/Archive")!) -// request.httpMethod = "POST" -// request.addValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept") -// request.addValue("application/json", forHTTPHeaderField: "Content-Type") -// -// let jsonObject = NSMutableDictionary() -// jsonObject.setValue(room.Link, forKey: "link") -// let jsonData: NSData -// do { -// jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions()) as NSData -// request.httpBody = jsonData as Data? -// } catch { -// print(error) -// } -// -// let task = URLSession.shared.dataTask(with: request) { data, _, error in -// if let data = data { -// do { -// let decoder = JSONDecoder() -// -// let json = try decoder.decode([MchadScript].self, from: data) -// print(json) -// if (json.first(where: { $0.Stext == "--- Stream Starts ---"})?.Stime) != nil { -// let messages = json.map { TranslatedMessage(from: $0, room: room) } -// -// observer(.success(messages)) -// } -// } catch { -// observer(.failure(error)) -// print(error) -// } -// } else if let error = error { -// print(error) -// observer(.failure(error)) -// } -// } -// task.resume() -// -// return Disposables.create { -// task.cancel() -// } -// } -// } + func getMchadArchiveTls(_ room: MchadRoom) -> Single<([TranslatedMessage], MchadRoom)> { + Single.create { observer in + var request = URLRequest(url: URL(string: "https://repo.mchatx.org/Archive")!) + request.httpMethod = "POST" + request.addValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept") + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + let jsonObject = NSMutableDictionary() + jsonObject.setValue(room.Link, forKey: "link") + let jsonData: NSData + do { + jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: JSONSerialization.WritingOptions()) as NSData + request.httpBody = jsonData as Data? + } catch { + print(error) + } + + let task = URLSession.shared.dataTask(with: request) { data, _, error in + if let data = data { + do { + let decoder = JSONDecoder() + let json = try decoder.decode([MchadScript].self, from: data) + if json.first != nil { + let messages = json.map { TranslatedMessage(from: $0, room: room) } + + observer(.success((messages, room))) + } + } catch { + print(error) + observer(.failure(error)) + } + } else if let error = error { + print(error) + observer(.failure(error)) + } + } + task.resume() + + return Disposables.create { + task.cancel() + } + } + } } diff --git a/ios/Core/Types/DisplayableMessage.swift b/ios/Core/Types/DisplayableMessage.swift index 91eac60..b7fd11b 100644 --- a/ios/Core/Types/DisplayableMessage.swift +++ b/ios/Core/Types/DisplayableMessage.swift @@ -14,6 +14,7 @@ protocol DisplayableMessage { var isMod : Bool { get } var isMember : Bool { get } var superchatData : Superchat? { get } + var isMchad : Bool { get } var sortTimestamp: Date { get } diff --git a/ios/Core/Types/InjectedMessage.swift b/ios/Core/Types/InjectedMessage.swift index fe1706b..6ad9180 100644 --- a/ios/Core/Types/InjectedMessage.swift +++ b/ios/Core/Types/InjectedMessage.swift @@ -28,6 +28,7 @@ struct InjectedMessage: Decodable { } extension InjectedMessage: DisplayableMessage { + var isMchad: Bool { false } var displayAuthor: String { author.name } var displayTimestamp: String { timestamp.toRelative(style: RelativeFormatter.twitterStyle()) } var displayMessage: [Message] { messages } diff --git a/ios/Core/Types/MchadTypes.swift b/ios/Core/Types/MchadTypes.swift index ba3eced..8ccdede 100644 --- a/ios/Core/Types/MchadTypes.swift +++ b/ios/Core/Types/MchadTypes.swift @@ -26,8 +26,9 @@ struct MchadIncoming: Decodable { } struct MchadScript: Decodable { - let _id: String - let Stime: Date + let _id: String? + let Stime: Double + var Stimestamp: Date { Date() } //Doesn't matter, never shown to the user, just for sorting let Stext: String let CC: String? let OC: String? diff --git a/ios/Core/Types/TranslatedMessage.swift b/ios/Core/Types/TranslatedMessage.swift index ceb4714..1e3dd5a 100644 --- a/ios/Core/Types/TranslatedMessage.swift +++ b/ios/Core/Types/TranslatedMessage.swift @@ -13,9 +13,10 @@ struct TranslatedMessage { let message: [Message] let languages: [String] - let timestamp: Date + var timestamp: Date let show: Double let superchat: Superchat? + let isMchad: Bool init?(from message: InjectedMessage) { self.author = Author(from: message.author) @@ -84,15 +85,17 @@ struct TranslatedMessage { let mess = m.compactMap { $0 } self.message = mess self.languages = lang + self.isMchad = false } init(from mchad: MchadScript, room: MchadRoom) { self.author = Author(from: room) self.message = [Message.text(mchad.Stext)] self.languages = [room.Tags] - self.timestamp = mchad.Stime - self.show = mchad.Stime.timeIntervalSince1970 + self.timestamp = mchad.Stimestamp + self.show = mchad.Stime self.superchat = nil + self.isMchad = true } struct Author { diff --git a/ios/Views/StreamView/ChatCell/ChatCell.swift b/ios/Views/StreamView/ChatCell/ChatCell.swift index ddb0427..c0653b9 100644 --- a/ios/Views/StreamView/ChatCell/ChatCell.swift +++ b/ios/Views/StreamView/ChatCell/ChatCell.swift @@ -22,6 +22,9 @@ class ChatCell: UITableViewCell { func configure(_ item: DisplayableMessage, useTimestamps: Bool) { author.text = item.displayAuthor timestamp.text = useTimestamps ? item.displayTimestamp : "" + if item.isMchad { + timestamp.text = "Mchad" + } // This should reset the cell, so we aviod duplicate superchats and members timestamp.font = .systemFont(ofSize: 17) @@ -37,6 +40,7 @@ class ChatCell: UITableViewCell { contentView.layer.cornerRadius = 10 contentView.backgroundColor = item.superchatData?.UIcolor } + if item.isMember { author.textColor = UIColor(red: 44/255, green: 166/255, blue: 63/255, alpha: 1) diff --git a/ios/en.lproj/Localizeable.strings b/ios/en.lproj/Localizeable.strings index 4642e43..785158b 100644 --- a/ios/en.lproj/Localizeable.strings +++ b/ios/en.lproj/Localizeable.strings @@ -200,3 +200,6 @@ /* a setting to enable english channel names */ "Use English Channel Names" = "Use English Channel Names"; + +/* a button that takes you to the About LiveTL screen */ +"About LiveTL" = "About LiveTL"; diff --git a/ios/ios.entitlements b/ios/ios.entitlements index 2eb7e33..4da9e8f 100644 --- a/ios/ios.entitlements +++ b/ios/ios.entitlements @@ -2,7 +2,11 @@ + com.apple.security.app-sandbox + com.apple.security.application-groups + com.apple.security.network.client +