Merge pull request #4 from talksik/live-voice-call-api-integration
Live voice call api integration
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
89091997277DDE030025F4BB /* ProfilePicturesOverlappedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89091996277DDE030025F4BB /* ProfilePicturesOverlappedView.swift */; };
|
||||
89091999277EFDCC0025F4BB /* leaveSound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 89091998277EFDCC0025F4BB /* leaveSound.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 */; };
|
||||
890C2460277BF896009B4A30 /* AgoraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 890C245F277BF896009B4A30 /* AgoraViewController.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -316,6 +318,7 @@
|
||||
891A15FE27656F9200B26659 /* HeaderView.swift */,
|
||||
89288ECA27675B9F00E962C4 /* OnboardingTemplateView.swift */,
|
||||
89BCABEC276D2B05009E9B10 /* WavesGlassBackgroundView.swift */,
|
||||
8909199D278155920025F4BB /* UserStatusView.swift */,
|
||||
);
|
||||
path = Templates;
|
||||
sourceTree = "<group>";
|
||||
@@ -656,6 +659,7 @@
|
||||
8904DEC8277055E90071E6CA /* OnboardingOneView.swift in Sources */,
|
||||
896CE7022770892F00DB474E /* CircleGridView.swift in Sources */,
|
||||
89091995277DD9F80025F4BB /* ConvoFooterView.swift in Sources */,
|
||||
8909199E278155920025F4BB /* UserStatusView.swift in Sources */,
|
||||
89BDCDEA27697F9C00577829 /* UserMenu.swift in Sources */,
|
||||
89BCABE7276B3D64009E9B10 /* InnerCircleViewModel.swift in Sources */,
|
||||
89A107E5277476CC00B971FD /* PushNotificationService.swift in Sources */,
|
||||
|
||||
@@ -30,7 +30,7 @@ struct Convo: Identifiable, Codable {
|
||||
|
||||
@ServerTimestamp var startedTimestamp: Date?
|
||||
var endedTimestamp: Date?
|
||||
var receiverEndedTimestamp: Date?
|
||||
var secondToLastUserEndedTimestamp: Date?
|
||||
}
|
||||
|
||||
//struct DetailConvo: Convo {
|
||||
|
||||
@@ -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
|
||||
self.firestoreService.getUserRealtime(userId: currUserId!) {[weak self] realtimeUpdatedUser in
|
||||
if realtimeUpdatedUser != nil {
|
||||
print("up to date user: \(realtimeUpdatedUser)")
|
||||
print("up to date user")
|
||||
self?.user = realtimeUpdatedUser
|
||||
self?.objectWillChange.send()
|
||||
|
||||
self?.relevantUsersDict[currUserId!] = realtimeUpdatedUser
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,89 +9,95 @@ import SwiftUI
|
||||
|
||||
struct ConvoFooterView: View {
|
||||
@EnvironmentObject var convoVM: ConvoViewModel
|
||||
@EnvironmentObject var authSessionStore: AuthSessionStore
|
||||
|
||||
@State private var convoRelativeTime = "started 20 minutes ago"
|
||||
@State private var selectedConvo: Convo? = nil
|
||||
|
||||
var body: some View {
|
||||
|
||||
ZStack(alignment:.bottomTrailing) {
|
||||
VStack(alignment:.center) {
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
ProfilePicturesOverlappedView(indvAvatarWidth: CGFloat(35))
|
||||
.padding(.leading, 20)
|
||||
.padding(.trailing, 10)
|
||||
|
||||
|
||||
// meta data of convo
|
||||
VStack (alignment: .leading) {
|
||||
Text("kev, sarth, and 5 others")
|
||||
.font(.footnote)
|
||||
.foregroundColor(NirvanaColor.light)
|
||||
if self.selectedConvo != nil {
|
||||
let filteredUsers = self.authSessionStore.relevantUsersDict.filter {
|
||||
return self.selectedConvo!.users.contains($0.key)
|
||||
}
|
||||
let convoUsers: [User] = Array(filteredUsers.values)
|
||||
|
||||
switch self.convoVM.connectionState {
|
||||
case .connecting:
|
||||
Label("connecting", systemImage: "chart.bar")
|
||||
.foregroundColor(Color.orange)
|
||||
.font(.caption)
|
||||
case .connected:
|
||||
Label("connected", systemImage: "chart.bar.fill")
|
||||
.foregroundColor(Color.green)
|
||||
.font(.caption)
|
||||
case .reconnecting:
|
||||
Label("reconnecting", systemImage: "chart.bar")
|
||||
.foregroundColor(Color.orange)
|
||||
.font(.caption)
|
||||
case .failed:
|
||||
Label("failed", systemImage: "chart.bar")
|
||||
.foregroundColor(Color.red)
|
||||
.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)
|
||||
ProfilePicturesOverlappedView(indvAvatarWidth: CGFloat(35), users: convoUsers)
|
||||
.padding(.leading, 20)
|
||||
.padding(.trailing, 10)
|
||||
|
||||
// meta data of convo
|
||||
VStack (alignment: .leading) {
|
||||
ConvoUsernames(users: convoUsers)
|
||||
|
||||
switch self.convoVM.connectionState {
|
||||
case .connecting:
|
||||
Label("connecting", systemImage: "chart.bar")
|
||||
.foregroundColor(Color.orange)
|
||||
.font(.caption)
|
||||
case .connected:
|
||||
Label("connected", systemImage: "chart.bar.fill")
|
||||
.foregroundColor(Color.green)
|
||||
.font(.caption)
|
||||
case .reconnecting:
|
||||
Label("reconnecting", systemImage: "chart.bar")
|
||||
.foregroundColor(Color.orange)
|
||||
.font(.caption)
|
||||
case .failed:
|
||||
Label("failed", systemImage: "chart.bar")
|
||||
.foregroundColor(Color.red)
|
||||
.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)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// disconnect button
|
||||
Button {
|
||||
print("showing more info about the chat")
|
||||
Spacer()
|
||||
|
||||
// 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: {
|
||||
Label("attendees", systemImage: "person.2.circle.fill")
|
||||
.labelStyle(.iconOnly)
|
||||
.foregroundColor(NirvanaColor.teal)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
.background(Color.white.opacity(0.5))
|
||||
@@ -105,17 +111,25 @@ struct ConvoFooterView: View {
|
||||
x: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
|
||||
|
||||
if self.convoVM.selectedConvoId == nil {
|
||||
print("nothing to show as there is no convo selected")
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
// let formatter = RelativeDateTimeFormatter()
|
||||
// formatter.unitsStyle = .full
|
||||
//
|
||||
// let relativeDate = formatter.localizedString(for: ?.sentTimestamp ?? Date(), relativeTo: Date())
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .full
|
||||
self.convoRelativeTime = formatter.localizedString(for: self.selectedConvo?.startedTimestamp ?? Date(), relativeTo: Date())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,3 +139,14 @@ struct ConvoFooterView_Previews: PreviewProvider {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import Firebase
|
||||
|
||||
class ConvoViewModel: NSObject, ObservableObject {
|
||||
@Published var selectedConvoId:String? = nil
|
||||
@Published var connectionState: AgoraConnectionStateType? = nil
|
||||
@Published var connectionState: AgoraConnectionStateType?
|
||||
|
||||
let firestoreService = FirestoreService()
|
||||
private var db = Firestore.firestore()
|
||||
@@ -19,7 +19,7 @@ class ConvoViewModel: NSObject, ObservableObject {
|
||||
|
||||
let agoraService = AgoraService()
|
||||
// TODO: put the value in environment variables
|
||||
lazy var agoraKit: AgoraRtcEngineKit? = AgoraRtcEngineKit.sharedEngine(withAppId: "c8dfd65deb5c4741bd564085627139d0", delegate: self)
|
||||
var agoraKit: AgoraRtcEngineKit?
|
||||
|
||||
private var allConvos: [Convo] = []
|
||||
@Published var relevantConvos: [Convo] = []
|
||||
@@ -28,15 +28,22 @@ class ConvoViewModel: NSObject, ObservableObject {
|
||||
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() {
|
||||
super.init()
|
||||
|
||||
initializeAgoraEngine()
|
||||
|
||||
// start process of data collection
|
||||
let userId = AuthSessionStore.getCurrentUserId()
|
||||
if userId == nil {
|
||||
print("no authenticated user")
|
||||
return
|
||||
}
|
||||
|
||||
// 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!.receiverUserId == userId && !(self?.isInCall())!
|
||||
&& convo!.receiverEndedTimestamp == nil {
|
||||
&& convo!.secondToLastUserEndedTimestamp == nil {
|
||||
print("I am the direct receiver...joining 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 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()
|
||||
} //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!)
|
||||
@@ -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)")
|
||||
|
||||
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!!!")
|
||||
return true
|
||||
}
|
||||
@@ -126,15 +139,12 @@ class ConvoViewModel: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
deinit {
|
||||
// deinit the firestore listener
|
||||
self.convosListener?.remove()
|
||||
|
||||
// deinit the agora engine
|
||||
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
|
||||
if self.isInCall() {
|
||||
self.leaveConvo()
|
||||
@@ -186,25 +197,19 @@ class ConvoViewModel: NSObject, ObservableObject {
|
||||
|
||||
self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true)
|
||||
// finally join channel
|
||||
self.agoraKit?.delegate = self
|
||||
self.agoraKit?.joinChannel(byToken: convo.agoraToken, channelId: convo.id!, info: nil, uid: 0, joinSuccess: nil)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
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 that this user leaves all other channels
|
||||
if self.isInCall() {
|
||||
print("can't join another call, already in one")
|
||||
return
|
||||
}
|
||||
// ensure that this user leaves all other channels
|
||||
|
||||
let convo = self.relevantConvos.first {convo in
|
||||
convo.id == self.selectedConvoId
|
||||
convo.id == convoId
|
||||
}
|
||||
|
||||
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...
|
||||
|
||||
self.selectedConvoId = convoId
|
||||
let convoAgoraToken:String = convo!.agoraToken
|
||||
let channelName:String = convo!.id!
|
||||
|
||||
print("joining convo: \(channelName)")
|
||||
|
||||
self.agoraKit?.setDefaultAudioRouteToSpeakerphone(true)
|
||||
// finally join channel
|
||||
self.agoraKit?.delegate = self
|
||||
self.agoraKit?.joinChannel(byToken: convoAgoraToken, channelId: channelName, info: nil, uid: 0, joinSuccess: nil)
|
||||
}
|
||||
|
||||
// anyone can leave at any time
|
||||
func leaveConvo() {
|
||||
self.agoraKit?.delegate = self
|
||||
agoraKit?.leaveChannel(nil)
|
||||
func addThirdPartyToConvo(friendId: String) {
|
||||
// recheck if this other friend that I am adding is online/free
|
||||
|
||||
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()
|
||||
|
||||
if userId == nil {
|
||||
@@ -241,6 +270,8 @@ class ConvoViewModel: NSObject, ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
agoraKit?.leaveChannel(nil)
|
||||
|
||||
self.firestoreService.updateUserStatus(userId: userId!, userStatus: .online) {[weak self] res in
|
||||
print(res)
|
||||
|
||||
@@ -269,7 +300,7 @@ class ConvoViewModel: NSObject, ObservableObject {
|
||||
updatedConvo!.endedTimestamp = Date()
|
||||
} // if I am second to last, the last guy is a loner
|
||||
else if updatedConvo!.users.count == 2 && updatedConvo!.users.contains(userId!) {
|
||||
updatedConvo!.receiverEndedTimestamp = Date()
|
||||
updatedConvo!.secondToLastUserEndedTimestamp = Date()
|
||||
}
|
||||
|
||||
let updatedUsers: [String] = updatedConvo!.users.filter{ arrUserId in
|
||||
@@ -282,13 +313,19 @@ class ConvoViewModel: NSObject, ObservableObject {
|
||||
self?.selectedConvoId = nil
|
||||
|
||||
print("left convo officially in our db as well")
|
||||
|
||||
|
||||
// deinit the agora engine
|
||||
if destroyInstance {
|
||||
self?.destroyInstance()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func destroyInstance() {
|
||||
AgoraRtcEngineKit.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConvoViewModel {
|
||||
@@ -377,7 +414,10 @@ extension ConvoViewModel: AgoraRtcEngineDelegate {
|
||||
case .success:
|
||||
// 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
|
||||
|
||||
self?.firestoreService.updateConvo(convo: updatedConvo!) {[weak self] res in
|
||||
|
||||
@@ -7,67 +7,70 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ConvosView: View {
|
||||
@EnvironmentObject var vm: ConvoViewModel
|
||||
@State var animate = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView([.horizontal]) {
|
||||
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
|
||||
/**
|
||||
deprecated...used to insert into circlegridview but can isolate later
|
||||
*/
|
||||
//struct ConvosView: View {
|
||||
// @EnvironmentObject var vm: ConvoViewModel
|
||||
// @State var animate = false
|
||||
//
|
||||
// Text("kev, sarth...")
|
||||
// .foregroundColor(NirvanaColor.teal)
|
||||
// .font(.caption)
|
||||
// var body: some View {
|
||||
// ScrollView([.horizontal]) {
|
||||
// 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)
|
||||
.scaleEffect(self.animate ? 1 : 0.85)
|
||||
.animation(
|
||||
Animation.easeInOut(duration: self.vm.selectedConvoId == currConvo.id ? 2 : 4
|
||||
).repeatForever(autoreverses: true),
|
||||
value: self.animate
|
||||
)
|
||||
.onTapGesture {
|
||||
// if not in a call already
|
||||
if !self.vm.isInCall() {
|
||||
self.vm.selectedConvoId = self.vm.relevantConvos[index].id!
|
||||
self.vm.joinConvo()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.vm.leaveConvo()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
self.animate = true
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
//
|
||||
// Text("+2")
|
||||
// .padding(5)
|
||||
// .font(.caption)
|
||||
// .foregroundColor(Color.white)
|
||||
// .background(NirvanaColor.dimTeal)
|
||||
// .cornerRadius(100)
|
||||
//
|
||||
//// ZStack(alignment: .bottom) {
|
||||
//// Color.clear
|
||||
////
|
||||
//// Text("kev, sarth...")
|
||||
//// .foregroundColor(NirvanaColor.teal)
|
||||
//// .font(.caption)
|
||||
//// }
|
||||
// }
|
||||
// .frame(width: 100, height: 100)
|
||||
// .scaleEffect(self.animate ? 1 : 0.85)
|
||||
// .animation(
|
||||
// Animation.easeInOut(duration: self.vm.selectedConvoId == currConvo.id ? 2 : 4
|
||||
// ).repeatForever(autoreverses: true),
|
||||
// value: self.animate
|
||||
// )
|
||||
// .onTapGesture {
|
||||
// // if not in a call already
|
||||
// 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 {
|
||||
// static var previews: some View {
|
||||
|
||||
@@ -7,31 +7,33 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
@return a view that overlaps the user avatars
|
||||
*/
|
||||
struct ProfilePicturesOverlappedView: View {
|
||||
let numberAttendees:Int = 5
|
||||
let offset:Int = 15
|
||||
|
||||
var indvAvatarWidth: CGFloat = 50
|
||||
|
||||
var users: [User]
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ForEach(0..<numberAttendees, id: \.self) {attendeeIndex in
|
||||
if attendeeIndex < 3 {
|
||||
Image(Avatars.avatarSystemNames[attendeeIndex+1])
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.background(NirvanaColor.dimTeal)
|
||||
.clipShape(Circle())
|
||||
.offset(x: CGFloat((-15 * attendeeIndex)) + 15)
|
||||
.frame(width: indvAvatarWidth)
|
||||
}
|
||||
ForEach(0..<users.count, id: \.self) {userIndex in
|
||||
Image(users[userIndex].avatar ?? Avatars.avatarSystemNames[1])
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.background(NirvanaColor.dimTeal)
|
||||
.clipShape(Circle())
|
||||
.offset(x: CGFloat((-15 * userIndex)) + 15)
|
||||
.frame(width: indvAvatarWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfilePicturesOverlappedView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProfilePicturesOverlappedView()
|
||||
}
|
||||
}
|
||||
//struct ProfilePicturesOverlappedView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ProfilePicturesOverlappedView()
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -25,7 +25,7 @@ struct CircleGridView: View {
|
||||
// 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
|
||||
@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 spacingBetweenRows: CGFloat = 0
|
||||
private static let totalColumns: Int = 3 // scaling the circles and calculating column count
|
||||
@@ -54,6 +54,8 @@ struct CircleGridView: View {
|
||||
|
||||
@State var animateLiveConvos = false
|
||||
|
||||
let maxNumAvatarsToShowInConvo = 3
|
||||
|
||||
var body: some View {
|
||||
// main communication hub
|
||||
// TODO: client side, sort the honeycomb from top left to bottom right
|
||||
@@ -74,6 +76,10 @@ struct CircleGridView: View {
|
||||
// live convos
|
||||
ForEach(0..<convos.count, id: \.self) {value in
|
||||
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
|
||||
let scale = getScale(proxy: gridProxy, itemNumber: value, userId: nil, convoId: currConvo.id)
|
||||
@@ -81,29 +87,26 @@ struct CircleGridView: View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Circle()
|
||||
.foregroundColor(self.getBubbleTint(convoId: currConvo.id))
|
||||
|
||||
|
||||
ZStack {
|
||||
Color.clear
|
||||
ProfilePicturesOverlappedView()
|
||||
ProfilePicturesOverlappedView(users: Array(convoUsers.prefix(maxNumAvatarsToShowInConvo)))
|
||||
.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)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.white)
|
||||
.background(NirvanaColor.dimTeal)
|
||||
.cornerRadius(100)
|
||||
|
||||
// ZStack(alignment: .bottom) {
|
||||
// Color.clear
|
||||
//
|
||||
// Text("kev, sarth...")
|
||||
// .foregroundColor(NirvanaColor.teal)
|
||||
// .font(.caption)
|
||||
// }
|
||||
.overlay(
|
||||
Text("\(convoUsers.count)")
|
||||
.foregroundColor(Color.white)
|
||||
.font(.caption2)
|
||||
)
|
||||
}
|
||||
.scaleEffect(self.animateLiveConvos ? scale + 0.2 : scale)
|
||||
.scaleEffect(scale)
|
||||
.padding(scale * 5)
|
||||
} // geometry reader
|
||||
.offset(
|
||||
@@ -115,21 +118,26 @@ struct CircleGridView: View {
|
||||
.onTapGesture {
|
||||
// if not in a call already
|
||||
if !self.convoVM.isInCall() {
|
||||
self.convoVM.selectedConvoId = self.convoVM.relevantConvos[value].id!
|
||||
self.convoVM.joinConvo()
|
||||
print("joining convo now")
|
||||
|
||||
// if had selected an individual, don't want them in here anymore
|
||||
self.selectedFriendIndex = nil
|
||||
if let convoId = currConvo.id {
|
||||
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.easeInOut(duration: self.convoVM.selectedConvoId == currConvo.id ? 2 : 4
|
||||
).repeatForever(autoreverses: true),
|
||||
value: self.animateLiveConvos
|
||||
)
|
||||
|
||||
// .animation(
|
||||
// Animation.easeInOut(duration: 4).repeatForever(autoreverses: true),
|
||||
// value: self.animateLiveConvos
|
||||
// )
|
||||
.animation(Animation.spring())
|
||||
}
|
||||
|
||||
// active friends
|
||||
@@ -141,25 +149,16 @@ struct CircleGridView: View {
|
||||
let scale = getScale(proxy: gridProxy, itemNumber: adjustedValue, userId: friendId)
|
||||
|
||||
ZStack(alignment: .topTrailing) {
|
||||
// bubble color
|
||||
Circle()
|
||||
.foregroundColor(self.getBubbleTint(friendDbId: friendId)) // different color for a selected user
|
||||
.foregroundColor(self.getBubbleTint(friendDbId: friendId))
|
||||
.blur(radius: 8)
|
||||
.cornerRadius(100)
|
||||
|
||||
// this friend is online
|
||||
if self.authSessionStore.relevantUsersDict[friendId]?.userStatus == .online {
|
||||
Circle()
|
||||
.frame(width: 20, height: 20)
|
||||
.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)
|
||||
}
|
||||
//
|
||||
// user status
|
||||
UserStatusView(status: self.authSessionStore.relevantUsersDict[friendId]?.userStatus, size: 20, padding: 5)
|
||||
|
||||
// inside avatar
|
||||
Image(self.authSessionStore.relevantUsersDict[friendId]?.avatar ?? Avatars.avatarSystemNames[0])
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
@@ -183,6 +182,12 @@ struct CircleGridView: View {
|
||||
.gesture(
|
||||
LongPressGesture(minimumDuration: longPressMinDuration)
|
||||
.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
|
||||
self.queuePlayer.removeAllItems()
|
||||
|
||||
@@ -331,7 +336,7 @@ struct CircleGridView: View {
|
||||
scrollReaderValue.scrollTo(self.selectedFriendIndex)
|
||||
}
|
||||
|
||||
self.animateLiveConvos = true
|
||||
self.animateLiveConvos = true
|
||||
}
|
||||
} // scrollview reader
|
||||
}
|
||||
@@ -457,16 +462,35 @@ extension CircleGridView {
|
||||
extension CircleGridView {
|
||||
// listening to messages
|
||||
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
|
||||
&& 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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
// clearing the player to make room for this friend's convo or to deselect this user
|
||||
self.queuePlayer.removeAllItems()
|
||||
|
||||
|
||||
@@ -103,22 +103,7 @@ struct CircleNavigationView: View {
|
||||
.shadow(radius: 10)
|
||||
.overlay(alignment: .topTrailing) {
|
||||
// user status
|
||||
switch self.authSessionStore.user?.userStatus {
|
||||
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()
|
||||
}
|
||||
UserStatusView(status: self.authSessionStore.user?.userStatus, size: 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
// }
|
||||
//}
|
||||
Reference in New Issue
Block a user