4
2
Fork 0

Merge branch 'feat/implementing-custom-webrtc-implementation' of beep/frontend_flutter into master

Finally completed working WebRTC support only for iPhone
pull/55/head
Sudharshan S. 2019-05-26 01:22:45 +00:00 committed by Gitea
commit 2b3891f2fe
33 changed files with 1332 additions and 461 deletions

View File

@ -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?

View File

@ -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)",

View File

@ -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>

View File

@ -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"

View File

@ -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)
}

View File

@ -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"/>

View File

@ -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)"
}
}

View File

@ -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>

View File

@ -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) {
}
}

View File

@ -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)
}
}
}
}

View File

@ -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")"])
}
}

View File

@ -1,5 +1,6 @@
import "package:flutter/services.dart";
import 'routes.dart';
import "src/blocs/heartbeat_bloc.dart";
void main() {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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: {

View File

@ -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}));

View File

@ -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]);

View File

@ -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 "";
}
}
}

View File

@ -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);
});
}

View File

@ -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,
});
}
}

View File

@ -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;
}
}
}
}

View File

@ -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),
));
}
}

View File

@ -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),
]));
}
}

View File

@ -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(),
])),
]),
);
}
}

View File

@ -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);

View File

@ -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());
}

View File

@ -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) {

View File

@ -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:

View File

@ -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');
}),
]));

View File

@ -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))));
}
}