Merge branch 'feat/implementing-custom-webrtc-implementation' of beep/frontend_flutter into master
Finally completed working WebRTC support only for iPhonepull/55/head
commit
2b3891f2fe
|
@ -40,6 +40,12 @@ target 'Runner' do
|
|||
system('rm -rf .symlinks')
|
||||
system('mkdir -p .symlinks/plugins')
|
||||
|
||||
# Custom Pods
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
pod 'GoogleWebRTC'
|
||||
pod 'Just'
|
||||
pod 'PercentEncoder'
|
||||
|
||||
# Flutter Pods
|
||||
generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig')
|
||||
if generated_xcode_build_settings.empty?
|
||||
|
|
|
@ -12,10 +12,13 @@
|
|||
3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
|
||||
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
4A87800125259F21EA397584 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6FE3C699402F0E32606816A /* Pods_Runner.framework */; };
|
||||
59A38837224A3BEA00697DC5 /* PeerConnectionWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59A38836224A3BEA00697DC5 /* PeerConnectionWrapper.swift */; };
|
||||
59B31C93224E0FFA000E7B7D /* PeerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B31C92224E0FFA000E7B7D /* PeerManager.swift */; };
|
||||
59B31C95224E101D000E7B7D /* SignalingApiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B31C94224E101D000E7B7D /* SignalingApiProvider.swift */; };
|
||||
59B31C97224E1B82000E7B7D /* EventSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B31C96224E1B82000E7B7D /* EventSource.swift */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
|
||||
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
|
@ -41,6 +44,10 @@
|
|||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
|
||||
59A38836224A3BEA00697DC5 /* PeerConnectionWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerConnectionWrapper.swift; sourceTree = "<group>"; };
|
||||
59B31C92224E0FFA000E7B7D /* PeerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerManager.swift; sourceTree = "<group>"; };
|
||||
59B31C94224E101D000E7B7D /* SignalingApiProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalingApiProvider.swift; sourceTree = "<group>"; };
|
||||
59B31C96224E1B82000E7B7D /* EventSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSource.swift; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
|
@ -118,9 +125,13 @@
|
|||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
97C146F11CF9000F007C117D /* Supporting Files */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
59B31C96224E1B82000E7B7D /* EventSource.swift */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
59A38836224A3BEA00697DC5 /* PeerConnectionWrapper.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
59B31C92224E0FFA000E7B7D /* PeerManager.swift */,
|
||||
59B31C94224E101D000E7B7D /* SignalingApiProvider.swift */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
|
@ -170,11 +181,12 @@
|
|||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0910;
|
||||
LastUpgradeCheck = 1010;
|
||||
ORGANIZATIONNAME = "The Chromium Authors";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
DevelopmentTeam = 7SW7CWZBQM;
|
||||
LastSwiftMigration = 0910;
|
||||
};
|
||||
};
|
||||
|
@ -184,6 +196,7 @@
|
|||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
|
@ -204,7 +217,6 @@
|
|||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
|
@ -268,6 +280,9 @@
|
|||
"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
|
||||
"${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework",
|
||||
"${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework",
|
||||
"${PODS_ROOT}/GoogleWebRTC/Frameworks/frameworks/WebRTC.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/Just/Just.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/PercentEncoder/PercentEncoder.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework",
|
||||
);
|
||||
|
@ -275,6 +290,9 @@
|
|||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Just.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PercentEncoder.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
|
||||
);
|
||||
|
@ -291,7 +309,11 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
59B31C95224E101D000E7B7D /* SignalingApiProvider.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
59B31C97224E1B82000E7B7D /* EventSource.swift in Sources */,
|
||||
59A38837224A3BEA00697DC5 /* PeerConnectionWrapper.swift in Sources */,
|
||||
59B31C93224E0FFA000E7B7D /* PeerManager.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -331,12 +353,14 @@
|
|||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
|
@ -371,7 +395,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = S8QB4VV633;
|
||||
DEVELOPMENT_TEAM = 7SW7CWZBQM;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -404,12 +428,14 @@
|
|||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
|
@ -458,12 +484,14 @@
|
|||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
|
@ -500,6 +528,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 7SW7CWZBQM;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -528,6 +557,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 7SW7CWZBQM;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<?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>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0910"
|
||||
LastUpgradeVersion = "1010"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -26,7 +26,6 @@
|
|||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
|
@ -46,7 +45,6 @@
|
|||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
|
|
@ -6,7 +6,43 @@ import Flutter
|
|||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
) -> Bool {
|
||||
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
|
||||
let conversationChannel = FlutterMethodChannel(name: "beepvoice.app/conversation", binaryMessenger: controller)
|
||||
let peerManager = PeerManager()
|
||||
|
||||
conversationChannel.setMethodCallHandler({
|
||||
(call: FlutterMethodCall, result: FlutterResult) -> Void in
|
||||
|
||||
switch call.method {
|
||||
case "init":
|
||||
if let authToken: String = call.arguments as? String {
|
||||
peerManager.initializeToken(authToken: authToken)
|
||||
}
|
||||
result(0)
|
||||
return
|
||||
case "join":
|
||||
if let conversationId: String = call.arguments as? String {
|
||||
peerManager.join(conversationId: conversationId)
|
||||
}
|
||||
result(0)
|
||||
return
|
||||
case "exit":
|
||||
peerManager.exit()
|
||||
result(0)
|
||||
return
|
||||
case "get":
|
||||
if let activeConversation = peerManager.get() {
|
||||
result(activeConversation)
|
||||
} else {
|
||||
result("")
|
||||
}
|
||||
return
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
return
|
||||
}
|
||||
})
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
|
@ -14,9 +18,9 @@
|
|||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
|
|
|
@ -0,0 +1,385 @@
|
|||
//
|
||||
// EventSource.swift
|
||||
// EventSource
|
||||
//
|
||||
// Created by Andres on 2/13/15.
|
||||
// Copyright (c) 2015 Inaka. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum EventSourceState {
|
||||
case connecting
|
||||
case open
|
||||
case closed
|
||||
}
|
||||
|
||||
open class EventSource: NSObject, URLSessionDataDelegate {
|
||||
static let DefaultsKey = "com.inaka.eventSource.lastEventId"
|
||||
|
||||
let url: URL
|
||||
fileprivate let lastEventIDKey: String
|
||||
fileprivate let receivedString: NSString?
|
||||
fileprivate var onOpenCallback: (() -> Void)?
|
||||
fileprivate var onErrorCallback: ((NSError?) -> Void)?
|
||||
fileprivate var onMessageCallback: ((_ id: String?, _ event: String?, _ data: String?) -> Void)?
|
||||
fileprivate var eventListeners = Dictionary<String, (_ id: String?, _ event: String?, _ data: String?) -> Void>()
|
||||
fileprivate var headers: Dictionary<String, String>
|
||||
fileprivate var operationQueue: OperationQueue
|
||||
fileprivate var errorBeforeSetErrorCallBack: NSError?
|
||||
fileprivate let uniqueIdentifier: String
|
||||
fileprivate let validNewlineCharacters = ["\r\n", "\n", "\r"]
|
||||
|
||||
open internal(set) var readyState: EventSourceState
|
||||
open fileprivate(set) var retryTime = 3000
|
||||
|
||||
internal var urlSession: Foundation.URLSession?
|
||||
internal var task: URLSessionDataTask?
|
||||
internal let receivedDataBuffer: NSMutableData
|
||||
|
||||
var event = Dictionary<String, String>()
|
||||
|
||||
public init(url: String, headers: [String : String] = [:]) {
|
||||
self.url = URL(string: url)!
|
||||
self.headers = headers
|
||||
self.readyState = EventSourceState.closed
|
||||
self.operationQueue = OperationQueue()
|
||||
self.receivedString = nil
|
||||
self.receivedDataBuffer = NSMutableData()
|
||||
|
||||
let port = String(self.url.port ?? 80)
|
||||
let relativePath = self.url.relativePath
|
||||
let host = self.url.host ?? ""
|
||||
let scheme = self.url.scheme ?? ""
|
||||
|
||||
self.uniqueIdentifier = "\(scheme).\(host).\(port).\(relativePath)"
|
||||
self.lastEventIDKey = "\(EventSource.DefaultsKey).\(self.uniqueIdentifier)"
|
||||
|
||||
super.init()
|
||||
self.connect()
|
||||
}
|
||||
|
||||
//Mark: Connect
|
||||
|
||||
func connect() {
|
||||
var additionalHeaders = self.headers
|
||||
if let eventID = self.lastEventID {
|
||||
additionalHeaders["Last-Event-Id"] = eventID
|
||||
}
|
||||
|
||||
additionalHeaders["Accept"] = "text/event-stream"
|
||||
additionalHeaders["Cache-Control"] = "no-cache"
|
||||
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.timeoutIntervalForRequest = TimeInterval(INT_MAX)
|
||||
configuration.timeoutIntervalForResource = TimeInterval(INT_MAX)
|
||||
configuration.httpAdditionalHeaders = additionalHeaders
|
||||
|
||||
self.readyState = EventSourceState.connecting
|
||||
self.urlSession = newSession(configuration)
|
||||
self.task = urlSession!.dataTask(with: self.url)
|
||||
|
||||
self.resumeSession()
|
||||
}
|
||||
|
||||
internal func resumeSession() {
|
||||
self.task!.resume()
|
||||
}
|
||||
|
||||
internal func newSession(_ configuration: URLSessionConfiguration) -> URLSession {
|
||||
return URLSession(
|
||||
configuration: configuration,
|
||||
delegate: self,
|
||||
delegateQueue: operationQueue
|
||||
)
|
||||
}
|
||||
|
||||
//Mark: Close
|
||||
|
||||
open func close() {
|
||||
self.readyState = EventSourceState.closed
|
||||
self.urlSession?.invalidateAndCancel()
|
||||
}
|
||||
|
||||
fileprivate func receivedMessageToClose(_ httpResponse: HTTPURLResponse?) -> Bool {
|
||||
guard let response = httpResponse else {
|
||||
return false
|
||||
}
|
||||
|
||||
if response.statusCode == 204 {
|
||||
self.close()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//Mark: EventListeners
|
||||
|
||||
open func onOpen(_ onOpenCallback: @escaping (() -> Void)) {
|
||||
self.onOpenCallback = onOpenCallback
|
||||
}
|
||||
|
||||
open func onError(_ onErrorCallback: @escaping ((NSError?) -> Void)) {
|
||||
self.onErrorCallback = onErrorCallback
|
||||
|
||||
if let errorBeforeSet = self.errorBeforeSetErrorCallBack {
|
||||
self.onErrorCallback!(errorBeforeSet)
|
||||
self.errorBeforeSetErrorCallBack = nil
|
||||
}
|
||||
}
|
||||
|
||||
open func onMessage(_ onMessageCallback: @escaping ((_ id: String?, _ event: String?, _ data: String?) -> Void)) {
|
||||
self.onMessageCallback = onMessageCallback
|
||||
}
|
||||
|
||||
open func addEventListener(_ event: String, handler: @escaping ((_ id: String?, _ event: String?, _ data: String?) -> Void)) {
|
||||
self.eventListeners[event] = handler
|
||||
}
|
||||
|
||||
open func removeEventListener(_ event: String) -> Void {
|
||||
self.eventListeners.removeValue(forKey: event)
|
||||
}
|
||||
|
||||
open func events() -> Array<String> {
|
||||
return Array(self.eventListeners.keys)
|
||||
}
|
||||
|
||||
//MARK: URLSessionDataDelegate
|
||||
|
||||
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
if self.receivedMessageToClose(dataTask.response as? HTTPURLResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
if self.readyState != EventSourceState.open {
|
||||
return
|
||||
}
|
||||
|
||||
self.receivedDataBuffer.append(data)
|
||||
let eventStream = extractEventsFromBuffer()
|
||||
self.parseEventStream(eventStream)
|
||||
}
|
||||
|
||||
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
||||
completionHandler(URLSession.ResponseDisposition.allow)
|
||||
|
||||
if self.receivedMessageToClose(dataTask.response as? HTTPURLResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
self.readyState = EventSourceState.open
|
||||
if self.onOpenCallback != nil {
|
||||
DispatchQueue.main.async {
|
||||
self.onOpenCallback!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
self.readyState = EventSourceState.closed
|
||||
|
||||
if self.receivedMessageToClose(task.response as? HTTPURLResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
guard let urlResponse = task.response as? HTTPURLResponse else {
|
||||
return
|
||||
}
|
||||
|
||||
if !hasHttpError(code: urlResponse.statusCode) && (error == nil || (error! as NSError).code != -999) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(retryTime)) {
|
||||
self.connect()
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
var theError: NSError? = error as NSError?
|
||||
|
||||
if self.hasHttpError(code: urlResponse.statusCode) {
|
||||
theError = NSError(
|
||||
domain: "com.inaka.eventSource.error",
|
||||
code: -1,
|
||||
userInfo: ["message": "HTTP Status Code: \(urlResponse.statusCode)"]
|
||||
)
|
||||
self.close()
|
||||
}
|
||||
|
||||
if let errorCallback = self.onErrorCallback {
|
||||
errorCallback(theError)
|
||||
} else {
|
||||
self.errorBeforeSetErrorCallBack = theError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: Helpers
|
||||
|
||||
fileprivate func extractEventsFromBuffer() -> [String] {
|
||||
var events = [String]()
|
||||
|
||||
// Find first occurrence of delimiter
|
||||
var searchRange = NSRange(location: 0, length: receivedDataBuffer.length)
|
||||
while let foundRange = searchForEventInRange(searchRange) {
|
||||
// Append event
|
||||
if foundRange.location > searchRange.location {
|
||||
let dataChunk = receivedDataBuffer.subdata(
|
||||
with: NSRange(location: searchRange.location, length: foundRange.location - searchRange.location)
|
||||
)
|
||||
|
||||
if let text = String(bytes: dataChunk, encoding: .utf8) {
|
||||
events.append(text)
|
||||
}
|
||||
}
|
||||
// Search for next occurrence of delimiter
|
||||
searchRange.location = foundRange.location + foundRange.length
|
||||
searchRange.length = receivedDataBuffer.length - searchRange.location
|
||||
}
|
||||
|
||||
// Remove the found events from the buffer
|
||||
self.receivedDataBuffer.replaceBytes(in: NSRange(location: 0, length: searchRange.location), withBytes: nil, length: 0)
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
fileprivate func searchForEventInRange(_ searchRange: NSRange) -> NSRange? {
|
||||
let delimiters = validNewlineCharacters.map { "\($0)\($0)".data(using: String.Encoding.utf8)! }
|
||||
|
||||
for delimiter in delimiters {
|
||||
let foundRange = receivedDataBuffer.range(of: delimiter,
|
||||
options: NSData.SearchOptions(),
|
||||
in: searchRange)
|
||||
if foundRange.location != NSNotFound {
|
||||
return foundRange
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func parseEventStream(_ events: [String]) {
|
||||
var parsedEvents: [(id: String?, event: String?, data: String?)] = Array()
|
||||
|
||||
for event in events {
|
||||
if event.isEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
if event.hasPrefix(":") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (event as NSString).contains("retry:") {
|
||||
if let reconnectTime = parseRetryTime(event) {
|
||||
self.retryTime = reconnectTime
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
parsedEvents.append(parseEvent(event))
|
||||
}
|
||||
|
||||
for parsedEvent in parsedEvents {
|
||||
self.lastEventID = parsedEvent.id
|
||||
|
||||
if parsedEvent.event == nil {
|
||||
if let data = parsedEvent.data, let onMessage = self.onMessageCallback {
|
||||
DispatchQueue.main.async {
|
||||
onMessage(self.lastEventID, "message", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let event = parsedEvent.event, let data = parsedEvent.data, let eventHandler = self.eventListeners[event] {
|
||||
DispatchQueue.main.async {
|
||||
eventHandler(self.lastEventID, event, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal var lastEventID: String? {
|
||||
set {
|
||||
if let lastEventID = newValue {
|
||||
let defaults = UserDefaults.standard
|
||||
defaults.set(lastEventID, forKey: lastEventIDKey)
|
||||
defaults.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
get {
|
||||
let defaults = UserDefaults.standard
|
||||
|
||||
if let lastEventID = defaults.string(forKey: lastEventIDKey) {
|
||||
return lastEventID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func parseEvent(_ eventString: String) -> (id: String?, event: String?, data: String?) {
|
||||
var event = Dictionary<String, String>()
|
||||
|
||||
for line in eventString.components(separatedBy: CharacterSet.newlines) as [String] {
|
||||
autoreleasepool {
|
||||
let (k, value) = self.parseKeyValuePair(line)
|
||||
guard let key = k else { return }
|
||||
|
||||
if let value = value {
|
||||
if event[key] != nil {
|
||||
event[key] = "\(event[key]!)\n\(value)"
|
||||
} else {
|
||||
event[key] = value
|
||||
}
|
||||
} else if value == nil {
|
||||
event[key] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (event["id"], event["event"], event["data"])
|
||||
}
|
||||
|
||||
fileprivate func parseKeyValuePair(_ line: String) -> (String?, String?) {
|
||||
var key: NSString?, value: NSString?
|
||||
let scanner = Scanner(string: line)
|
||||
scanner.scanUpTo(":", into: &key)
|
||||
scanner.scanString(":", into: nil)
|
||||
|
||||
for newline in validNewlineCharacters {
|
||||
if scanner.scanUpTo(newline, into: &value) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (key as String?, value as String?)
|
||||
}
|
||||
|
||||
fileprivate func parseRetryTime(_ eventString: String) -> Int? {
|
||||
var reconnectTime: Int?
|
||||
let separators = CharacterSet(charactersIn: ":")
|
||||
if let milli = eventString.components(separatedBy: separators).last {
|
||||
let milliseconds = trim(milli)
|
||||
|
||||
if let intMiliseconds = Int(milliseconds) {
|
||||
reconnectTime = intMiliseconds
|
||||
}
|
||||
}
|
||||
return reconnectTime
|
||||
}
|
||||
|
||||
fileprivate func trim(_ string: String) -> String {
|
||||
return string.trimmingCharacters(in: CharacterSet.whitespaces)
|
||||
}
|
||||
|
||||
fileprivate func hasHttpError(code: Int) -> Bool {
|
||||
return code >= 400
|
||||
}
|
||||
|
||||
class open func basicAuth(_ username: String, password: String) -> String {
|
||||
let authString = "\(username):\(password)"
|
||||
let authData = authString.data(using: String.Encoding.utf8)
|
||||
let base64String = authData!.base64EncodedString(options: [])
|
||||
|
||||
return "Basic \(base64String)"
|
||||
}
|
||||
}
|
|
@ -32,6 +32,11 @@
|
|||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
@ -41,7 +46,7 @@
|
|||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>$(PRODUCT_NAME) Microphone Usage!</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>$(PRODUCT_NAME) Microphone Usage!</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -0,0 +1,232 @@
|
|||
//
|
||||
// RTCWrapper.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by Sudharshan on 3/26/19.
|
||||
// Copyright © 2019 The Chromium Authors. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WebRTC
|
||||
|
||||
public enum RTCWrapperState {
|
||||
case disconnected
|
||||
case connected
|
||||
case connecting
|
||||
}
|
||||
|
||||
class PeerConnectionWrapper: NSObject{
|
||||
// State
|
||||
var state: RTCWrapperState = .disconnected
|
||||
var remoteUserId: String?
|
||||
var remoteDeviceId: String?
|
||||
|
||||
// WebRTC initialization
|
||||
var connectionFactory: RTCPeerConnectionFactory?
|
||||
var signalingApiProvider: SignalingApiProvider?
|
||||
var peerConnection: RTCPeerConnection?
|
||||
var remoteIceCandidates: [RTCIceCandidate] = []
|
||||
|
||||
// Constant configuration defaults
|
||||
let iceServers: [RTCIceServer] = [RTCIceServer(urlStrings: ["stun:stun.l.google.com:19302"])]
|
||||
let connectionConstraint = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: ["DtlsSrtpKeyAgreement": "true"])
|
||||
let channelConstraint = RTCMediaConstraints(mandatoryConstraints: ["OfferToReceiveAudio" : "true"], optionalConstraints: nil)
|
||||
|
||||
// Streams
|
||||
var localMediaStream: RTCMediaStream!
|
||||
var localAudioTrack: RTCAudioTrack!
|
||||
var remoteAudioTrack: RTCAudioTrack!
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
public convenience init(connectionFactory: RTCPeerConnectionFactory, signalingApiProvider: SignalingApiProvider, remoteUserId: String, remoteDeviceId: String) {
|
||||
self.init()
|
||||
self.connectionFactory = connectionFactory
|
||||
self.signalingApiProvider = signalingApiProvider
|
||||
self.remoteUserId = remoteUserId
|
||||
self.remoteDeviceId = remoteDeviceId
|
||||
|
||||
initialisePeerConnection()
|
||||
}
|
||||
|
||||
public func connect() {
|
||||
if let peerConnection = self.peerConnection {
|
||||
self.state = .connecting
|
||||
let localStream = self.localStream()
|
||||
peerConnection.add(localStream)
|
||||
}
|
||||
}
|
||||
|
||||
public func disconnect() {
|
||||
if let peerConnection = self.peerConnection {
|
||||
peerConnection.close()
|
||||
if let stream = peerConnection.localStreams.first {
|
||||
peerConnection.remove(stream)
|
||||
}
|
||||
self.state = .disconnected
|
||||
}
|
||||
}
|
||||
|
||||
public func createOffer(userId: String, deviceId: String) {
|
||||
if let peerConnection = self.peerConnection {
|
||||
|
||||
peerConnection.offer(for: self.channelConstraint, completionHandler: { [weak self] (sdp, error) in
|
||||
// Exit if this object doesn't exist anymore cause it is a weak link
|
||||
guard self != nil else { return }
|
||||
|
||||
if let error = error {
|
||||
print(error)
|
||||
} else {
|
||||
// Use the sdp generated
|
||||
self?.handleLocalSdpSet(sdp: sdp)
|
||||
self?.signalingApiProvider?.postDataToUser(userId: userId, deviceId: deviceId, data: sdp!.sdp, event: "offer")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func handleAnswer(remoteSdp: String) {
|
||||
if let peerConnection = self.peerConnection {
|
||||
let sessionDescription = RTCSessionDescription.init(type: .answer, sdp: remoteSdp)
|
||||
|
||||
peerConnection.setRemoteDescription(sessionDescription, completionHandler: { [weak self] (error) in
|
||||
// Exit if this object doesn't exist anymore cause it is a weak link
|
||||
guard let this = self else { return }
|
||||
|
||||
if let error = error {
|
||||
// Throw an error
|
||||
print(error)
|
||||
} else {
|
||||
// handle the remote sdp
|
||||
this.handleRemoteDescriptionSet()
|
||||
this.state = .connected
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func createAnswerForOffer(userId: String, deviceId: String, remoteSdp: String) {
|
||||
if let peerConnection = self.peerConnection {
|
||||
let sessionDescription = RTCSessionDescription.init(type: .offer, sdp: remoteSdp)
|
||||
peerConnection.setRemoteDescription(sessionDescription, completionHandler: { [weak self] (error) in
|
||||
// Exit if this object doesn't exist anymore cause it is a weak link
|
||||
guard let this = self else { return }
|
||||
|
||||
if let error = error {
|
||||
// Throw an error
|
||||
print(error)
|
||||
} else {
|
||||
// handle the remote sdp
|
||||
this.handleRemoteDescriptionSet()
|
||||
|
||||
// create answer
|
||||
peerConnection.answer(for: this.channelConstraint, completionHandler:
|
||||
{ (sdp, error) in
|
||||
if let error = error {
|
||||
// Throw an error
|
||||
print(error)
|
||||
} else {
|
||||
// handle generated local sdp
|
||||
self?.handleLocalSdpSet(sdp: sdp)
|
||||
self?.signalingApiProvider?.postDataToUser(userId: userId, deviceId: deviceId, data: sdp!.sdp, event: "answer")
|
||||
this.state = .connected
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func addIceCandidate(iceCandidate: RTCIceCandidate) {
|
||||
// Set ice candidate after setting remote description
|
||||
if self.peerConnection?.remoteDescription != nil {
|
||||
self.peerConnection?.add(iceCandidate)
|
||||
} else {
|
||||
self.remoteIceCandidates.append(iceCandidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PeerConnectionWrapper {
|
||||
/*
|
||||
func initialisePeerConnectionFactory () {
|
||||
RTCPeerConnectionFactory.initialize()
|
||||
self.connectionFactory = RTCPeerConnectionFactory()
|
||||
}*/
|
||||
|
||||
func initialisePeerConnection () {
|
||||
let configuration = RTCConfiguration()
|
||||
configuration.iceServers = self.iceServers;
|
||||
self.peerConnection = self.connectionFactory?.peerConnection(with: configuration, constraints: self.connectionConstraint, delegate: self)
|
||||
}
|
||||
|
||||
func handleLocalSdpSet(sdp: RTCSessionDescription?) {
|
||||
guard let sdp = sdp else{
|
||||
return
|
||||
}
|
||||
|
||||
self.peerConnection?.setLocalDescription(sdp, completionHandler: {[weak self] (error) in
|
||||
guard let _ = self, let error = error else { return }
|
||||
print(error)
|
||||
})
|
||||
}
|
||||
|
||||
func handleRemoteDescriptionSet() {
|
||||
for iceCandidate in self.remoteIceCandidates {
|
||||
self.peerConnection?.add(iceCandidate)
|
||||
}
|
||||
self.remoteIceCandidates = []
|
||||
}
|
||||
|
||||
func localStream() -> RTCMediaStream {
|
||||
let factory = self.connectionFactory!
|
||||
let localStream = factory.mediaStream(withStreamId: "RTCmS")
|
||||
|
||||
let audioTrack = factory.audioTrack(withTrackId: "RTCaS0")
|
||||
localStream.addAudioTrack(audioTrack)
|
||||
|
||||
return localStream
|
||||
}
|
||||
}
|
||||
|
||||
extension PeerConnectionWrapper: RTCPeerConnectionDelegate {
|
||||
func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {
|
||||
|
||||
}
|
||||
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) {
|
||||
|
||||
}
|
||||
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
|
||||
print("adding new stream from remote")
|
||||
self.remoteAudioTrack = stream.audioTracks[0]
|
||||
}
|
||||
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {
|
||||
|
||||
}
|
||||
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
|
||||
|
||||
}
|
||||
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) {
|
||||
|
||||
}
|
||||
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) {
|
||||
let candidateString = "\(candidate.sdp)::\(candidate.sdpMLineIndex)::\(candidate.sdpMid ?? "")"
|
||||
self.signalingApiProvider?.postDataToUser(userId: remoteUserId!, deviceId: remoteDeviceId!, data: candidateString, event: "ice-candidate")
|
||||
}
|
||||
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {
|
||||
|
||||
}
|
||||
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
//
|
||||
// PeerManager.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by Sudharshan on 3/29/19.
|
||||
// Copyright © 2019 The Chromium Authors. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WebRTC
|
||||
import PercentEncoder
|
||||
|
||||
class PeerManager: NSObject {
|
||||
// WebRTC initialization
|
||||
var connectionFactory: RTCPeerConnectionFactory?
|
||||
var signalingApiProvider: SignalingApiProvider?
|
||||
var eventSource: EventSource?
|
||||
|
||||
// List of users
|
||||
var peerList: [String: PeerConnectionWrapper] = [:]
|
||||
var whitePeerList: [String] = []
|
||||
var activeConversation: String?
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
RTCPeerConnectionFactory.initialize()
|
||||
self.connectionFactory = RTCPeerConnectionFactory()
|
||||
}
|
||||
|
||||
// MUST CALL THIS BEFORE SIGNALLING WORKS
|
||||
public func initializeToken(authToken: String) {
|
||||
self.signalingApiProvider = SignalingApiProvider(authToken: authToken)
|
||||
self.eventSource = EventSource(url: "http://localhost/signal/subscribe?token=\(authToken)")
|
||||
initialiseEventSource()
|
||||
}
|
||||
|
||||
public func join(conversationId: String) {
|
||||
let userOpList = signalingApiProvider?.getConversationUsers(conversationId: conversationId)
|
||||
activeConversation = conversationId
|
||||
|
||||
guard let userList = userOpList else {
|
||||
// Error incorrect conversation ID
|
||||
return
|
||||
}
|
||||
|
||||
for user in userList {
|
||||
let deviceOpList = signalingApiProvider?.getUserDevices(userId: user)
|
||||
|
||||
guard let deviceList = deviceOpList else {
|
||||
// Error incorrect user ID
|
||||
return
|
||||
}
|
||||
|
||||
for device in deviceList {
|
||||
let connection: PeerConnectionWrapper = PeerConnectionWrapper(connectionFactory: self.connectionFactory!, signalingApiProvider: self.signalingApiProvider!, remoteUserId: user, remoteDeviceId: device)
|
||||
self.peerList["\(user):\(device)"] = connection
|
||||
whitePeerList.append("\(user):\(device)")
|
||||
|
||||
connection.connect()
|
||||
connection.createOffer(userId: user, deviceId: device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func exit() {
|
||||
for (_, connection) in peerList {
|
||||
connection.disconnect()
|
||||
}
|
||||
|
||||
whitePeerList = []
|
||||
peerList = [:]
|
||||
activeConversation = nil
|
||||
}
|
||||
|
||||
public func get() -> String? {
|
||||
return activeConversation
|
||||
}
|
||||
}
|
||||
|
||||
private extension PeerManager {
|
||||
func initialiseEventSource() {
|
||||
eventSource?.addEventListener("offer") { (id, event, data) in
|
||||
guard let id = id, let data = data else {
|
||||
// Incorrect packet type error
|
||||
return
|
||||
}
|
||||
|
||||
// Handling offers, if in list accept
|
||||
if self.whitePeerList.contains(id) {
|
||||
// Split id into user and device
|
||||
let idArr = id.components(separatedBy: ":")
|
||||
|
||||
// Check id format
|
||||
if idArr.count != 2 {
|
||||
// Incorrect id format error
|
||||
return
|
||||
}
|
||||
|
||||
// Formatting data
|
||||
let decoded_data = PercentEncoding.decodeURI.evaluate(string: data)
|
||||
|
||||
print("OFFER!")
|
||||
|
||||
let connection: PeerConnectionWrapper = PeerConnectionWrapper(connectionFactory: self.connectionFactory!, signalingApiProvider: self.signalingApiProvider!, remoteUserId: idArr[0], remoteDeviceId: idArr[1])
|
||||
self.peerList[id] = connection
|
||||
|
||||
connection.connect()
|
||||
connection.createAnswerForOffer(userId: idArr[0], deviceId: idArr[1], remoteSdp: decoded_data)
|
||||
}
|
||||
}
|
||||
|
||||
eventSource?.addEventListener("answer") { (id, event, data) in
|
||||
|
||||
guard let id = id, let data = data else {
|
||||
// Incorrect packet type error
|
||||
return
|
||||
}
|
||||
|
||||
// Handling answers, if in list accept
|
||||
if self.whitePeerList.contains(id) {
|
||||
// Formatting data
|
||||
let decoded_data = PercentEncoding.decodeURI.evaluate(string: data)
|
||||
|
||||
print("ANSWER")
|
||||
|
||||
let connection: PeerConnectionWrapper = self.peerList[id]!
|
||||
connection.handleAnswer(remoteSdp: decoded_data)
|
||||
}
|
||||
}
|
||||
|
||||
eventSource?.addEventListener("ice-candidate") { (id, event, data) in
|
||||
|
||||
guard let id = id, let data = data else {
|
||||
// Incorrect packet type error
|
||||
return
|
||||
}
|
||||
|
||||
// Handling ice candidates, if in list accept
|
||||
if self.whitePeerList.contains(id) {
|
||||
let connection: PeerConnectionWrapper = self.peerList[id]!
|
||||
|
||||
// Formatting data
|
||||
let decoded_data = PercentEncoding.decodeURI.evaluate(string: data)
|
||||
|
||||
print("ICE!")
|
||||
|
||||
let dataArr = decoded_data.components(separatedBy: "::")
|
||||
|
||||
// Check dataArr size of 3
|
||||
if dataArr.count != 3 {
|
||||
// Incorrect data format error
|
||||
return
|
||||
}
|
||||
|
||||
// Convert sdpMLineIndex to Int32
|
||||
guard let sdpMLineIndex = Int32(dataArr[1]) else {
|
||||
// Invalid sdpMLineIndex error
|
||||
return
|
||||
}
|
||||
|
||||
let iceCandidate = RTCIceCandidate(sdp: dataArr[0], sdpMLineIndex: sdpMLineIndex, sdpMid: dataArr[2])
|
||||
connection.addIceCandidate(iceCandidate: iceCandidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// SignalingApiProvider.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by Sudharshan on 3/29/19.
|
||||
// Copyright © 2019 The Chromium Authors. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WebRTC
|
||||
import Just
|
||||
|
||||
class SignalingApiProvider: NSObject {
|
||||
var authToken: String?
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
public convenience init(authToken: String) {
|
||||
self.init()
|
||||
self.authToken = authToken
|
||||
}
|
||||
|
||||
public func getUserDevices(userId: String) -> [String]? {
|
||||
var deviceList: [String] = []
|
||||
let response = Just.get("http://localhost/signal/user/\(userId)/devices",
|
||||
headers: ["Authorization": "Bearer \(authToken ?? "0")"])
|
||||
|
||||
if(response.ok) {
|
||||
do {
|
||||
if let jsonResult = try JSONSerialization.jsonObject(with: response.content!, options: []) as? [String] {
|
||||
// convert this to an array of strings
|
||||
for device in jsonResult {
|
||||
deviceList.append(device)
|
||||
}
|
||||
|
||||
return deviceList
|
||||
} else {
|
||||
// Invalid response format
|
||||
return nil
|
||||
}
|
||||
} catch let error as NSError {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func getConversationUsers(conversationId: String) -> [String]? {
|
||||
var userList: [String] = []
|
||||
let response = Just.get("http://localhost/core/user/conversation/\(conversationId)/member",
|
||||
headers: ["Authorization": "Bearer \(authToken ?? "0")"])
|
||||
|
||||
if (response.ok) {
|
||||
do {
|
||||
if let jsonResult = try JSONSerialization.jsonObject(with: response.content!, options: []) as? [Any] {
|
||||
// Need code to convert this to an array of strings
|
||||
for user in jsonResult {
|
||||
if let userObject = user as? [String: String] {
|
||||
guard let userId = userObject["id"] else {
|
||||
// Invalid response format
|
||||
return nil
|
||||
}
|
||||
|
||||
userList.append(userId)
|
||||
}
|
||||
}
|
||||
|
||||
return userList
|
||||
}
|
||||
} catch let error as NSError {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CHECK FOR WHEN DEVICE IS UNAVAILABLE
|
||||
public func postDataToUser(userId: String, deviceId: String, data: String, event: String) {
|
||||
Just.post("http://localhost/signal/user/\(userId)/device/\(deviceId)",
|
||||
json: ["event": event, "data": data],
|
||||
headers: ["Authorization": "Bearer \(authToken ?? "0")"])
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import "package:flutter/services.dart";
|
||||
import 'routes.dart';
|
||||
import "src/blocs/heartbeat_bloc.dart";
|
||||
|
||||
void main() {
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import "package:rxdart/rxdart.dart";
|
||||
|
||||
class BottomBusBloc {
|
||||
final _bottomBarBus = PublishSubject<Map<String, String>>();
|
||||
Map<String, String> _lastMessage = {"state": "no_connection"};
|
||||
|
||||
BottomBusBloc() {
|
||||
_bottomBarBus.listen((data) => print(data));
|
||||
}
|
||||
|
||||
Observable<Map<String, String>> get bus => _bottomBarBus.stream;
|
||||
Map<String, String> get lastMessage => _lastMessage;
|
||||
|
||||
publish(Map<String, String> message) async {
|
||||
print(message);
|
||||
_lastMessage = message;
|
||||
_bottomBarBus.sink.add(message);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
_bottomBarBus.close();
|
||||
}
|
||||
}
|
||||
|
||||
// global instance for access throughout the app
|
||||
final bottomBusBloc = BottomBusBloc();
|
|
@ -3,7 +3,6 @@ import "package:rxdart/rxdart.dart";
|
|||
import "../resources/contact_api_provider.dart";
|
||||
import "../models/user_model.dart";
|
||||
|
||||
// TODO: SHOULD BE A INHERITED SCOPED BLOC Widget
|
||||
class ContactBloc {
|
||||
final _provider = ContactApiProvider();
|
||||
final _contactsFetcher = PublishSubject<List<User>>();
|
||||
|
@ -13,9 +12,12 @@ class ContactBloc {
|
|||
fetchContacts() async {
|
||||
List<User> contactList = await _provider.fetchContacts();
|
||||
_contactsFetcher.sink.add(contactList);
|
||||
return contactList;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
_contactsFetcher.close();
|
||||
}
|
||||
}
|
||||
|
||||
final contactBloc = ContactBloc();
|
||||
|
|
|
@ -21,6 +21,7 @@ class ConversationsBloc {
|
|||
}
|
||||
}
|
||||
|
||||
// Should be a scoped widget
|
||||
class ConversationMembersBloc {
|
||||
final String conversationId;
|
||||
final _provider = ConversationApiProvider();
|
||||
|
@ -40,3 +41,5 @@ class ConversationMembersBloc {
|
|||
_membersFetcher.close();
|
||||
}
|
||||
}
|
||||
|
||||
final conversationsBloc = ConversationsBloc();
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import "dart:async";
|
||||
import "dart:convert";
|
||||
import "dart:core";
|
||||
import "dart:ui";
|
||||
|
||||
import 'package:eventsource/eventsource.dart';
|
||||
import "package:rxdart/rxdart.dart";
|
||||
|
@ -9,61 +8,82 @@ import "package:http/http.dart" as http;
|
|||
|
||||
import "../models/ping_model.dart";
|
||||
import "../services/login_manager.dart";
|
||||
import "../resources/contact_api_provider.dart";
|
||||
import "../../settings.dart";
|
||||
|
||||
class HeartbeatReceiverBloc {
|
||||
LoginManager loginManager = new LoginManager();
|
||||
ContactApiProvider contactApiProvider = new ContactApiProvider();
|
||||
|
||||
String userId;
|
||||
DateTime lastSeen;
|
||||
String status;
|
||||
final Map<String, DateTime> lastSeen = {};
|
||||
final Map<String, String> status = {};
|
||||
|
||||
final _coloursFetcher = PublishSubject<String>();
|
||||
final http.Client client = http.Client();
|
||||
final _statusFetcher = PublishSubject<Map<String, String>>();
|
||||
|
||||
final http.Client client;
|
||||
Observable<String> get colours => _coloursFetcher.stream;
|
||||
Observable<Map<String, String>> get stream => _statusFetcher.stream;
|
||||
|
||||
HeartbeatReceiverBloc(String userId) : client = http.Client() {
|
||||
this.userId = userId;
|
||||
lastSeen = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
status = "";
|
||||
HeartbeatReceiverBloc() {
|
||||
init();
|
||||
}
|
||||
|
||||
loginManager.getToken().then((token) {
|
||||
EventSource.connect("$baseUrlHeartbeat/subscribe/$userId?token=$token",
|
||||
init() async {
|
||||
final users = await contactApiProvider.fetchContacts();
|
||||
final authToken = await loginManager.getToken();
|
||||
|
||||
// Setting up event
|
||||
for (final user in users) {
|
||||
this.lastSeen[user.id] = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
this.status[user.id] = "";
|
||||
|
||||
EventSource.connect(
|
||||
"$baseUrlHeartbeat/subscribe/${user.id}?token=$authToken",
|
||||
client: client)
|
||||
.then((es) {
|
||||
es.listen((Event event) {
|
||||
// Guard against empty packets
|
||||
if (event.data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Ping ping = Ping.fromJson(jsonDecode(event.data));
|
||||
lastSeen = DateTime.fromMillisecondsSinceEpoch(ping.time * 1000);
|
||||
status = ping.status;
|
||||
this.lastSeen[user.id] =
|
||||
DateTime.fromMillisecondsSinceEpoch(ping.time * 1000);
|
||||
});
|
||||
final oneMinute = Duration(minutes: 1);
|
||||
final timeoutDuration = Duration(minutes: 30);
|
||||
new Timer.periodic(oneMinute, (Timer t) {
|
||||
if (status == "on_call") {
|
||||
_coloursFetcher.sink.add("busy");
|
||||
} else {
|
||||
final now = new DateTime.now();
|
||||
final difference = now.difference(this.lastSeen);
|
||||
if (difference > timeoutDuration) {
|
||||
_coloursFetcher.sink.add("online");
|
||||
} else {
|
||||
_coloursFetcher.sink.add("offline");
|
||||
}
|
||||
}
|
||||
});
|
||||
}).catchError((e) => print(
|
||||
e)); // Add actual error handling logic for stopped connections
|
||||
}).catchError((e) => {});
|
||||
}
|
||||
|
||||
// Setting up timers
|
||||
final checkDuration = Duration(seconds: 20);
|
||||
final timeoutDuration = Duration(minutes: 1);
|
||||
|
||||
new Timer.periodic(checkDuration, (Timer t) {
|
||||
for (final user in users) {
|
||||
final now = new DateTime.now();
|
||||
final difference = now.difference(this.lastSeen[user.id]);
|
||||
|
||||
if (difference < timeoutDuration) {
|
||||
_statusFetcher.sink.add({"user": user.id, "status": "online"});
|
||||
this.status[user.id] = "online";
|
||||
} else {
|
||||
_statusFetcher.sink.add({"user": user.id, "status": "offline"});
|
||||
this.status[user.id] = "offline";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getLastStatus(String userId) {
|
||||
return status[userId];
|
||||
}
|
||||
|
||||
flush() {
|
||||
_statusFetcher.sink.add({"user": "flushing", "status": ""});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
_coloursFetcher.close();
|
||||
_statusFetcher.close();
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
final heartbeatReceiverBloc = HeartbeatReceiverBloc();
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import "package:rxdart/rxdart.dart";
|
||||
|
||||
class MessageBloc {
|
||||
final _messageBus = PublishSubject<Map<String, String>>();
|
||||
|
||||
MessageBloc() {
|
||||
_messageBus.listen((data) => print("MESSAGE: $data"));
|
||||
}
|
||||
|
||||
Observable<Map<String, String>> get bus => _messageBus.stream;
|
||||
|
||||
publish(Map<String, String> message) async {
|
||||
_messageBus.sink.add(message);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
_messageBus.close();
|
||||
}
|
||||
}
|
||||
|
||||
// global instance for access throughout the app
|
||||
final messageBloc = MessageBloc();
|
|
@ -35,7 +35,6 @@ class ConversationApiProvider {
|
|||
|
||||
Future<List<Conversation>> fetchConversations() async {
|
||||
final jwt = await loginManager.getToken();
|
||||
print("jwt: ${jwt}");
|
||||
try {
|
||||
final responseBody =
|
||||
await this.cache.fetch("$baseUrlCore/user/conversation", headers: {
|
||||
|
|
|
@ -11,7 +11,6 @@ class HeartbeatApiProvider {
|
|||
|
||||
Future<void> ping({String status = ""}) async {
|
||||
final jwt = await loginManager.getToken();
|
||||
print("GOT JWT: $jwt");
|
||||
await http.post("$baseUrlHeartbeat/ping",
|
||||
headers: {HttpHeaders.authorizationHeader: "Bearer $jwt"},
|
||||
body: jsonEncode({"status": status}));
|
||||
|
|
|
@ -32,11 +32,12 @@ class CacheHttp {
|
|||
Future<String> fetch(String url,
|
||||
{bool update = false, Map<String, String> headers = const {}}) async {
|
||||
if (!this.hasInit) {
|
||||
this.hasInit = true;
|
||||
await this.init();
|
||||
this.hasInit = true;
|
||||
}
|
||||
try {
|
||||
final response = await http.get(url, headers: headers);
|
||||
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
// Unsuccessful response, use cache
|
||||
final body = await this.getCache(url);
|
||||
|
@ -54,6 +55,9 @@ class CacheHttp {
|
|||
}
|
||||
|
||||
Future<String> getCache(String url) async {
|
||||
if (!this.hasInit) {
|
||||
print("UNINIT");
|
||||
}
|
||||
List<Map> cached = await this
|
||||
.db
|
||||
.rawQuery("SELECT resource FROM cache WHERE url = ?", [url]);
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import "dart:async";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
class ConversationManager {
|
||||
static const channel = const MethodChannel('beepvoice.app/conversation');
|
||||
static bool isInit = false;
|
||||
|
||||
static init(String authToken) async {
|
||||
if (isInit == false) {
|
||||
try {
|
||||
await channel.invokeMethod('init', authToken);
|
||||
isInit = true;
|
||||
} on PlatformException catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> join(String conversationId) async {
|
||||
try {
|
||||
await channel.invokeMethod('join', conversationId);
|
||||
} on PlatformException catch (e) {
|
||||
print(e);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
Future<int> exit() async {
|
||||
try {
|
||||
await channel.invokeMethod('exit');
|
||||
} on PlatformException catch (e) {
|
||||
print(e);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
Future<String> get() async {
|
||||
try {
|
||||
final String conversationId = await channel.invokeMethod('get');
|
||||
return conversationId;
|
||||
} on PlatformException catch (e) {
|
||||
print(e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,8 +11,8 @@ class HeartbeatSendManager {
|
|||
HeartbeatSendManager() {
|
||||
status = "a";
|
||||
heartbeatApiProvider = HeartbeatApiProvider();
|
||||
const oneMinute = const Duration(minutes: 1);
|
||||
new Timer.periodic(oneMinute, (Timer t) {
|
||||
const transmitInterval = const Duration(seconds: 20);
|
||||
new Timer.periodic(transmitInterval, (Timer t) {
|
||||
heartbeatApiProvider.ping(status: this.status);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
import "dart:async";
|
||||
import "dart:convert";
|
||||
import "package:eventsource/eventsource.dart";
|
||||
import "package:flutter_webrtc/webrtc.dart";
|
||||
|
||||
import "../resources/signaling_api_provider.dart";
|
||||
import "../../settings.dart";
|
||||
|
||||
// Available utility enums
|
||||
enum SignalType { CANDIDATE, OFFER, ANSWER }
|
||||
|
||||
// Callback definitions
|
||||
typedef void OnMessageCallback(Map<String, dynamic> data);
|
||||
|
||||
class PeerConnectionFactory {
|
||||
final String _localUserId;
|
||||
final String _localDeviceId;
|
||||
|
||||
EventSource _signalingServer;
|
||||
MediaStream _stream;
|
||||
SignalingApiProvider signalingApiProvider = SignalingApiProvider();
|
||||
|
||||
//Callbacks
|
||||
OnMessageCallback onMessageCallback;
|
||||
|
||||
final Map<String, dynamic> _iceServers = {
|
||||
"iceServers": [
|
||||
{"url": "stun:stun.l.google.com:19302"}
|
||||
]
|
||||
};
|
||||
|
||||
final Map<String, dynamic> _config = {
|
||||
"mandatory": {},
|
||||
"optional": [
|
||||
{"DtlsSrtpKeyAgreement": true}
|
||||
]
|
||||
};
|
||||
|
||||
final Map<String, dynamic> _constraints = {
|
||||
"mandatory": {
|
||||
"OfferToReceiveAudio": true,
|
||||
"OfferToReceiveVideo": false,
|
||||
},
|
||||
"optional": []
|
||||
};
|
||||
|
||||
PeerConnectionFactory(this._localUserId, this._localDeviceId, this._stream,
|
||||
this.onMessageCallback);
|
||||
|
||||
// initialize() method sets up a subscription to the eventsource and
|
||||
// attaches a callback to it
|
||||
initialize() async {
|
||||
_signalingServer = await EventSource.connect(
|
||||
"$baseUrlSignaling/subscribe/$_localUserId/device/$_localDeviceId");
|
||||
|
||||
_signalingServer.listen((event) {
|
||||
// Don't process empty colons
|
||||
if (event.data == null) return;
|
||||
|
||||
print("signalling/ ${event.data}");
|
||||
onMessageCallback(jsonDecode(event.data));
|
||||
});
|
||||
}
|
||||
|
||||
Future<RTCPeerConnection> newPeerConnection(
|
||||
String remoteUserId, String remoteDeviceId) async {
|
||||
RTCPeerConnection connection =
|
||||
await createPeerConnection(_iceServers, _config);
|
||||
connection.addStream(_stream);
|
||||
|
||||
// Send candidates to remote
|
||||
connection.onIceCandidate = (candidate) =>
|
||||
signalingApiProvider.sendData(remoteUserId, remoteDeviceId, {
|
||||
"fromUser": this._localUserId,
|
||||
"fromDevice": this._localDeviceId,
|
||||
"type": SignalType.CANDIDATE.index,
|
||||
"sdpMLineIndex": candidate.sdpMlineIndex,
|
||||
"sdpMid": candidate.sdpMid,
|
||||
"candidate": candidate.candidate
|
||||
});
|
||||
|
||||
// Create and send the offer
|
||||
RTCSessionDescription session = await connection.createOffer(_constraints);
|
||||
connection.setLocalDescription(session);
|
||||
await signalingApiProvider.sendData(remoteUserId, remoteDeviceId, {
|
||||
"fromUser": this._localUserId,
|
||||
"fromDevice": this._localDeviceId,
|
||||
"type": SignalType.OFFER.index,
|
||||
"sdp": session.sdp,
|
||||
"session": session.type,
|
||||
});
|
||||
|
||||
return connection;
|
||||
// NEED TO WAIT FOR ANSWER BEFORE CONNECTION IS ESTABLISHED
|
||||
}
|
||||
|
||||
leavePeerConnection(RTCPeerConnection connection) {
|
||||
connection.removeStream(_stream);
|
||||
connection.close();
|
||||
}
|
||||
|
||||
sendAnswer(RTCPeerConnection connection, String remoteUserId,
|
||||
String remoteDeviceId) async {
|
||||
RTCSessionDescription session = await connection.createAnswer(_constraints);
|
||||
connection.setLocalDescription(session);
|
||||
await signalingApiProvider.sendData(remoteUserId, remoteDeviceId, {
|
||||
"fromUser": this._localUserId,
|
||||
"fromDevice": this._localDeviceId,
|
||||
"type": SignalType.ANSWER.index,
|
||||
"sdp": session.sdp,
|
||||
"session": session.type,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
import "package:flutter_webrtc/webrtc.dart";
|
||||
import "peer_connection_factory.dart";
|
||||
|
||||
import "../resources/signaling_api_provider.dart";
|
||||
|
||||
class PeerManager {
|
||||
String _selfUserId;
|
||||
String _selfDeviceId;
|
||||
|
||||
PeerConnectionFactory _peerConnectionFactory;
|
||||
MediaStream _localStream;
|
||||
SignalingApiProvider signalingApiProvider = SignalingApiProvider();
|
||||
|
||||
List<MediaStream> _currentStreams = [];
|
||||
Map<String, RTCPeerConnection> _currentConnections = {};
|
||||
|
||||
PeerManager(this._selfUserId, this._selfDeviceId);
|
||||
|
||||
List<MediaStream> get streams => _currentStreams;
|
||||
|
||||
initialize() async {
|
||||
final Map<String, dynamic> mediaConstraints = {
|
||||
"audio": true,
|
||||
"video": false
|
||||
};
|
||||
|
||||
_localStream = await navigator.getUserMedia(mediaConstraints);
|
||||
_peerConnectionFactory = PeerConnectionFactory(
|
||||
_selfUserId, _selfDeviceId, _localStream, _signalEventHandler);
|
||||
await _peerConnectionFactory.initialize();
|
||||
}
|
||||
|
||||
addPeer(String userId) async {
|
||||
List<dynamic> deviceIds = await signalingApiProvider.getUserDevices(userId);
|
||||
|
||||
deviceIds.forEach((deviceId) async {
|
||||
RTCPeerConnection connection =
|
||||
await _peerConnectionFactory.newPeerConnection(userId, deviceId);
|
||||
|
||||
// Handle streams being added and removed remotely
|
||||
connection.onAddStream = (stream) => _currentStreams.add(stream);
|
||||
connection.onRemoveStream =
|
||||
(stream) => _currentStreams.removeWhere((it) => stream.id == it.id);
|
||||
|
||||
// Add peer connection to the map
|
||||
_currentConnections.addAll({"$userId-$deviceId": connection});
|
||||
});
|
||||
}
|
||||
|
||||
leaveAll() async {
|
||||
// DO WE NEED TO CLOSE THE REMOTE STREAMS???
|
||||
_currentConnections.forEach((key, value) {
|
||||
_peerConnectionFactory.leavePeerConnection(value);
|
||||
_currentConnections[key] = null;
|
||||
});
|
||||
|
||||
_currentStreams.forEach((stream) {
|
||||
stream.dispose();
|
||||
});
|
||||
|
||||
_currentStreams = [];
|
||||
}
|
||||
|
||||
_signalEventHandler(Map<String, dynamic> data) async {
|
||||
switch (SignalType.values[data["type"]]) {
|
||||
case SignalType.CANDIDATE:
|
||||
String userId = data["fromUser"];
|
||||
String deviceId = data["fromDevice"];
|
||||
RTCPeerConnection connection = _currentConnections["$userId-$deviceId"];
|
||||
|
||||
if (connection != null) {
|
||||
RTCIceCandidate candidate = RTCIceCandidate(
|
||||
data["candidate"], data["sdpMid"], data["sdpMLineIndex"]);
|
||||
connection.addCandidate(candidate);
|
||||
}
|
||||
break;
|
||||
|
||||
case SignalType.OFFER:
|
||||
String userId = data["fromUser"];
|
||||
String deviceId = data["fromDevice"];
|
||||
|
||||
RTCPeerConnection connection =
|
||||
await _peerConnectionFactory.newPeerConnection(userId, deviceId);
|
||||
|
||||
// Handle streams being added and removed remotely
|
||||
connection.onAddStream = (stream) => _currentStreams.add(stream);
|
||||
connection.onRemoveStream =
|
||||
(stream) => _currentStreams.removeWhere((it) => stream.id == it.id);
|
||||
|
||||
_currentConnections["$userId-$deviceId"] = connection;
|
||||
|
||||
connection.setRemoteDescription(
|
||||
RTCSessionDescription(data["sdp"], data["sessionType"]));
|
||||
_peerConnectionFactory.sendAnswer(connection, userId, deviceId);
|
||||
break;
|
||||
|
||||
case SignalType.ANSWER:
|
||||
String userId = data["fromUser"];
|
||||
String deviceId = data["fromDevice"];
|
||||
|
||||
RTCPeerConnection connection = _currentConnections["$userId-$deviceId"];
|
||||
if (connection != null) {
|
||||
connection.setRemoteDescription(
|
||||
RTCSessionDescription(data["sdp"], data["sessionType"]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +1,12 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
import "../../blocs/bottom_bus_bloc.dart";
|
||||
import "../../services/heartbeat_manager.dart";
|
||||
import "widgets/conversation_inactive_view.dart";
|
||||
import "widgets/conversation_active_view.dart";
|
||||
|
||||
class BottomBar extends StatefulWidget {
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _BottomBarState();
|
||||
}
|
||||
}
|
||||
class BottomBar extends StatelessWidget {
|
||||
final String conversationId;
|
||||
|
||||
class _BottomBarState extends State<BottomBar> {
|
||||
final bloc = bottomBusBloc;
|
||||
final heartbeatSendManager = HeartbeatSendManager();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// bloc.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget getWidgetForState(Map<String, String> message) {
|
||||
if (message["state"] == "no_connection") {
|
||||
return ConversationInactiveView();
|
||||
} else if (message["state"] == "connection") {
|
||||
return ConversationActiveView(conversationName: message["title"]);
|
||||
} else {
|
||||
return ConversationInactiveView();
|
||||
}
|
||||
}
|
||||
BottomBar({this.conversationId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -42,21 +18,11 @@ class _BottomBarState extends State<BottomBar> {
|
|||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(40.0), topRight: Radius.circular(40.0)),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: 20.0,
|
||||
left: 20.0,
|
||||
right: 20.0,
|
||||
bottom: 30.0 + bottomPadding),
|
||||
child: StreamBuilder(
|
||||
stream: bloc.bus,
|
||||
builder:
|
||||
(context, AsyncSnapshot<Map<String, String>> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return getWidgetForState(snapshot.data);
|
||||
} else {
|
||||
final message = bloc.lastMessage;
|
||||
return getWidgetForState(message);
|
||||
}
|
||||
})));
|
||||
padding: EdgeInsets.only(
|
||||
top: 20.0, left: 20.0, right: 20.0, bottom: 30.0 + bottomPadding),
|
||||
child: (conversationId == "")
|
||||
? ConversationInactiveView()
|
||||
: ConversationActiveView(conversationId: conversationId),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,65 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
import "../../widgets/user_avatar.dart";
|
||||
import "../../../resources/conversation_api_provider.dart";
|
||||
import "../../../models/user_model.dart";
|
||||
import "../../../blocs/bottom_bus_bloc.dart";
|
||||
import "../../../models/conversation_model.dart";
|
||||
import "../../../blocs/message_bloc.dart";
|
||||
|
||||
class ConversationActiveView extends StatelessWidget {
|
||||
final String conversationName;
|
||||
final bus = bottomBusBloc;
|
||||
class ConversationActiveView extends StatefulWidget {
|
||||
final String conversationId;
|
||||
|
||||
ConversationActiveView({@required this.conversationName});
|
||||
ConversationActiveView({@required this.conversationId});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _ConversationActiveViewState();
|
||||
}
|
||||
}
|
||||
|
||||
class _ConversationActiveViewState extends State<ConversationActiveView> {
|
||||
final bus = messageBloc;
|
||||
final conversationApiProvider = ConversationApiProvider();
|
||||
Conversation _conversation;
|
||||
List<Widget> _users;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
_getConversation();
|
||||
_getConversationMembers();
|
||||
}
|
||||
|
||||
_getConversation() {
|
||||
conversationApiProvider
|
||||
.fetchConversation(widget.conversationId)
|
||||
.then((conversation) {
|
||||
setState(() {
|
||||
_conversation = conversation;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getConversationMembers() {
|
||||
conversationApiProvider
|
||||
.fetchConversationMembers(widget.conversationId)
|
||||
.then((users) {
|
||||
print(users[0].id);
|
||||
setState(() {
|
||||
_users = users
|
||||
.map((user) =>
|
||||
UserAvatar(padding: EdgeInsets.only(right: 5.0), user: user))
|
||||
.toList();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if ((_conversation == null) || (_users == null)) {
|
||||
return SizedBox(height: 20, width: 20);
|
||||
}
|
||||
|
||||
return Container(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
Row(
|
||||
|
@ -26,7 +74,7 @@ class ConversationActiveView extends StatelessWidget {
|
|||
shape: BoxShape.circle)),
|
||||
Container(
|
||||
margin: EdgeInsets.only(left: 10.0),
|
||||
child: Text(conversationName,
|
||||
child: Text(_conversation.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.display1
|
||||
|
@ -41,26 +89,16 @@ class ConversationActiveView extends StatelessWidget {
|
|||
IconButton(
|
||||
color: Theme.of(context).accentColor,
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
// Call method to close connection
|
||||
bus.publish({"state": "no_connection"});
|
||||
await bus.publish({"state": "disconnect"});
|
||||
print("Pressed close");
|
||||
}),
|
||||
]),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
UserAvatar(
|
||||
padding: EdgeInsets.only(right: 5.0),
|
||||
user: User("1", "Isaac", "Tay", "+65 91043593")),
|
||||
UserAvatar(
|
||||
padding: EdgeInsets.only(right: 5.0),
|
||||
user: User("1", "Isaac", "Tay", "+65 91043593")),
|
||||
UserAvatar(
|
||||
padding: EdgeInsets.only(right: 5.0),
|
||||
user: User("1", "Rui", "Juidfsdf", "+65 91043593"))
|
||||
]),
|
||||
children: _users),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,24 +4,36 @@ import "./widgets/top_bar.dart";
|
|||
import "./widgets/conversation_list.dart";
|
||||
import "./widgets/contact_list.dart";
|
||||
import "../bottom_bar/bottom_bar.dart";
|
||||
import "../../services/heartbeat_manager.dart";
|
||||
import "../../services/conversation_manager.dart";
|
||||
import "../../blocs/message_bloc.dart";
|
||||
import "../../blocs/heartbeat_bloc.dart";
|
||||
|
||||
class Home extends StatefulWidget {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
|
||||
@override
|
||||
_HomeState createState() => _HomeState();
|
||||
}
|
||||
|
||||
class _HomeState extends State<Home> {
|
||||
final List<String> titleList = ["Conversations", "Contacts", "Settings"];
|
||||
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
final PageController controller = PageController();
|
||||
final heartbeatSendManager = HeartbeatSendManager();
|
||||
final conversationManager = ConversationManager();
|
||||
|
||||
int _pageNumber = 0;
|
||||
|
||||
PersistentBottomSheetController bottomBarController;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
controller.addListener(_updatePageNumber);
|
||||
|
||||
messageBloc.bus.listen(
|
||||
(Map<String, String> data) async => await _processMessage(data));
|
||||
_setupBottomState();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -36,18 +48,48 @@ class _HomeState extends State<Home> {
|
|||
});
|
||||
}
|
||||
|
||||
_processMessage(Map<String, String> data) async {
|
||||
if (data["state"] == "disconnect") {
|
||||
// Disconnect and change state
|
||||
await conversationManager.exit();
|
||||
bottomBarController.close();
|
||||
bottomBarController = _scaffoldKey.currentState.showBottomSheet<void>(
|
||||
(BuildContext context) => BottomBar(conversationId: ""));
|
||||
} else if (data["state"] == "connect") {
|
||||
// Connect and change state
|
||||
await conversationManager.join(data["conversationId"]);
|
||||
bottomBarController.close();
|
||||
bottomBarController = _scaffoldKey.currentState.showBottomSheet<void>(
|
||||
(BuildContext context) =>
|
||||
BottomBar(conversationId: data["conversationId"]));
|
||||
} else {
|
||||
// show default
|
||||
await conversationManager.exit();
|
||||
bottomBarController.close();
|
||||
bottomBarController = _scaffoldKey.currentState.showBottomSheet<void>(
|
||||
(BuildContext context) => BottomBar(conversationId: ""));
|
||||
}
|
||||
}
|
||||
|
||||
_setupBottomState() {
|
||||
conversationManager.get().then((conversationId) {
|
||||
bottomBarController = _scaffoldKey.currentState.showBottomSheet<void>(
|
||||
(BuildContext context) => BottomBar(conversationId: conversationId));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
key: widget._scaffoldKey,
|
||||
body: Column(children: <Widget>[
|
||||
TopBar(title: titleList[_pageNumber], pageNumber: _pageNumber),
|
||||
Expanded(
|
||||
child: PageView(controller: controller, children: <Widget>[
|
||||
ConversationList(),
|
||||
ContactList(),
|
||||
])),
|
||||
]),
|
||||
bottomSheet: BottomBar());
|
||||
key: _scaffoldKey,
|
||||
body: Column(children: <Widget>[
|
||||
TopBar(title: titleList[_pageNumber], pageNumber: _pageNumber),
|
||||
Expanded(
|
||||
child: PageView(controller: controller, children: <Widget>[
|
||||
ConversationList(),
|
||||
ContactList(),
|
||||
])),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,24 +13,16 @@ class ContactList extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ContactListState extends State<ContactList> {
|
||||
final bloc = ContactBloc();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
bloc.fetchContacts();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bloc.dispose();
|
||||
super.dispose();
|
||||
contactBloc.fetchContacts();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: bloc.contacts,
|
||||
stream: contactBloc.contacts,
|
||||
builder: (context, AsyncSnapshot<List<User>> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return buildList(snapshot);
|
||||
|
|
|
@ -3,7 +3,7 @@ import "package:flutter/material.dart";
|
|||
import "../../../models/user_model.dart";
|
||||
import "../../../models/conversation_model.dart";
|
||||
import "../../../blocs/conversation_bloc.dart";
|
||||
import "../../../blocs/bottom_bus_bloc.dart";
|
||||
import "../../../blocs/message_bloc.dart";
|
||||
|
||||
import "../../widgets/user_avatar.dart";
|
||||
|
||||
|
@ -20,7 +20,7 @@ class ConversationItem extends StatefulWidget {
|
|||
class _ConversationItemState extends State<ConversationItem> {
|
||||
final bloc;
|
||||
final Conversation conversation;
|
||||
final bus = bottomBusBloc;
|
||||
final bus = messageBloc;
|
||||
|
||||
_ConversationItemState({@required this.conversation})
|
||||
: bloc = ConversationMembersBloc(conversation.id);
|
||||
|
@ -39,49 +39,68 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
onTap: () =>
|
||||
bus.publish({"state": "connection", "title": conversation.title}),
|
||||
contentPadding:
|
||||
EdgeInsets.only(top: 0.0, left: 20.0, right: 20.0, bottom: 0.0),
|
||||
title: Text(conversation.title, style: Theme.of(context).textTheme.title),
|
||||
subtitle: Text("Mum I might have forgotten to close the windows",
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.subtitle),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Text("1m ago",
|
||||
style: Theme.of(context).primaryTextTheme.body1.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).primaryColorDark)),
|
||||
StreamBuilder(
|
||||
stream: bloc.members,
|
||||
builder: (context, AsyncSnapshot<List<User>> snapshot) {
|
||||
print(snapshot.data);
|
||||
if (snapshot.hasData) {
|
||||
return membersBuilder(snapshot.data);
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
}
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}),
|
||||
]),
|
||||
);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await bus.publish(
|
||||
{"state": "connect", "conversationId": conversation.id});
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: 5.0, left: 20.0, right: 20.0, bottom: 5.0),
|
||||
child: Column(children: <Widget>[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(conversation.title,
|
||||
style: Theme.of(context).textTheme.title),
|
||||
Spacer(),
|
||||
Text("1m ago",
|
||||
style: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.body1
|
||||
.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).primaryColorDark)),
|
||||
]),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 5.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
"I might have forgotten to close the windows",
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
Theme.of(context).textTheme.subtitle)),
|
||||
StreamBuilder(
|
||||
stream: bloc.members,
|
||||
builder: (context,
|
||||
AsyncSnapshot<List<User>> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return membersBuilder(snapshot.data);
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
}
|
||||
return SizedBox(width: 18.0, height: 18.0);
|
||||
}),
|
||||
]))
|
||||
]))));
|
||||
}
|
||||
|
||||
Widget membersBuilder(List<User> data) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: data
|
||||
.map((user) => UserAvatar(
|
||||
radius: 15.0,
|
||||
padding: EdgeInsets.only(top: 10.0, left: 5.0),
|
||||
radius: 18.0,
|
||||
padding: EdgeInsets.only(top: 0.0, left: 5.0),
|
||||
user: user))
|
||||
.toList());
|
||||
}
|
||||
|
|
|
@ -13,32 +13,26 @@ class ConversationList extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ConversationListState extends State<ConversationList> {
|
||||
final bloc = ConversationsBloc();
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
bloc.fetchConversations();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
bloc.dispose();
|
||||
super.dispose();
|
||||
conversationsBloc.fetchConversations();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
stream: bloc.conversations,
|
||||
builder: (context, AsyncSnapshot<List<Conversation>> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return buildList(snapshot.data);
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
}
|
||||
return Center(child: CircularProgressIndicator());
|
||||
});
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 10.0),
|
||||
child: StreamBuilder(
|
||||
stream: conversationsBloc.conversations,
|
||||
builder: (context, AsyncSnapshot<List<Conversation>> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return buildList(snapshot.data);
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
}
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}));
|
||||
}
|
||||
|
||||
Widget buildList(List<Conversation> data) {
|
||||
|
|
|
@ -51,7 +51,7 @@ class Welcome extends StatelessWidget {
|
|||
builder = (BuildContext _) =>
|
||||
OtpPage(buttonCallback: (String otp) async {
|
||||
// loginManager.processOtp(otp); disabled for testing
|
||||
Navigator.of(context).pushNamed("/home");
|
||||
Navigator.of(context).pushReplacementNamed("/home");
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -3,6 +3,7 @@ import "package:flutter_svg/flutter_svg.dart";
|
|||
|
||||
import "../../widgets/text_button.dart";
|
||||
import "../../../services/login_manager.dart";
|
||||
import "../../../services/conversation_manager.dart";
|
||||
import "phone_input.dart";
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
|
@ -53,9 +54,14 @@ class _LoginPageState extends State<LoginPage> {
|
|||
Spacer(),
|
||||
TextButton(
|
||||
text: "Continue",
|
||||
onClickCallback: () {
|
||||
print(controller.text);
|
||||
widget.loginManager.loginTest(controller.text);
|
||||
onClickCallback: () async {
|
||||
final authToken =
|
||||
await widget.loginManager.loginTest(controller.text);
|
||||
|
||||
// Waiting for initialization
|
||||
await ConversationManager.init(authToken);
|
||||
print(authToken);
|
||||
|
||||
Navigator.pushNamed(context, 'welcome/otp');
|
||||
}),
|
||||
]));
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "dart:async";
|
||||
|
||||
import "../../models/user_model.dart";
|
||||
import "../../blocs/heartbeat_bloc.dart";
|
||||
|
@ -20,19 +21,18 @@ class UserAvatar extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _UserAvatarState extends State<UserAvatar> {
|
||||
HeartbeatReceiverBloc bloc;
|
||||
String lastStatus = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
print(widget.user.id);
|
||||
initState() {
|
||||
super.initState();
|
||||
bloc = HeartbeatReceiverBloc(widget.user.id);
|
||||
lastStatus = heartbeatReceiverBloc.getLastStatus(widget.user.id);
|
||||
initializeStream();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bloc.dispose();
|
||||
super.dispose();
|
||||
initializeStream() async {
|
||||
return Future.delayed(
|
||||
const Duration(milliseconds: 1), () => heartbeatReceiverBloc.flush());
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -56,17 +56,25 @@ class _UserAvatarState extends State<UserAvatar> {
|
|||
),
|
||||
radius: widget.radius),
|
||||
StreamBuilder(
|
||||
stream: bloc.colours,
|
||||
builder: (context, AsyncSnapshot<String> snapshot) {
|
||||
String state;
|
||||
stream: heartbeatReceiverBloc.stream,
|
||||
builder: (context, AsyncSnapshot<Map<String, String>> snapshot) {
|
||||
Map<String, String> state;
|
||||
if (snapshot.hasData) {
|
||||
state = snapshot.data;
|
||||
if (state == "online") {
|
||||
|
||||
if (state["user"] == widget.user.id) {
|
||||
this.lastStatus = state["status"];
|
||||
}
|
||||
|
||||
if (lastStatus == "online") {
|
||||
return Container(
|
||||
width: 12.0,
|
||||
height: 12.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green[400], shape: BoxShape.circle));
|
||||
color: Colors.green[400],
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
width: 1.5, color: const Color(0xFFFFFFFF))));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue