Merge pull request #4 from talksik/live-voice-call-api-integration

Live voice call api integration
This commit is contained in:
Arjun Patel
2022-01-01 22:36:01 -08:00
committed by GitHub
10 changed files with 375 additions and 247 deletions
+4
View File
@@ -13,6 +13,7 @@
89091997277DDE030025F4BB /* ProfilePicturesOverlappedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */; }; 89091997277DDE030025F4BB /* ProfilePicturesOverlappedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */; };
89091999277EFDCC0025F4BB /* leaveSound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 89091998277EFDCC0025F4BB /* leaveSound.mp3 */; }; 89091999277EFDCC0025F4BB /* leaveSound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 89091998277EFDCC0025F4BB /* leaveSound.mp3 */; };
8909199C277EFDF30025F4BB /* joinSound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 8909199B277EFDF30025F4BB /* joinSound.mp3 */; }; 8909199C277EFDF30025F4BB /* joinSound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 8909199B277EFDF30025F4BB /* joinSound.mp3 */; };
8909199E278155920025F4BB /* UserStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8909199D278155920025F4BB /* UserStatusView.swift */; };
890C245D277BF628009B4A30 /* AgoraRtcKit in Frameworks */ = {isa = PBXBuildFile; productRef = 890C245C277BF628009B4A30 /* AgoraRtcKit */; }; 890C245D277BF628009B4A30 /* AgoraRtcKit in Frameworks */ = {isa = PBXBuildFile; productRef = 890C245C277BF628009B4A30 /* AgoraRtcKit */; };
890C2460277BF896009B4A30 /* AgoraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C245F277BF896009B4A30 /* AgoraViewController.swift */; }; 890C2460277BF896009B4A30 /* AgoraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C245F277BF896009B4A30 /* AgoraViewController.swift */; };
890C2462277BF935009B4A30 /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C2461277BF935009B4A30 /* CallView.swift */; }; 890C2462277BF935009B4A30 /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C2461277BF935009B4A30 /* CallView.swift */; };
@@ -102,6 +103,7 @@
89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesOverlappedView.swift; sourceTree = "<group>"; }; 89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesOverlappedView.swift; sourceTree = "<group>"; };
89091998277EFDCC0025F4BB /* leaveSound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = leaveSound.mp3; sourceTree = "<group>"; }; 89091998277EFDCC0025F4BB /* leaveSound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = leaveSound.mp3; sourceTree = "<group>"; };
8909199B277EFDF30025F4BB /* joinSound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = joinSound.mp3; sourceTree = "<group>"; }; 8909199B277EFDF30025F4BB /* joinSound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = joinSound.mp3; sourceTree = "<group>"; };
8909199D278155920025F4BB /* UserStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusView.swift; sourceTree = "<group>"; };
890C245F277BF896009B4A30 /* AgoraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraViewController.swift; sourceTree = "<group>"; }; 890C245F277BF896009B4A30 /* AgoraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraViewController.swift; sourceTree = "<group>"; };
890C2461277BF935009B4A30 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = "<group>"; }; 890C2461277BF935009B4A30 /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = "<group>"; };
890C2463277BFA97009B4A30 /* AgroRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgroRep.swift; sourceTree = "<group>"; }; 890C2463277BFA97009B4A30 /* AgroRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgroRep.swift; sourceTree = "<group>"; };
@@ -316,6 +318,7 @@
891A15FE27656F9200B26659 /* HeaderView.swift */, 891A15FE27656F9200B26659 /* HeaderView.swift */,
89288ECA27675B9F00E962C4 /* OnboardingTemplateView.swift */, 89288ECA27675B9F00E962C4 /* OnboardingTemplateView.swift */,
89BCABEC276D2B05009E9B10 /* WavesGlassBackgroundView.swift */, 89BCABEC276D2B05009E9B10 /* WavesGlassBackgroundView.swift */,
8909199D278155920025F4BB /* UserStatusView.swift */,
); );
path = Templates; path = Templates;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -656,6 +659,7 @@
8904DEC8277055E90071E6CA /* OnboardingOneView.swift in Sources */, 8904DEC8277055E90071E6CA /* OnboardingOneView.swift in Sources */,
896CE7022770892F00DB474E /* CircleGridView.swift in Sources */, 896CE7022770892F00DB474E /* CircleGridView.swift in Sources */,
89091995277DD9F80025F4BB /* ConvoFooterView.swift in Sources */, 89091995277DD9F80025F4BB /* ConvoFooterView.swift in Sources */,
8909199E278155920025F4BB /* UserStatusView.swift in Sources */,
89BDCDEA27697F9C00577829 /* UserMenu.swift in Sources */, 89BDCDEA27697F9C00577829 /* UserMenu.swift in Sources */,
89BCABE7276B3D64009E9B10 /* InnerCircleViewModel.swift in Sources */, 89BCABE7276B3D64009E9B10 /* InnerCircleViewModel.swift in Sources */,
89A107E5277476CC00B971FD /* PushNotificationService.swift in Sources */, 89A107E5277476CC00B971FD /* PushNotificationService.swift in Sources */,
+1 -1
View File
@@ -30,7 +30,7 @@ struct Convo: Identifiable, Codable {
@ServerTimestamp var startedTimestamp: Date? @ServerTimestamp var startedTimestamp: Date?
var endedTimestamp: Date? var endedTimestamp: Date?
var receiverEndedTimestamp: Date? var secondToLastUserEndedTimestamp: Date?
} }
//struct DetailConvo: Convo { //struct DetailConvo: Convo {
+3 -1
View File
@@ -224,9 +224,11 @@ extension AuthSessionStore {
// TODO: THIS WON'T UPDATE VIEW since user is a reference type...can manually publish...research and learn more // TODO: THIS WON'T UPDATE VIEW since user is a reference type...can manually publish...research and learn more
self.firestoreService.getUserRealtime(userId: currUserId!) {[weak self] realtimeUpdatedUser in self.firestoreService.getUserRealtime(userId: currUserId!) {[weak self] realtimeUpdatedUser in
if realtimeUpdatedUser != nil { if realtimeUpdatedUser != nil {
print("up to date user: \(realtimeUpdatedUser)") print("up to date user")
self?.user = realtimeUpdatedUser self?.user = realtimeUpdatedUser
self?.objectWillChange.send() self?.objectWillChange.send()
self?.relevantUsersDict[currUserId!] = realtimeUpdatedUser
} }
} }
+99 -74
View File
@@ -9,89 +9,95 @@ import SwiftUI
struct ConvoFooterView: View { struct ConvoFooterView: View {
@EnvironmentObject var convoVM: ConvoViewModel @EnvironmentObject var convoVM: ConvoViewModel
@EnvironmentObject var authSessionStore: AuthSessionStore
@State private var convoRelativeTime = "started 20 minutes ago" @State private var convoRelativeTime = "started 20 minutes ago"
@State private var selectedConvo: Convo? = nil
var body: some View { var body: some View {
ZStack(alignment:.bottomTrailing) { ZStack(alignment:.bottomTrailing) {
VStack(alignment:.center) { VStack(alignment:.center) {
Spacer() Spacer()
HStack { HStack {
ProfilePicturesOverlappedView(indvAvatarWidth: CGFloat(35)) if self.selectedConvo != nil {
.padding(.leading, 20) let filteredUsers = self.authSessionStore.relevantUsersDict.filter {
.padding(.trailing, 10) return self.selectedConvo!.users.contains($0.key)
}
let convoUsers: [User] = Array(filteredUsers.values)
// meta data of convo
VStack (alignment: .leading) {
Text("kev, sarth, and 5 others")
.font(.footnote)
.foregroundColor(NirvanaColor.light)
switch self.convoVM.connectionState { ProfilePicturesOverlappedView(indvAvatarWidth: CGFloat(35), users: convoUsers)
case .connecting: .padding(.leading, 20)
Label("connecting", systemImage: "chart.bar") .padding(.trailing, 10)
.foregroundColor(Color.orange)
.font(.caption) // meta data of convo
case .connected: VStack (alignment: .leading) {
Label("connected", systemImage: "chart.bar.fill") ConvoUsernames(users: convoUsers)
.foregroundColor(Color.green)
.font(.caption) switch self.convoVM.connectionState {
case .reconnecting: case .connecting:
Label("reconnecting", systemImage: "chart.bar") Label("connecting", systemImage: "chart.bar")
.foregroundColor(Color.orange) .foregroundColor(Color.orange)
.font(.caption) .font(.caption)
case .failed: case .connected:
Label("failed", systemImage: "chart.bar") Label("connected", systemImage: "chart.bar.fill")
.foregroundColor(Color.red) .foregroundColor(Color.green)
.font(.caption) .font(.caption)
case .disconnected: case .reconnecting:
Label("disconnected", systemImage: "chart.bar") Label("reconnecting", systemImage: "chart.bar")
.foregroundColor(Color.red) .foregroundColor(Color.orange)
.font(.caption) .font(.caption)
default: case .failed:
Label("disconnected", systemImage: "chart.bar") Label("failed", systemImage: "chart.bar")
.foregroundColor(Color.red) .foregroundColor(Color.red)
.font(.caption) .font(.caption)
case .disconnected:
Label("disconnected", systemImage: "chart.bar")
.foregroundColor(Color.red)
.font(.caption)
default:
Label("disconnected", systemImage: "chart.bar")
.foregroundColor(Color.red)
.font(.caption)
}
Text(self.convoRelativeTime)
.font(.caption2)
.foregroundColor(.gray)
} }
Text(self.convoRelativeTime) Spacer()
.font(.caption2)
.foregroundColor(.gray)
}
Spacer()
// disconnect button
Button {
print("showing more info about the chat")
// show a sheet of the members in the call and add them if I want // disconnect button
Button {
print("showing more info about the chat")
// show a sheet of the members in the call and add them if I want
} label: {
Label("attendees", systemImage: "person.2.circle.fill")
.labelStyle(.iconOnly)
.foregroundColor(NirvanaColor.teal)
.font(.title2)
.padding(.trailing, 5)
}
} label: { // disconnect button
Label("attendees", systemImage: "person.2.circle.fill") Button {
.labelStyle(.iconOnly) print("disconnecting user now ")
.foregroundColor(NirvanaColor.teal)
.font(.title2) self.convoVM.leaveConvo()
.padding(.trailing, 5)
// show success or failure toast
} label: {
Label("Call", systemImage: "powerplug.fill")
.labelStyle(.iconOnly)
.foregroundColor(Color.orange)
.font(.title2)
.padding(.trailing, 5)
}
} }
// disconnect button
Button {
print("disconnecting user now ")
self.convoVM.leaveConvo()
// show success or failure toast
} label: {
Label("Call", systemImage: "powerplug.fill")
.labelStyle(.iconOnly)
.foregroundColor(Color.orange)
.font(.title2)
.padding(.trailing, 5)
}
} }
.frame(maxWidth: .infinity, maxHeight: 60) // 60 is the height of the footer control big circle .frame(maxWidth: .infinity, maxHeight: 60) // 60 is the height of the footer control big circle
.background(Color.white.opacity(0.5)) .background(Color.white.opacity(0.5))
@@ -105,17 +111,25 @@ struct ConvoFooterView: View {
x:0, x:0,
y:self.convoVM.selectedConvoId == nil ? 150 : 0 y:self.convoVM.selectedConvoId == nil ? 150 : 0
) )
.onChange(of: self.convoVM.selectedConvoId) {newConvo in .onReceive(self.convoVM.$relevantConvos) {newConvos in
// update meta data based on which convo was selected // update meta data based on which convo was selected
if self.convoVM.selectedConvoId == nil {
print("nothing to show as there is no convo selected")
return
}
// reset the selected options to start fresh // reset the selected options to start fresh
// self.convoRelativeTime = "" self.convoRelativeTime = ""
self.selectedConvo = newConvos.first {convo in
convo.id == self.convoVM.selectedConvoId
}
// get relative start time of convo // get relative start time of convo
// let formatter = RelativeDateTimeFormatter() let formatter = RelativeDateTimeFormatter()
// formatter.unitsStyle = .full formatter.unitsStyle = .full
// self.convoRelativeTime = formatter.localizedString(for: self.selectedConvo?.startedTimestamp ?? Date(), relativeTo: Date())
// let relativeDate = formatter.localizedString(for: ?.sentTimestamp ?? Date(), relativeTo: Date())
} }
} }
} }
@@ -125,3 +139,14 @@ struct ConvoFooterView_Previews: PreviewProvider {
ConvoFooterView() ConvoFooterView()
} }
} }
struct ConvoUsernames: View {
var users: [User]
var body: some View {
let userNames = self.users.map{$0.nickname ?? ""}.joined(separator: ",")
Text(userNames)
.font(.footnote)
.foregroundColor(NirvanaColor.light)
}
}
+73 -33
View File
@@ -11,7 +11,7 @@ import Firebase
class ConvoViewModel: NSObject, ObservableObject { class ConvoViewModel: NSObject, ObservableObject {
@Published var selectedConvoId:String? = nil @Published var selectedConvoId:String? = nil
@Published var connectionState: AgoraConnectionStateType? = nil @Published var connectionState: AgoraConnectionStateType?
let firestoreService = FirestoreService() let firestoreService = FirestoreService()
private var db = Firestore.firestore() private var db = Firestore.firestore()
@@ -19,7 +19,7 @@ class ConvoViewModel: NSObject, ObservableObject {
let agoraService = AgoraService() let agoraService = AgoraService()
// TODO: put the value in environment variables // TODO: put the value in environment variables
lazy var agoraKit: AgoraRtcEngineKit? = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: self) var agoraKit: AgoraRtcEngineKit?
private var allConvos: [Convo] = [] private var allConvos: [Convo] = []
@Published var relevantConvos: [Convo] = [] @Published var relevantConvos: [Convo] = []
@@ -28,15 +28,22 @@ class ConvoViewModel: NSObject, ObservableObject {
return self.selectedConvoId != nil return self.selectedConvoId != nil
} }
static var relevancyAcceptance = 0.6 static var relevancyAcceptance = 0.5
func initializeAgoraEngine() {
self.agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: self)
}
override init() { override init() {
super.init() super.init()
initializeAgoraEngine()
// start process of data collection // start process of data collection
let userId = AuthSessionStore.getCurrentUserId() let userId = AuthSessionStore.getCurrentUserId()
if userId == nil { if userId == nil {
print("no authenticated user") print("no authenticated user")
return
} }
// get all active userfriends // get all active userfriends
@@ -88,13 +95,19 @@ class ConvoViewModel: NSObject, ObservableObject {
// if convo receiver is me and I'm not in a call or this call already, then join in // if convo receiver is me and I'm not in a call or this call already, then join in
if convo!.receiverUserId == userId && !(self?.isInCall())! if convo!.receiverUserId == userId && !(self?.isInCall())!
&& convo!.receiverEndedTimestamp == nil { && convo!.secondToLastUserEndedTimestamp == nil {
print("I am the direct receiver...joining convo")
self?.joinConvo(convo: convo!) self?.joinConvo(convo: convo!)
} }
// if I am in a convo in which the last/loner has left, I need to leave the convo // if I am in a convo in which the last/loner has left, I need to leave the convo
if convo!.receiverEndedTimestamp != nil && (self?.isInCall())! && convo!.users.contains(userId!) { else if convo!.secondToLastUserEndedTimestamp != nil && (self?.isInCall())! && convo!.users.contains(userId!) {
print("I am the last one now... ending the convo")
self?.leaveConvo() self?.leaveConvo()
} //also if I am added after a receiver was added and I am not already in a convo, then join the call
else if convo!.users.contains(userId!) && !(self?.isInCall())! {
// should be free or online and not in another call unless it's a race condition
print("I was added onto the call because I was free...joining automatically")
self?.joinConvo(convo: convo!)
} }
self?.allConvos.append(convo!) self?.allConvos.append(convo!)
@@ -111,10 +124,10 @@ class ConvoViewModel: NSObject, ObservableObject {
} }
} }
let relevancyScore = Double(relevancyCount / convo.users.count) let relevancyScore = Double(relevancyCount) / Double(convo.users.count)
print("relevancy score: \(relevancyScore)") print("relevancy score: \(relevancyScore)")
if relevancyScore > Self.relevancyAcceptance { if relevancyScore > Self.relevancyAcceptance && convo.users.count > 1 { //offchance that it's just the one person in the convo right now, don't want to see it until there are at least 2 people
print("this convo counts as relevant!!!") print("this convo counts as relevant!!!")
return true return true
} }
@@ -126,15 +139,12 @@ class ConvoViewModel: NSObject, ObservableObject {
} }
deinit { deinit {
// deinit the firestore listener
self.convosListener?.remove()
// deinit the agora engine
print("deiniting and cleaning up convo view model/agora services") print("deiniting and cleaning up convo view model/agora services")
AgoraRtcEngineKit.destroy() self.leaveConvo(destroyInstance: true)
self.leaveConvo() // deinit the firestore listener
self.convosListener?.remove()
} }
/** /**
@@ -175,7 +185,8 @@ class ConvoViewModel: NSObject, ObservableObject {
} }
} }
func joinConvo(convo: Convo) { /* internal control to join a convo */
private func joinConvo(convo: Convo) {
// ensure that this user leaves all other channels // ensure that this user leaves all other channels
if self.isInCall() { if self.isInCall() {
self.leaveConvo() self.leaveConvo()
@@ -186,25 +197,19 @@ class ConvoViewModel: NSObject, ObservableObject {
self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true)
// finally join channel // finally join channel
self.agoraKit?.delegate = self
self.agoraKit?.joinChannel(byToken: convo.agoraToken, channelId: convo.id!, info: nil, uid: 0, joinSuccess: nil) self.agoraKit?.joinChannel(byToken: convo.agoraToken, channelId: convo.id!, info: nil, uid: 0, joinSuccess: nil)
} }
/** /**
Allow user to join an active convo Allow user to join an active convo
*/ */
func joinConvo() { func joinConvo(convoId: String) {
// ensure the convo is active and includes 2 people in it currently...shouldn't be shown if not anyway // ensure the convo is active and includes 2 people in it currently...shouldn't be shown if not anyway
// ensure that this user leaves all other channels // ensure that this user leaves all other channels
if self.isInCall() {
print("can't join another call, already in one")
return
}
let convo = self.relevantConvos.first {convo in let convo = self.relevantConvos.first {convo in
convo.id == self.selectedConvoId convo.id == convoId
} }
if convo == nil { if convo == nil {
@@ -215,20 +220,44 @@ class ConvoViewModel: NSObject, ObservableObject {
// TODO: if the convo has more than 10 people, stop user from joining...that's too expensive... // TODO: if the convo has more than 10 people, stop user from joining...that's too expensive...
self.selectedConvoId = convoId
let convoAgoraToken:String = convo!.agoraToken let convoAgoraToken:String = convo!.agoraToken
let channelName:String = convo!.id! let channelName:String = convo!.id!
print("joining convo: \(channelName)")
self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true) self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true)
// finally join channel // finally join channel
self.agoraKit?.delegate = self
self.agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: channelName, info: nil, uid: 0, joinSuccess: nil) self.agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: channelName, info: nil, uid: 0, joinSuccess: nil)
} }
// anyone can leave at any time func addThirdPartyToConvo(friendId: String) {
func leaveConvo() { // recheck if this other friend that I am adding is online/free
self.agoraKit?.delegate = self
agoraKit?.leaveChannel(nil)
if !self.isInCall() {
print("not in a call, can't invite/force someone else...must have a selected convo")
return
}
// get the currconvo details
var convo = self.relevantConvos.first {convo in
convo.id == self.selectedConvoId
}
if convo == nil {
print("convo not available")
return
}
// update convo users array with this other friend
convo!.users.append(friendId)
self.firestoreService.updateConvo(convo: convo!) {[weak self] res in
print("updated with new friend added in the convo")
}
}
// anyone can leave at any time
func leaveConvo(destroyInstance: Bool = false) {
let userId = AuthSessionStore.getCurrentUserId() let userId = AuthSessionStore.getCurrentUserId()
if userId == nil { if userId == nil {
@@ -241,6 +270,8 @@ class ConvoViewModel: NSObject, ObservableObject {
return return
} }
agoraKit?.leaveChannel(nil)
self.firestoreService.updateUserStatus(userId: userId!, userStatus: .online) {[weak self] res in self.firestoreService.updateUserStatus(userId: userId!, userStatus: .online) {[weak self] res in
print(res) print(res)
@@ -269,7 +300,7 @@ class ConvoViewModel: NSObject, ObservableObject {
updatedConvo!.endedTimestamp = Date() updatedConvo!.endedTimestamp = Date()
} // if I am second to last, the last guy is a loner } // if I am second to last, the last guy is a loner
else if updatedConvo!.users.count == 2 && updatedConvo!.users.contains(userId!) { else if updatedConvo!.users.count == 2 && updatedConvo!.users.contains(userId!) {
updatedConvo!.receiverEndedTimestamp = Date() updatedConvo!.secondToLastUserEndedTimestamp = Date()
} }
let updatedUsers: [String] = updatedConvo!.users.filter{ arrUserId in let updatedUsers: [String] = updatedConvo!.users.filter{ arrUserId in
@@ -282,13 +313,19 @@ class ConvoViewModel: NSObject, ObservableObject {
self?.selectedConvoId = nil self?.selectedConvoId = nil
print("left convo officially in our db as well") print("left convo officially in our db as well")
// deinit the agora engine
if destroyInstance {
self?.destroyInstance()
}
} }
} }
} }
func destroyInstance() { func destroyInstance() {
AgoraRtcEngineKit.destroy() AgoraRtcEngineKit.destroy()
} }
} }
extension ConvoViewModel { extension ConvoViewModel {
@@ -377,7 +414,10 @@ extension ConvoViewModel: AgoraRtcEngineDelegate {
case .success: case .success:
// update convo in database users array to let them know I am in now // update convo in database users array to let them know I am in now
updatedConvo!.users.append(userId!) // only add if I am not already in the users arr...if I was a third party, I should be in there already
if !updatedConvo!.users.contains(userId!) {
updatedConvo!.users.append(userId!)
}
updatedConvo!.state = .active updatedConvo!.state = .active
self?.firestoreService.updateConvo(convo: updatedConvo!) {[weak self] res in self?.firestoreService.updateConvo(convo: updatedConvo!) {[weak self] res in
+62 -59
View File
@@ -7,67 +7,70 @@
import SwiftUI import SwiftUI
struct ConvosView: View { /**
@EnvironmentObject var vm: ConvoViewModel deprecated...used to insert into circlegridview but can isolate later
@State var animate = false */
//struct ConvosView: View {
var body: some View { // @EnvironmentObject var vm: ConvoViewModel
ScrollView([.horizontal]) { // @State var animate = false
HStack {
ForEach(0..<self.vm.relevantConvos.count, id: \.self) {index in
let currConvo = self.vm.relevantConvos[index]
ZStack(alignment: .topTrailing) {
Circle()
.foregroundColor(self.vm.selectedConvoId == currConvo.id ? Color.green : NirvanaColor.dimTeal.opacity(0.2))
ZStack {
Color.clear
ProfilePicturesOverlappedView()
.shadow(radius: 10)
}
Text("+2")
.padding(5)
.font(.caption)
.foregroundColor(Color.white)
.background(NirvanaColor.dimTeal)
.cornerRadius(100)
// ZStack(alignment: .bottom) {
// Color.clear
// //
// Text("kev, sarth...") // var body: some View {
// .foregroundColor(NirvanaColor.teal) // ScrollView([.horizontal]) {
// .font(.caption) // HStack {
// ForEach(0..<self.vm.relevantConvos.count, id: \.self) {index in
// let currConvo = self.vm.relevantConvos[index]
// ZStack(alignment: .topTrailing) {
// Circle()
// .foregroundColor(self.vm.selectedConvoId == currConvo.id ? Color.green : NirvanaColor.dimTeal.opacity(0.2))
//
// ZStack {
// Color.clear
// ProfilePicturesOverlappedView()
// .shadow(radius: 10)
// } // }
} //
.frame(width: 100, height: 100) // Text("+2")
.scaleEffect(self.animate ? 1 : 0.85) // .padding(5)
.animation( // .font(.caption)
Animation.easeInOut(duration: self.vm.selectedConvoId == currConvo.id ? 2 : 4 // .foregroundColor(Color.white)
).repeatForever(autoreverses: true), // .background(NirvanaColor.dimTeal)
value: self.animate // .cornerRadius(100)
) //
.onTapGesture { //// ZStack(alignment: .bottom) {
// if not in a call already //// Color.clear
if !self.vm.isInCall() { ////
self.vm.selectedConvoId = self.vm.relevantConvos[index].id! //// Text("kev, sarth...")
self.vm.joinConvo() //// .foregroundColor(NirvanaColor.teal)
//// .font(.caption)
return //// }
} // }
// .frame(width: 100, height: 100)
self.vm.leaveConvo() // .scaleEffect(self.animate ? 1 : 0.85)
} // .animation(
} // Animation.easeInOut(duration: self.vm.selectedConvoId == currConvo.id ? 2 : 4
} // ).repeatForever(autoreverses: true),
.onAppear { // value: self.animate
self.animate = true // )
} // .onTapGesture {
} // // if not in a call already
.padding() // if !self.vm.isInCall() {
} // self.vm.selectedConvoId = self.vm.relevantConvos[index].id!
} // self.vm.joinConvo(convoId: currConvo.id!)
//
// return
// }
//
// self.vm.leaveConvo()
// }
// }
// }
// .onAppear {
// self.animate = true
// }
// }
// .padding()
// }
//}
//struct ConvosView_Previews: PreviewProvider { //struct ConvosView_Previews: PreviewProvider {
// static var previews: some View { // static var previews: some View {
@@ -7,31 +7,33 @@
import SwiftUI import SwiftUI
/**
@return a view that overlaps the user avatars
*/
struct ProfilePicturesOverlappedView: View { struct ProfilePicturesOverlappedView: View {
let numberAttendees:Int = 5
let offset:Int = 15 let offset:Int = 15
var indvAvatarWidth: CGFloat = 50 var indvAvatarWidth: CGFloat = 50
var users: [User]
var body: some View { var body: some View {
ZStack { ZStack {
ForEach(0..<numberAttendees, id: \.self) {attendeeIndex in ForEach(0..<users.count, id: \.self) {userIndex in
if attendeeIndex < 3 { Image(users[userIndex].avatar ?? Avatars.avatarSystemNames[1])
Image(Avatars.avatarSystemNames[attendeeIndex+1]) .resizable()
.resizable() .scaledToFit()
.scaledToFit() .background(NirvanaColor.dimTeal)
.background(NirvanaColor.dimTeal) .clipShape(Circle())
.clipShape(Circle()) .offset(x: CGFloat((-15 * userIndex)) + 15)
.offset(x: CGFloat((-15 * attendeeIndex)) + 15) .frame(width: indvAvatarWidth)
.frame(width: indvAvatarWidth)
}
} }
} }
} }
} }
struct ProfilePicturesOverlappedView_Previews: PreviewProvider { //struct ProfilePicturesOverlappedView_Previews: PreviewProvider {
static var previews: some View { // static var previews: some View {
ProfilePicturesOverlappedView() // ProfilePicturesOverlappedView()
} // }
} //}
@@ -25,7 +25,7 @@ struct CircleGridView: View {
// magic variables for grid // magic variables for grid
// TODO: make the top left person or one horizontal person be in the center of screen...makes the top honeycomb pop // TODO: make the top left person or one horizontal person be in the center of screen...makes the top honeycomb pop
@State var numberOfItems: Int = 12 @State var numberOfItems: Int = 12
private static let size: CGFloat = UIScreen.main.bounds.height*0.15 // scaling with screen size private static let size: CGFloat = 130 // TODO: scaling with screen size? nahhh no need
private static let spacingBetweenColumns: CGFloat = 0 private static let spacingBetweenColumns: CGFloat = 0
private static let spacingBetweenRows: CGFloat = 0 private static let spacingBetweenRows: CGFloat = 0
private static let totalColumns: Int = 3 // scaling the circles and calculating column count private static let totalColumns: Int = 3 // scaling the circles and calculating column count
@@ -54,6 +54,8 @@ struct CircleGridView: View {
@State var animateLiveConvos = false @State var animateLiveConvos = false
let maxNumAvatarsToShowInConvo = 3
var body: some View { var body: some View {
// main communication hub // main communication hub
// TODO: client side, sort the honeycomb from top left to bottom right // TODO: client side, sort the honeycomb from top left to bottom right
@@ -74,6 +76,10 @@ struct CircleGridView: View {
// live convos // live convos
ForEach(0..<convos.count, id: \.self) {value in ForEach(0..<convos.count, id: \.self) {value in
let currConvo = self.convoVM.relevantConvos[value] let currConvo = self.convoVM.relevantConvos[value]
let filteredUsers = self.authSessionStore.relevantUsersDict.filter {
return currConvo.users.contains($0.key)
}
let convoUsers: [User] = Array(filteredUsers.values)
GeometryReader {gridProxy in GeometryReader {gridProxy in
let scale = getScale(proxy: gridProxy, itemNumber: value, userId: nil, convoId: currConvo.id) let scale = getScale(proxy: gridProxy, itemNumber: value, userId: nil, convoId: currConvo.id)
@@ -81,29 +87,26 @@ struct CircleGridView: View {
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
Circle() Circle()
.foregroundColor(self.getBubbleTint(convoId: currConvo.id)) .foregroundColor(self.getBubbleTint(convoId: currConvo.id))
ZStack { ZStack {
Color.clear Color.clear
ProfilePicturesOverlappedView() ProfilePicturesOverlappedView(users: Array(convoUsers.prefix(maxNumAvatarsToShowInConvo)))
.shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 20) .shadow(color: Color.black.opacity(0.2), radius: 10, x: 0, y: 20)
} }
Text("+2") // top right number or people in convo
Circle()
.frame(width: 20, height: 20)
.foregroundColor(Color.green)
.font(.title2)
.padding(5) .padding(5)
.font(.caption) .overlay(
.foregroundColor(Color.white) Text("\(convoUsers.count)")
.background(NirvanaColor.dimTeal) .foregroundColor(Color.white)
.cornerRadius(100) .font(.caption2)
)
// ZStack(alignment: .bottom) {
// Color.clear
//
// Text("kev, sarth...")
// .foregroundColor(NirvanaColor.teal)
// .font(.caption)
// }
} }
.scaleEffect(self.animateLiveConvos ? scale + 0.2 : scale) .scaleEffect(scale)
.padding(scale * 5) .padding(scale * 5)
} // geometry reader } // geometry reader
.offset( .offset(
@@ -115,21 +118,26 @@ struct CircleGridView: View {
.onTapGesture { .onTapGesture {
// if not in a call already // if not in a call already
if !self.convoVM.isInCall() { if !self.convoVM.isInCall() {
self.convoVM.selectedConvoId = self.convoVM.relevantConvos[value].id! print("joining convo now")
self.convoVM.joinConvo()
// if had selected an individual, don't want them in here anymore if let convoId = currConvo.id {
self.selectedFriendIndex = nil self.convoVM.selectedConvoId = convoId
self.convoVM.joinConvo(convoId: convoId)
// if had selected an individual, don't want them selected here anymore
self.selectedFriendIndex = nil
}
else {
print("no convo id to join")
}
return
} }
} }
.animation( // .animation(
Animation.easeInOut(duration: self.convoVM.selectedConvoId == currConvo.id ? 2 : 4 // Animation.easeInOut(duration: 4).repeatForever(autoreverses: true),
).repeatForever(autoreverses: true), // value: self.animateLiveConvos
value: self.animateLiveConvos // )
) .animation(Animation.spring())
} }
// active friends // active friends
@@ -141,25 +149,16 @@ struct CircleGridView: View {
let scale = getScale(proxy: gridProxy, itemNumber: adjustedValue, userId: friendId) let scale = getScale(proxy: gridProxy, itemNumber: adjustedValue, userId: friendId)
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
// bubble color
Circle() Circle()
.foregroundColor(self.getBubbleTint(friendDbId: friendId)) // different color for a selected user .foregroundColor(self.getBubbleTint(friendDbId: friendId))
.blur(radius: 8) .blur(radius: 8)
.cornerRadius(100) .cornerRadius(100)
// this friend is online // user status
if self.authSessionStore.relevantUsersDict[friendId]?.userStatus == .online { UserStatusView(status: self.authSessionStore.relevantUsersDict[friendId]?.userStatus, size: 20, padding: 5)
Circle()
.frame(width: 20, height: 20) // inside avatar
.foregroundColor(Color.green)
.font(.title2)
.padding(5)
}
else if self.haveNewMessageFromFriend(friendDbId: friendId) { // check if the last message in the conversation between me and my friend was me talking or him
Image(systemName: "arrow.down.left.circle.fill")
.foregroundColor(Color.orange)
.font(.title2)
}
//
Image(self.authSessionStore.relevantUsersDict[friendId]?.avatar ?? Avatars.avatarSystemNames[0]) Image(self.authSessionStore.relevantUsersDict[friendId]?.avatar ?? Avatars.avatarSystemNames[0])
.resizable() .resizable()
.scaledToFit() .scaledToFit()
@@ -183,6 +182,12 @@ struct CircleGridView: View {
.gesture( .gesture(
LongPressGesture(minimumDuration: longPressMinDuration) LongPressGesture(minimumDuration: longPressMinDuration)
.onEnded {_ in // on activation of long press .onEnded {_ in // on activation of long press
// if in a call, I cannot record or select someone...strict rules on user if in a call
if self.convoVM.isInCall() {
print("can't select another friend because I am in a call...leave call first")
return
}
// stop any player still playing of a message // stop any player still playing of a message
self.queuePlayer.removeAllItems() self.queuePlayer.removeAllItems()
@@ -331,7 +336,7 @@ struct CircleGridView: View {
scrollReaderValue.scrollTo(self.selectedFriendIndex) scrollReaderValue.scrollTo(self.selectedFriendIndex)
} }
self.animateLiveConvos = true self.animateLiveConvos = true
} }
} // scrollview reader } // scrollview reader
} }
@@ -457,16 +462,35 @@ extension CircleGridView {
extension CircleGridView { extension CircleGridView {
// listening to messages // listening to messages
private func handleTap(gridItemIndex: Int, friendId: String) { private func handleTap(gridItemIndex: Int, friendId: String) {
print("tap gesture activated") print("tap gesture activated")
// if friend and I are online, start call immediately // if friend and I are online, and I am not in a convo, start convo immediately with them
if self.authSessionStore.relevantUsersDict[friendId]?.userStatus == .online if self.authSessionStore.relevantUsersDict[friendId]?.userStatus == .online
&& self.authSessionStore.user?.userStatus == .online { && self.authSessionStore.user?.userStatus == .online && !self.convoVM.isInCall() {
print("starting a direct convo...getting it going")
self.selectedFriendIndex = nil // deselect and clear up ui for the call
self.convoVM.startConvo(friendId: friendId) self.convoVM.startConvo(friendId: friendId)
return
} // if I am in a convo and I am adding someone else online, then make them join my convo
else if self.convoVM.isInCall() && self.authSessionStore.relevantUsersDict[friendId]?.userStatus == .online {
print("chaining the convo with more people...forcing someone else in")
self.selectedFriendIndex = nil // deselect and clear up ui for the call
self.convoVM.addThirdPartyToConvo(friendId: friendId)
return
} // if I am in a convo, don't allow listening to a message or expanding details of a friend in my circle
else if self.convoVM.isInCall() {
// TODO: show toast
print("can't select this friend as you are in a convo")
return return
} }
// clearing the player to make room for this friend's convo or to deselect this user // clearing the player to make room for this friend's convo or to deselect this user
self.queuePlayer.removeAllItems() self.queuePlayer.removeAllItems()
@@ -103,22 +103,7 @@ struct CircleNavigationView: View {
.shadow(radius: 10) .shadow(radius: 10)
.overlay(alignment: .topTrailing) { .overlay(alignment: .topTrailing) {
// user status // user status
switch self.authSessionStore.user?.userStatus { UserStatusView(status: self.authSessionStore.user?.userStatus, size: 10)
case .online:
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.green)
case .offline:
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.red)
case .inConvo:
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.orange)
default:
EmptyView()
}
} }
} }
} }
@@ -0,0 +1,43 @@
//
// UserStatusView.swift
// nirvana-ios
//
// Created by Arjun Patel on 1/1/22.
//
import SwiftUI
struct UserStatusView: View {
var status: UserStatus?
var size: CGFloat = 10
var padding: CGFloat = 0
var body: some View {
// user status
switch self.status {
case .online:
Circle()
.frame(width: size, height: size)
.foregroundColor(Color.green)
.padding(padding)
case .offline:
Circle()
.frame(width: size, height: size)
.foregroundColor(Color.red)
.padding(padding)
case .inConvo:
Circle()
.frame(width: size, height: size)
.foregroundColor(Color.orange)
.padding(padding)
default:
EmptyView()
}
}
}
//struct UserStatusView_Previews: PreviewProvider {
// static var previews: some View {
// UserStatusView()
// }
//}