mobile: call chat items (#680)

* mobile: call chat items

* android: call chat items
This commit is contained in:
Evgeny Poberezkin
2022-05-21 12:13:37 +01:00
committed by GitHub
parent d2d8498258
commit d971e7c31f
17 changed files with 396 additions and 77 deletions
@@ -121,6 +121,22 @@ fun ChatView(chatModel: ChatModel) {
chatModel.showCallView.value = true
chatModel.callCommand.value = WCallCommand.Capabilities
}
},
acceptCall = { contact ->
val invitation = chatModel.callInvitations.remove(contact.id)
if (invitation == null) {
AlertManager.shared.showAlertMsg("Call already ended!")
} else {
chatModel.activeCallInvitation.value = null
chatModel.activeCall.value = Call(
contact = contact,
callState = CallState.InvitationReceived,
localMedia = invitation.peerMedia,
sharedKey = invitation.sharedKey
)
chatModel.showCallView.value = true
chatModel.callCommand.value = WCallCommand.Start(media = invitation.peerMedia, aesKey = invitation.sharedKey)
}
}
)
}
@@ -141,7 +157,8 @@ fun ChatLayout(
openDirectChat: (Long) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
startCall: (CallMediaType) -> Unit
startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit
) {
Surface(
Modifier
@@ -167,7 +184,7 @@ fun ChatLayout(
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
ChatItemsList(user, chat, composeState, chatItems, openDirectChat, deleteMessage, receiveFile)
ChatItemsList(user, chat, composeState, chatItems, openDirectChat, deleteMessage, receiveFile, acceptCall)
}
}
}
@@ -255,7 +272,8 @@ fun ChatItemsList(
chatItems: List<ChatItem>,
openDirectChat: (Long) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit
receiveFile: (Long) -> Unit,
acceptCall: (Contact) -> Unit
) {
val listState = rememberLazyListState(initialFirstVisibleItemIndex = chatItems.size - chatItems.count { it.isRcvNew })
val keyboardState by getKeyboardState()
@@ -293,11 +311,11 @@ fun ChatItemsList(
} else {
Spacer(Modifier.size(42.dp))
}
ChatItemView(user, cItem, composeState, cxt, uriHandler, showMember = showMember, deleteMessage = deleteMessage, receiveFile = receiveFile)
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, showMember = showMember, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall)
}
} else {
Box(Modifier.padding(start = 86.dp, end = 12.dp)) {
ChatItemView(user, cItem, composeState, cxt, uriHandler, deleteMessage = deleteMessage, receiveFile = receiveFile)
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall)
}
}
} else { // direct message
@@ -308,7 +326,7 @@ fun ChatItemsList(
end = if (sent) 12.dp else 76.dp,
)
) {
ChatItemView(user, cItem, composeState, cxt, uriHandler, deleteMessage = deleteMessage, receiveFile = receiveFile)
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, deleteMessage = deleteMessage, receiveFile = receiveFile, acceptCall = acceptCall)
}
}
}
@@ -377,7 +395,8 @@ fun PreviewChatLayout() {
openDirectChat = {},
deleteMessage = { _, _ -> },
receiveFile = {},
startCall = {}
startCall = {},
acceptCall = { _ -> }
)
}
}
@@ -422,7 +441,8 @@ fun PreviewGroupChatLayout() {
openDirectChat = {},
deleteMessage = { _, _ -> },
receiveFile = {},
startCall = {}
startCall = {},
acceptCall = { _ -> }
)
}
}
@@ -0,0 +1,157 @@
package chat.simplex.app.views.chat.item
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PhoneInTalk
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleButton
@Composable
fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, duration: Int, acceptCall: (Contact) -> Unit) {
val sent = cItem.chatDir.sent
Column(
Modifier
.padding(horizontal = 4.dp)
.padding(bottom = 8.dp), horizontalAlignment = Alignment.CenterHorizontally) {
@Composable fun ConnectingCallIcon() = Icon(Icons.Outlined.SettingsPhone, stringResource(R.string.icon_descr_call_connecting), tint = Color.Green)
when (status) {
CICallStatus.Pending -> if (sent) {
Icon(Icons.Outlined.Call, stringResource(R.string.icon_descr_call_pending_sent))
} else {
AcceptCallButton(cInfo, acceptCall)
}
CICallStatus.Missed -> Icon(Icons.Outlined.Call, stringResource(R.string.icon_descr_call_missed), tint = Color.Red)
CICallStatus.Rejected -> Icon(Icons.Outlined.CallEnd, stringResource(R.string.icon_descr_call_rejected), tint = HighOrLowlight)
CICallStatus.Accepted -> ConnectingCallIcon()
CICallStatus.Negotiated -> ConnectingCallIcon()
CICallStatus.Progress -> Icon(Icons.Filled.PhoneInTalk, stringResource(R.string.icon_descr_call_progress), tint = Color.Green)
CICallStatus.Ended -> Row {
Icon(Icons.Outlined.CallEnd, stringResource(R.string.icon_descr_call_ended), tint = HighOrLowlight, modifier = Modifier.padding(end = 4.dp))
Text(status.duration(duration), color = HighOrLowlight)
}
}
Text(
cItem.timestampText,
color = HighOrLowlight,
fontSize = 14.sp,
modifier = Modifier.padding(start = 3.dp)
)
}
}
@Composable
fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
if (cInfo is ChatInfo.Direct) {
SimpleButton(stringResource(R.string.answer_call), Icons.Outlined.RingVolume) { acceptCall(cInfo.contact) }
} else {
Icon(Icons.Outlined.RingVolume, stringResource(R.string.answer_call), tint = HighOrLowlight)
}
// if case let .direct(contact) = chatInfo {
// Button {
// if let invitation = m.callInvitations.removeValue(forKey: contact.id) {
// m.activeCallInvitation = nil
// m.activeCall = Call(
// contact: contact,
// callState: .invitationReceived,
// localMedia: invitation.peerMedia,
// sharedKey: invitation.sharedKey
// )
// m.showCallView = true
// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true)
// } else {
// AlertManager.shared.showAlertMsg(title: "Call already ended!")
// }
// } label: {
// Label("Answer call", systemImage: "phone.arrow.down.left")
// }
// } else {
// Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary)
// }
}
//struct CICallItemView: View {
// @EnvironmentObject var m: ChatModel
// var chatInfo: ChatInfo
// var chatItem: ChatItem
// var status: CICallStatus
// var duration: Int
//
// var body: some View {
// switch status {
// case .pending:
// if sent {
// Image(systemName: "phone.arrow.up.right").foregroundColor(.secondary)
// } else {
// acceptCallButton()
// }
// case .missed: missedCallIcon(sent).foregroundColor(.red)
// case .rejected: Image(systemName: "phone.down").foregroundColor(.secondary)
// case .accepted: connectingCallIcon()
// case .negotiated: connectingCallIcon()
// case .progress: Image(systemName: "phone.and.waveform.fill").foregroundColor(.green)
// case .ended: endedCallIcon(sent)
// case .error: missedCallIcon(sent).foregroundColor(.orange)
// }
//
// chatItem.timestampText
// .font(.caption)
// .foregroundColor(.secondary)
// .padding(.bottom, 8)
// .padding(.horizontal, 12)
// }
// }
//
// private func missedCallIcon(_ sent: Bool) -> some View {
// Image(systemName: sent ? "phone.arrow.up.right" : "phone.arrow.down.left")
// }
//
// private func connectingCallIcon() -> some View {
// Image(systemName: "phone.connection").foregroundColor(.green)
// }
//
// @ViewBuilder private func endedCallIcon(_ sent: Bool) -> some View {
// HStack {
// Image(systemName: "phone.down")
// Text(CICallStatus.durationText(duration)).foregroundColor(.secondary)
// }
// }
//
//
// @ViewBuilder private func acceptCallButton() -> some View {
// if case let .direct(contact) = chatInfo {
// Button {
// if let invitation = m.callInvitations.removeValue(forKey: contact.id) {
// m.activeCallInvitation = nil
// m.activeCall = Call(
// contact: contact,
// callState: .invitationReceived,
// localMedia: invitation.peerMedia,
// sharedKey: invitation.sharedKey
// )
// m.showCallView = true
// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true)
// } else {
// AlertManager.shared.showAlertMsg(title: "Call already ended!")
// }
// } label: {
// Label("Answer call", systemImage: "phone.arrow.down.left")
// }
// } else {
// Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary)
// }
// }
//}
@@ -30,13 +30,15 @@ import kotlinx.datetime.Clock
@Composable
fun ChatItemView(
user: User,
cInfo: ChatInfo,
cItem: ChatItem,
composeState: MutableState<ComposeState>,
cxt: Context,
uriHandler: UriHandler? = null,
showMember: Boolean = false,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit
receiveFile: (Long) -> Unit,
acceptCall: (Contact) -> Unit
) {
val context = LocalContext.current
val sent = cItem.chatDir.sent
@@ -54,18 +56,12 @@ fun ChatItemView(
.clip(RoundedCornerShape(18.dp))
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = {})
) {
if (cItem.isMsgContent) {
@Composable fun ContentItem() {
if (cItem.file == null && cItem.quotedItem == null && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem)
} else {
FramedItemView(user, cItem, uriHandler, showMember = showMember, showMenu, receiveFile)
}
} else if (cItem.isDeletedContent) {
DeletedItemView(cItem, showMember = showMember)
} else if (cItem.isCall) {
FramedItemView(user, cItem, uriHandler, showMember = showMember, showMenu, receiveFile)
}
if (cItem.isMsgContent) {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
@@ -116,7 +112,10 @@ fun ChatItemView(
color = Color.Red
)
}
} else if (cItem.isDeletedContent) {
}
@Composable fun DeletedItem() {
DeletedItemView(cItem, showMember = showMember)
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
@@ -133,6 +132,19 @@ fun ChatItemView(
)
}
}
@Composable fun CallItem(status: CICallStatus, duration: Int) {
CICallItemView(cInfo, cItem, status, duration, acceptCall)
}
when (val c = cItem.content) {
is CIContent.SndMsgContent -> ContentItem()
is CIContent.RcvMsgContent -> ContentItem()
is CIContent.SndDeleted -> DeletedItem()
is CIContent.RcvDeleted -> DeletedItem()
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
}
}
}
}
@@ -186,13 +198,15 @@ fun PreviewChatItemView() {
SimpleXTheme {
ChatItemView(
User.sampleData,
ChatInfo.Direct.sampleData,
ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
),
composeState = remember { mutableStateOf(ComposeState()) },
cxt = LocalContext.current,
deleteMessage = { _, _ -> },
receiveFile = {}
receiveFile = {},
acceptCall = { _ -> }
)
}
}
@@ -203,11 +217,13 @@ fun PreviewChatItemViewDeletedContent() {
SimpleXTheme {
ChatItemView(
User.sampleData,
ChatInfo.Direct.sampleData,
ChatItem.getDeletedContentSampleData(),
composeState = remember { mutableStateOf(ComposeState()) },
cxt = LocalContext.current,
deleteMessage = { _, _ -> },
receiveFile = {}
receiveFile = {},
acceptCall = { _ -> }
)
}
}
@@ -367,4 +367,13 @@
<string name="icon_descr_audio_off">Audio off</string>
<string name="icon_descr_audio_on">Audio on</string>
<string name="icon_descr_flip_camera">Flip camera</string>
<!-- Call items -->
<string name="icon_descr_call_pending_sent">Pending call</string>
<string name="icon_descr_call_missed">Missed call</string>
<string name="icon_descr_call_rejected">Rejected call</string>
<string name="icon_descr_call_connecting">Connecting call</string>
<string name="icon_descr_call_progress">Call in progress</string>
<string name="icon_descr_call_ended">Call ended</string>
<string name="answer_call">Answer call</string>
</resources>
@@ -368,4 +368,13 @@
<string name="icon_descr_audio_off">Audio off</string>
<string name="icon_descr_audio_on">Audio on</string>
<string name="icon_descr_flip_camera">Flip camera</string>
<!-- Call items -->
<string name="icon_descr_call_pending_sent">Pending call</string>
<string name="icon_descr_call_missed">Missed call</string>
<string name="icon_descr_call_rejected">Rejected call</string>
<string name="icon_descr_call_connecting">Connecting call</string>
<string name="icon_descr_call_progress">Call in progress</string>
<string name="icon_descr_call_ended">Call ended</string>
<string name="answer_call">Answer call</string>
</resources>
+2 -1
View File
@@ -27,10 +27,11 @@ final class ChatModel: ObservableObject {
@Published var deviceToken: String?
@Published var tokenStatus = NtfTknStatus.new
// current WebRTC call
@Published var callInvitations: Dictionary<String, CallInvitation> = [:]
@Published var callInvitations: Dictionary<ChatId, CallInvitation> = [:]
@Published var activeCallInvitation: ContactRef?
@Published var activeCall: Call?
@Published var callCommand: WCallCommand?
@Published var showCallView = false
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
+3 -3
View File
@@ -38,10 +38,11 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
} else if content.categoryIdentifier == ntfCategoryCallInvitation && (action == ntfActionAcceptCall || action == ntfActionRejectCall),
let chatId = content.userInfo["chatId"] as? String,
case let .direct(contact) = chatModel.getChat(chatId)?.chatInfo,
let invitation = chatModel.callInvitations[chatId] {
let invitation = chatModel.callInvitations.removeValue(forKey: chatId) {
if action == ntfActionAcceptCall {
chatModel.activeCallInvitation = nil
chatModel.activeCall = Call(contact: contact, callState: .invitationReceived, localMedia: invitation.peerMedia)
chatModel.chatId = nil
chatModel.showCallView = true
chatModel.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey)
} else {
Task {
@@ -56,7 +57,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
}
}
}
chatModel.callInvitations.removeValue(forKey: chatId)
} else {
chatModel.chatId = content.targetContentIdentifier
}
+5 -1
View File
@@ -884,8 +884,12 @@ enum CICallStatus: String, Decodable {
case .accepted: return NSLocalizedString("accepted", comment: "call status")
case .negotiated: return NSLocalizedString("connecting…", comment: "call status")
case .progress: return NSLocalizedString("in progress", comment: "call status")
case .ended: return String.localizedStringWithFormat(NSLocalizedString("ended %02d:%02d", comment: "call status"), sec / 60, sec % 60)
case .ended: return String.localizedStringWithFormat(NSLocalizedString("ended %@", comment: "call status"), CICallStatus.durationText(sec))
case .error: return NSLocalizedString("error", comment: "call status")
}
}
static func durationText(_ sec: Int) -> String {
String(format: "%02d:%02d", sec / 60, sec % 60)
}
}
@@ -11,7 +11,6 @@ import SwiftUI
struct ActiveCallView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) private var dismiss
@Binding var showCallView: Bool
@State private var coordinator: WebRTCCoordinator? = nil
@State private var webViewReady: Bool = false
@State private var webViewMsg: WVAPIMessage? = nil
@@ -71,7 +70,7 @@ struct ActiveCallView: View {
m.activeCall = nil
m.activeCallInvitation = nil
m.callCommand = nil
showCallView = false
m.showCallView = false
case .ok:
switch msg.command {
case let .media(media, enable):
@@ -85,7 +84,7 @@ struct ActiveCallView: View {
m.activeCall = nil
m.activeCallInvitation = nil
m.callCommand = nil
showCallView = false
m.showCallView = false
default: ()
}
case let .error(message):
+1 -1
View File
@@ -388,7 +388,7 @@ struct ConnectionInfo: Codable, Equatable {
return "via relay"
} else {
let unknown = NSLocalizedString("unknown", comment: "connection info")
return "\(localCandidate?.candidateType?.rawValue ?? unknown)) / \(remoteCandidate?.candidateType?.rawValue ?? unknown)"
return "\(localCandidate?.candidateType?.rawValue ?? unknown) / \(remoteCandidate?.candidateType?.rawValue ?? unknown)"
}
}
}
@@ -0,0 +1,90 @@
//
// CICallItemView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 20/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct CICallItemView: View {
@EnvironmentObject var m: ChatModel
var chatInfo: ChatInfo
var chatItem: ChatItem
var status: CICallStatus
var duration: Int
var body: some View {
let sent = chatItem.chatDir.sent
VStack(spacing: 4) {
switch status {
case .pending:
if sent {
Image(systemName: "phone.arrow.up.right").foregroundColor(.secondary)
} else {
acceptCallButton()
}
case .missed: missedCallIcon(sent).foregroundColor(.red)
case .rejected: Image(systemName: "phone.down").foregroundColor(.secondary)
case .accepted: connectingCallIcon()
case .negotiated: connectingCallIcon()
case .progress: Image(systemName: "phone.and.waveform.fill").foregroundColor(.green)
case .ended: endedCallIcon(sent)
case .error: missedCallIcon(sent).foregroundColor(.orange)
}
chatItem.timestampText
.font(.caption)
.foregroundColor(.secondary)
.padding(.bottom, 8)
.padding(.horizontal, 12)
}
}
private func missedCallIcon(_ sent: Bool) -> some View {
Image(systemName: sent ? "phone.arrow.up.right" : "phone.arrow.down.left")
}
private func connectingCallIcon() -> some View {
Image(systemName: "phone.connection").foregroundColor(.green)
}
@ViewBuilder private func endedCallIcon(_ sent: Bool) -> some View {
HStack {
Image(systemName: "phone.down")
Text(CICallStatus.durationText(duration)).foregroundColor(.secondary)
}
}
@ViewBuilder private func acceptCallButton() -> some View {
if case let .direct(contact) = chatInfo {
Button {
if let invitation = m.callInvitations.removeValue(forKey: contact.id) {
m.activeCallInvitation = nil
m.activeCall = Call(
contact: contact,
callState: .invitationReceived,
localMedia: invitation.peerMedia,
sharedKey: invitation.sharedKey
)
m.showCallView = true
m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true)
} else {
AlertManager.shared.showAlertMsg(title: "Call already ended!")
}
} label: {
Label("Answer call", systemImage: "phone.arrow.down.left")
}
} else {
Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary)
}
}
}
//struct CICallItemView_Previews: PreviewProvider {
// static var previews: some View {
// CICallItemView()
// }
//}
+29 -15
View File
@@ -9,34 +9,48 @@
import SwiftUI
struct ChatItemView: View {
var chatInfo: ChatInfo
var chatItem: ChatItem
var showMember = false
var maxWidth: CGFloat = .infinity
var body: some View {
if chatItem.isMsgContent() {
if (chatItem.quotedItem == nil && chatItem.file == nil && isShortEmoji(chatItem.content.text)) {
EmojiItemView(chatItem: chatItem)
} else {
FramedItemView(chatItem: chatItem, showMember: showMember, maxWidth: maxWidth)
}
} else if chatItem.isDeletedContent() {
DeletedItemView(chatItem: chatItem, showMember: showMember)
} else if chatItem.isCall() {
switch chatItem.content {
case .sndMsgContent: contentItemView()
case .rcvMsgContent: contentItemView()
case .sndDeleted: deletedItemView()
case .rcvDeleted: deletedItemView()
case let .sndCall(status, duration): callItemView(status, duration)
case let .rcvCall(status, duration): callItemView(status, duration)
}
}
@ViewBuilder private func contentItemView() -> some View {
if (chatItem.quotedItem == nil && chatItem.file == nil && isShortEmoji(chatItem.content.text)) {
EmojiItemView(chatItem: chatItem)
} else {
FramedItemView(chatItem: chatItem, showMember: showMember, maxWidth: maxWidth)
}
}
private func deletedItemView() -> some View {
DeletedItemView(chatItem: chatItem, showMember: showMember)
}
private func callItemView(_ status: CICallStatus, _ duration: Int) -> some View {
CICallItemView(chatInfo: chatInfo, chatItem: chatItem, status: status, duration: duration)
}
}
struct ChatItemView_Previews: PreviewProvider {
static var previews: some View {
Group{
ChatItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"))
ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"))
ChatItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"))
ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"))
ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"))
ChatItemView(chatItem: ChatItem.getDeletedContentSample())
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getDeletedContentSample())
}
.previewLayout(.fixed(width: 360, height: 70))
}
+3 -5
View File
@@ -14,7 +14,6 @@ struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
@Binding var showCallView: Bool
@State private var composeState = ComposeState()
@State private var deletingItem: ChatItem? = nil
@FocusState private var keyboardVisible: Bool
@@ -125,7 +124,7 @@ struct ChatView: View {
callState: .waitCapabilities,
localMedia: media
)
showCallView = true
chatModel.showCallView = true
chatModel.callCommand = .capabilities(useWorker: true)
} label: {
Image(systemName: imageName)
@@ -134,7 +133,7 @@ struct ChatView: View {
private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat, showMember: Bool = false) -> some View {
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
return ChatItemView(chatItem: ci, showMember: showMember, maxWidth: maxWidth)
return ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth)
.contextMenu {
if ci.isMsgContent() {
Button {
@@ -263,7 +262,6 @@ struct ChatView: View {
struct ChatView_Previews: PreviewProvider {
static var previews: some View {
@State var showCallView = false
let chatModel = ChatModel()
chatModel.chatId = "@1"
chatModel.chatItems = [
@@ -277,7 +275,7 @@ struct ChatView_Previews: PreviewProvider {
ChatItem.getSample(8, .directSnd, .now, "👍👍👍👍"),
ChatItem.getSample(9, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
]
return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showCallView: $showCallView)
return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
.environmentObject(chatModel)
}
}
@@ -11,7 +11,6 @@ import SwiftUI
struct ChatListNavLink: View {
@EnvironmentObject var chatModel: ChatModel
@State var chat: Chat
@Binding var showCallView: Bool
@State private var showContactRequestDialog = false
var body: some View {
@@ -28,7 +27,7 @@ struct ChatListNavLink: View {
}
private func chatView() -> some View {
ChatView(chat: chat, showCallView: $showCallView)
ChatView(chat: chat)
.onAppear {
do {
let cInfo = chat.chatInfo
@@ -279,20 +278,19 @@ struct ChatListNavLink: View {
struct ChatListNavLink_Previews: PreviewProvider {
static var previews: some View {
@State var chatId: String? = "@1"
@State var showCallView = false
return Group {
ChatListNavLink(chat: Chat(
chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
), showCallView: $showCallView)
))
ChatListNavLink(chat: Chat(
chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
), showCallView: $showCallView)
))
ChatListNavLink(chat: Chat(
chatInfo: ChatInfo.sampleData.contactRequest,
chatItems: []
), showCallView: $showCallView)
))
}
.previewLayout(.fixed(width: 360, height: 80))
}
@@ -13,7 +13,6 @@ struct ChatListView: View {
// not really used in this view
@State private var showSettings = false
@State private var searchText = ""
@State private var showCallView = false
@AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true
var user: User
@@ -22,7 +21,7 @@ struct ChatListView: View {
let v = NavigationView {
List {
ForEach(filteredChats()) { chat in
ChatListNavLink(chat: chat, showCallView: $showCallView)
ChatListNavLink(chat: chat)
.padding(.trailing, -16)
}
}
@@ -50,11 +49,11 @@ struct ChatListView: View {
NewChatButton()
}
}
.fullScreenCover(isPresented: $showCallView) {
ActiveCallView(showCallView: $showCallView)
.fullScreenCover(isPresented: $chatModel.showCallView) {
ActiveCallView()
}
.onChange(of: showCallView) { _ in
if (showCallView) { return }
.onChange(of: chatModel.showCallView) { _ in
if (chatModel.showCallView) { return }
if let call = chatModel.activeCall {
Task {
do {
@@ -69,7 +68,7 @@ struct ChatListView: View {
.onChange(of: chatModel.activeCallInvitation) { _ in
if let contactRef = chatModel.activeCallInvitation,
case let .direct(contact) = chatModel.getChat(contactRef.id)?.chatInfo,
let invitation = chatModel.callInvitations.removeValue(forKey: contactRef.id) {
let invitation = chatModel.callInvitations[contactRef.id] {
answerCallAlert(contact, invitation)
}
}
@@ -105,11 +104,8 @@ struct ChatListView: View {
Text(invitation.callTypeText) +
Text("\nIf you accept this call and you don't use relay, your IP address might be visible to your contact."),
primaryButton: .default(Text("Answer")) {
if chatModel.activeCallInvitation == nil {
DispatchQueue.main.async {
AlertManager.shared.showAlertMsg(title: "Call already ended!")
}
} else {
if let activeCallInvitation = chatModel.activeCallInvitation {
chatModel.callInvitations.removeValue(forKey: activeCallInvitation.id)
chatModel.activeCallInvitation = nil
chatModel.activeCall = Call(
contact: contact,
@@ -117,8 +113,12 @@ struct ChatListView: View {
localMedia: invitation.peerMedia,
sharedKey: invitation.sharedKey
)
showCallView = true
chatModel.showCallView = true
chatModel.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true)
} else {
DispatchQueue.main.async {
AlertManager.shared.showAlertMsg(title: "Call already ended!")
}
}
},
secondaryButton: .cancel()
+10 -10
View File
@@ -148,16 +148,16 @@ struct CIFileView_Previews: PreviewProvider {
file: nil
)
Group{
ChatItemView(chatItem: sentFile)
ChatItemView(chatItem: ChatItem.getFileMsgContentSample())
ChatItemView(chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation))
ChatItemView(chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted))
ChatItemView(chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer))
ChatItemView(chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled))
ChatItemView(chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation))
ChatItemView(chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation))
ChatItemView(chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation))
ChatItemView(chatItem: fileChatItemWtFile)
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentFile)
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample())
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: fileChatItemWtFile)
}
.previewLayout(.fixed(width: 360, height: 360))
}
@@ -12,6 +12,7 @@
3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; };
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; };
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; };
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
@@ -128,6 +129,7 @@
3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = "<group>"; };
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; };
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = "<group>"; };
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = "<group>"; };
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = "<group>"; };
5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; };
@@ -496,6 +498,7 @@
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */,
5C3A88D027DF57800060F1C2 /* FramedItemView.swift */,
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */,
5C029EA72837DBB3004A9677 /* CICallItemView.swift */,
);
path = ChatItem;
sourceTree = "<group>";
@@ -718,6 +721,7 @@
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
5C5E5D3D282447AB00B0488A /* CallTypes.swift in Sources */,
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */,
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,