|
|
|
@ -35,6 +35,7 @@ import com.utun.chat.data.Message |
|
|
|
import com.utun.chat.data.NativeLib |
|
|
|
import com.utun.chat.data.NativeLib |
|
|
|
import com.utun.chat.data.RadioAudioEngine |
|
|
|
import com.utun.chat.data.RadioAudioEngine |
|
|
|
import com.utun.chat.headless.HeadlessService |
|
|
|
import com.utun.chat.headless.HeadlessService |
|
|
|
|
|
|
|
import com.utun.chat.ui.theme.VibeChatTheme |
|
|
|
import com.utun.chat.ui.components.ActiveCallBanner |
|
|
|
import com.utun.chat.ui.components.ActiveCallBanner |
|
|
|
import com.utun.chat.ui.screens.* |
|
|
|
import com.utun.chat.ui.screens.* |
|
|
|
import com.utun.chat.viewmodel.ChatViewModel |
|
|
|
import com.utun.chat.viewmodel.ChatViewModel |
|
|
|
@ -58,10 +59,10 @@ class MainActivity : ComponentActivity() { |
|
|
|
showOverLockScreen() |
|
|
|
showOverLockScreen() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Edge-to-edge with dark status/navigation bar icons (light app theme) */ |
|
|
|
/* Светлые системные значки на тёмном фоне приложения. */ |
|
|
|
enableEdgeToEdge( |
|
|
|
enableEdgeToEdge( |
|
|
|
statusBarStyle = SystemBarStyle.light(android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT), |
|
|
|
statusBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT), |
|
|
|
navigationBarStyle = SystemBarStyle.light(android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT) |
|
|
|
navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT) |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
/* Start uTun headless service (must be from foreground Activity on API 34+). |
|
|
|
/* Start uTun headless service (must be from foreground Activity on API 34+). |
|
|
|
@ -71,288 +72,290 @@ class MainActivity : ComponentActivity() { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
setContent { |
|
|
|
setContent { |
|
|
|
val vm: ChatViewModel = viewModel(factory = ChatViewModel.Factory()) |
|
|
|
VibeChatTheme { |
|
|
|
|
|
|
|
val vm: ChatViewModel = viewModel(factory = ChatViewModel.Factory()) |
|
|
|
/* Pause GUI polling (health-check, recording/playback timers) when backgrounded, |
|
|
|
|
|
|
|
refresh statuses on resume. */ |
|
|
|
/* Pause GUI polling (health-check, recording/playback timers) when backgrounded, |
|
|
|
LifecycleStartEffect(Unit) { |
|
|
|
refresh statuses on resume. */ |
|
|
|
vm.setForeground(true) |
|
|
|
LifecycleStartEffect(Unit) { |
|
|
|
onStopOrDispose { vm.setForeground(false) } |
|
|
|
vm.setForeground(true) |
|
|
|
} |
|
|
|
onStopOrDispose { vm.setForeground(false) } |
|
|
|
|
|
|
|
|
|
|
|
var screen by remember { mutableStateOf(Screen.ChannelList) } |
|
|
|
|
|
|
|
var qrResult by remember { mutableStateOf<String?>(null) } |
|
|
|
|
|
|
|
var showJoinDialog by remember { mutableStateOf(false) } |
|
|
|
|
|
|
|
var joinAutoLink by remember { mutableStateOf<String?>(null) } |
|
|
|
|
|
|
|
var inviteChannelId by remember { mutableStateOf<String?>(null) } |
|
|
|
|
|
|
|
var videoMessage by remember { mutableStateOf<Message?>(null) } |
|
|
|
|
|
|
|
var photoMessage by remember { mutableStateOf<Message?>(null) } |
|
|
|
|
|
|
|
var hadCall by remember { mutableStateOf(false) } |
|
|
|
|
|
|
|
var screenBeforeCall by remember { mutableStateOf<Screen?>(null) } |
|
|
|
|
|
|
|
val snackbarHostState = remember { SnackbarHostState() } |
|
|
|
|
|
|
|
val scope = rememberCoroutineScope() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val callState by vm.callState.collectAsState() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Пока идёт звонок — держим экран включённым; после завершения снимаем |
|
|
|
|
|
|
|
и showWhenLocked (если он был выставлен для входящего поверх блокировки). */ |
|
|
|
|
|
|
|
LaunchedEffect(callState) { |
|
|
|
|
|
|
|
val st = callState |
|
|
|
|
|
|
|
val inCall = st != null && st.phase != ChatViewModel.CallPhase.NONE && |
|
|
|
|
|
|
|
st.phase != ChatViewModel.CallPhase.ENDED |
|
|
|
|
|
|
|
if (inCall) { |
|
|
|
|
|
|
|
hadCall = true |
|
|
|
|
|
|
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) |
|
|
|
|
|
|
|
if (hadCall) releaseLockScreen() |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Звонок управляет навигацией: OUTGOING/RINGING/ACTIVE → экран Call (запоминаем |
|
|
|
var screen by remember { mutableStateOf(Screen.ChannelList) } |
|
|
|
откуда пришли, чтобы свернуться обратно). ENDED не навигирует — CallScreen сам |
|
|
|
var qrResult by remember { mutableStateOf<String?>(null) } |
|
|
|
показывает «Завершён», если пользователь уже на нём. INCOMING → диалог поверх. */ |
|
|
|
var showJoinDialog by remember { mutableStateOf(false) } |
|
|
|
LaunchedEffect(callState?.phase) { |
|
|
|
var joinAutoLink by remember { mutableStateOf<String?>(null) } |
|
|
|
when (callState?.phase) { |
|
|
|
var inviteChannelId by remember { mutableStateOf<String?>(null) } |
|
|
|
ChatViewModel.CallPhase.OUTGOING, ChatViewModel.CallPhase.RINGING, |
|
|
|
var videoMessage by remember { mutableStateOf<Message?>(null) } |
|
|
|
ChatViewModel.CallPhase.ACTIVE -> { |
|
|
|
var photoMessage by remember { mutableStateOf<Message?>(null) } |
|
|
|
if (screen != Screen.Call) screenBeforeCall = screen |
|
|
|
var hadCall by remember { mutableStateOf(false) } |
|
|
|
screen = Screen.Call |
|
|
|
var screenBeforeCall by remember { mutableStateOf<Screen?>(null) } |
|
|
|
|
|
|
|
val snackbarHostState = remember { SnackbarHostState() } |
|
|
|
|
|
|
|
val scope = rememberCoroutineScope() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val callState by vm.callState.collectAsState() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Пока идёт звонок — держим экран включённым; после завершения снимаем |
|
|
|
|
|
|
|
и showWhenLocked (если он был выставлен для входящего поверх блокировки). */ |
|
|
|
|
|
|
|
LaunchedEffect(callState) { |
|
|
|
|
|
|
|
val st = callState |
|
|
|
|
|
|
|
val inCall = st != null && st.phase != ChatViewModel.CallPhase.NONE && |
|
|
|
|
|
|
|
st.phase != ChatViewModel.CallPhase.ENDED |
|
|
|
|
|
|
|
if (inCall) { |
|
|
|
|
|
|
|
hadCall = true |
|
|
|
|
|
|
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) |
|
|
|
|
|
|
|
if (hadCall) releaseLockScreen() |
|
|
|
} |
|
|
|
} |
|
|
|
null -> if (screen == Screen.Call) screen = screenBeforeCall ?: Screen.ChannelList |
|
|
|
|
|
|
|
else -> {} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Автооткрытие DM-беседы после нажатия «Message» в списке участников. */ |
|
|
|
/* Звонок управляет навигацией: OUTGOING/RINGING/ACTIVE → экран Call (запоминаем |
|
|
|
LaunchedEffect(Unit) { |
|
|
|
откуда пришли, чтобы свернуться обратно). ENDED не навигирует — CallScreen сам |
|
|
|
vm.dmOpen.collect { convId -> |
|
|
|
показывает «Завершён», если пользователь уже на нём. INCOMING → диалог поверх. */ |
|
|
|
if (convId != null) { |
|
|
|
LaunchedEffect(callState?.phase) { |
|
|
|
vm.consumeDmOpen() |
|
|
|
when (callState?.phase) { |
|
|
|
screen = Screen.Dm |
|
|
|
ChatViewModel.CallPhase.OUTGOING, ChatViewModel.CallPhase.RINGING, |
|
|
|
|
|
|
|
ChatViewModel.CallPhase.ACTIVE -> { |
|
|
|
|
|
|
|
if (screen != Screen.Call) screenBeforeCall = screen |
|
|
|
|
|
|
|
screen = Screen.Call |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
null -> if (screen == Screen.Call) screen = screenBeforeCall ?: Screen.ChannelList |
|
|
|
|
|
|
|
else -> {} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val showInvite: (Channel) -> Unit = { ch -> |
|
|
|
/* Автооткрытие DM-беседы после нажатия «Message» в списке участников. */ |
|
|
|
if (ch.id.toULongOrNull() == null) { |
|
|
|
LaunchedEffect(Unit) { |
|
|
|
LogManager.addLog("ERROR", "INVITE", "channel id is not numeric: ${ch.id}") |
|
|
|
vm.dmOpen.collect { convId -> |
|
|
|
scope.launch { snackbarHostState.showSnackbar("Invalid channel id") } |
|
|
|
if (convId != null) { |
|
|
|
} else { |
|
|
|
vm.consumeDmOpen() |
|
|
|
inviteChannelId = ch.id |
|
|
|
screen = Screen.Dm |
|
|
|
vm.requestInvite(ch.id) |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BackHandler(enabled = screen != Screen.ChannelList) { |
|
|
|
val showInvite: (Channel) -> Unit = { ch -> |
|
|
|
when (screen) { |
|
|
|
if (ch.id.toULongOrNull() == null) { |
|
|
|
Screen.MemberList -> screen = Screen.Chat |
|
|
|
LogManager.addLog("ERROR", "INVITE", "channel id is not numeric: ${ch.id}") |
|
|
|
Screen.VideoRecord -> screen = Screen.Chat |
|
|
|
scope.launch { snackbarHostState.showSnackbar("Invalid channel id") } |
|
|
|
Screen.PhotoRecord -> screen = Screen.Chat |
|
|
|
} else { |
|
|
|
Screen.Chat -> screen = Screen.ChannelList |
|
|
|
inviteChannelId = ch.id |
|
|
|
Screen.Dm -> { vm.closeDm(); screen = Screen.ChannelList } |
|
|
|
vm.requestInvite(ch.id) |
|
|
|
Screen.Call -> screen = screenBeforeCall ?: Screen.ChannelList |
|
|
|
} |
|
|
|
else -> screen = Screen.ChannelList |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BackHandler(enabled = videoMessage != null) { |
|
|
|
BackHandler(enabled = screen != Screen.ChannelList) { |
|
|
|
videoMessage = null |
|
|
|
when (screen) { |
|
|
|
} |
|
|
|
Screen.MemberList -> screen = Screen.Chat |
|
|
|
|
|
|
|
Screen.VideoRecord -> screen = Screen.Chat |
|
|
|
|
|
|
|
Screen.PhotoRecord -> screen = Screen.Chat |
|
|
|
|
|
|
|
Screen.Chat -> screen = Screen.ChannelList |
|
|
|
|
|
|
|
Screen.Dm -> { vm.closeDm(); screen = Screen.ChannelList } |
|
|
|
|
|
|
|
Screen.Call -> screen = screenBeforeCall ?: Screen.ChannelList |
|
|
|
|
|
|
|
else -> screen = Screen.ChannelList |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
BackHandler(enabled = photoMessage != null) { |
|
|
|
BackHandler(enabled = videoMessage != null) { |
|
|
|
photoMessage = null |
|
|
|
videoMessage = null |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
val connected = NativeLib.isReady() |
|
|
|
BackHandler(enabled = photoMessage != null) { |
|
|
|
|
|
|
|
photoMessage = null |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Subscribe native logs only when LogScreen is open */ |
|
|
|
val connected = NativeLib.isReady() |
|
|
|
DisposableEffect(screen) { |
|
|
|
|
|
|
|
if (screen == Screen.Logs) NativeLib.subscribeNativeLogs(true) |
|
|
|
|
|
|
|
onDispose { NativeLib.subscribeNativeLogs(false) } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Column(modifier = Modifier.fillMaxSize()) { |
|
|
|
/* Subscribe native logs only when LogScreen is open */ |
|
|
|
val callPhase = callState?.phase |
|
|
|
DisposableEffect(screen) { |
|
|
|
val showCallBanner = screen != Screen.Call && |
|
|
|
if (screen == Screen.Logs) NativeLib.subscribeNativeLogs(true) |
|
|
|
(callPhase == ChatViewModel.CallPhase.OUTGOING || |
|
|
|
onDispose { NativeLib.subscribeNativeLogs(false) } |
|
|
|
callPhase == ChatViewModel.CallPhase.RINGING || |
|
|
|
|
|
|
|
callPhase == ChatViewModel.CallPhase.ACTIVE) |
|
|
|
|
|
|
|
if (showCallBanner) { |
|
|
|
|
|
|
|
ActiveCallBanner( |
|
|
|
|
|
|
|
viewModel = vm, |
|
|
|
|
|
|
|
onOpen = { screen = Screen.Call }, |
|
|
|
|
|
|
|
onHangup = { vm.hangupCall() } |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
Box( |
|
|
|
|
|
|
|
modifier = Modifier.weight(1f) |
|
|
|
Column(modifier = Modifier.fillMaxSize()) { |
|
|
|
.then(if (showCallBanner) Modifier.consumeWindowInsets(WindowInsets.statusBars) else Modifier) |
|
|
|
val callPhase = callState?.phase |
|
|
|
) { |
|
|
|
val showCallBanner = screen != Screen.Call && |
|
|
|
when (screen) { |
|
|
|
(callPhase == ChatViewModel.CallPhase.OUTGOING || |
|
|
|
Screen.ChannelList -> { |
|
|
|
callPhase == ChatViewModel.CallPhase.RINGING || |
|
|
|
val channels by vm.channels.collectAsState() |
|
|
|
callPhase == ChatViewModel.CallPhase.ACTIVE) |
|
|
|
val dmConversations by vm.dmConversations.collectAsState() |
|
|
|
if (showCallBanner) { |
|
|
|
val stuck by vm.isStuck.collectAsState() |
|
|
|
ActiveCallBanner( |
|
|
|
ChannelListScreen( |
|
|
|
viewModel = vm, |
|
|
|
channels = channels, |
|
|
|
onOpen = { screen = Screen.Call }, |
|
|
|
dmConversations = dmConversations, |
|
|
|
onHangup = { vm.hangupCall() } |
|
|
|
connected = connected, |
|
|
|
) |
|
|
|
isStuck = stuck, |
|
|
|
} |
|
|
|
onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat }, |
|
|
|
Box( |
|
|
|
onSelectDm = { dm -> vm.openDm(dm.convId, dm.peerNodeId, dm.peerName); screen = Screen.Dm }, |
|
|
|
modifier = Modifier.weight(1f) |
|
|
|
onCreateChannel = { name -> vm.createChannel(name, "channel_1") }, |
|
|
|
.then(if (showCallBanner) Modifier.consumeWindowInsets(WindowInsets.statusBars) else Modifier) |
|
|
|
onJoinChannel = { showJoinDialog = true; joinAutoLink = null }, |
|
|
|
) { |
|
|
|
onInviteClick = showInvite, |
|
|
|
when (screen) { |
|
|
|
onDeleteChannel = { vm.deleteChannel(it.id) }, |
|
|
|
Screen.ChannelList -> { |
|
|
|
onSettings = { screen = Screen.Settings }, |
|
|
|
val channels by vm.channels.collectAsState() |
|
|
|
onLogs = { screen = Screen.Logs }, |
|
|
|
val dmConversations by vm.dmConversations.collectAsState() |
|
|
|
onRefresh = { vm.requestRefresh() } |
|
|
|
val stuck by vm.isStuck.collectAsState() |
|
|
|
) |
|
|
|
ChannelListScreen( |
|
|
|
} |
|
|
|
channels = channels, |
|
|
|
Screen.Chat -> { |
|
|
|
dmConversations = dmConversations, |
|
|
|
val messages by vm.messages.collectAsState() |
|
|
|
|
|
|
|
val channel = vm.currentChannel.collectAsState().value |
|
|
|
|
|
|
|
if (channel != null) { |
|
|
|
|
|
|
|
ChatScreen( |
|
|
|
|
|
|
|
channel = channel, |
|
|
|
|
|
|
|
messages = messages, |
|
|
|
|
|
|
|
connected = connected, |
|
|
|
connected = connected, |
|
|
|
viewModel = vm, |
|
|
|
isStuck = stuck, |
|
|
|
onSendMessage = { vm.sendMessage(it) }, |
|
|
|
onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat }, |
|
|
|
onMembersClick = { screen = Screen.MemberList }, |
|
|
|
onSelectDm = { dm -> vm.openDm(dm.convId, dm.peerNodeId, dm.peerName); screen = Screen.Dm }, |
|
|
|
onInvite = { showInvite(channel) }, |
|
|
|
onCreateChannel = { name -> vm.createChannel(name, "channel_1") }, |
|
|
|
onBack = { screen = Screen.ChannelList }, |
|
|
|
onJoinChannel = { showJoinDialog = true; joinAutoLink = null }, |
|
|
|
onPlayVideo = { msg -> videoMessage = msg }, |
|
|
|
onInviteClick = showInvite, |
|
|
|
onViewPhoto = { msg -> photoMessage = msg }, |
|
|
|
onDeleteChannel = { vm.deleteChannel(it.id) }, |
|
|
|
onRecordVideo = { screen = Screen.VideoRecord }, |
|
|
|
onSettings = { screen = Screen.Settings }, |
|
|
|
onCapturePhoto = { screen = Screen.PhotoRecord } |
|
|
|
onLogs = { screen = Screen.Logs }, |
|
|
|
|
|
|
|
onRefresh = { vm.requestRefresh() } |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Screen.Chat -> { |
|
|
|
Screen.MemberList -> { |
|
|
|
val messages by vm.messages.collectAsState() |
|
|
|
val members by vm.members.collectAsState() |
|
|
|
val channel = vm.currentChannel.collectAsState().value |
|
|
|
val selectedMember by vm.selectedMember.collectAsState() |
|
|
|
if (channel != null) { |
|
|
|
val channel = vm.currentChannel.collectAsState().value |
|
|
|
ChatScreen( |
|
|
|
if (channel != null) { |
|
|
|
channel = channel, |
|
|
|
MemberListScreen( |
|
|
|
messages = messages, |
|
|
|
channel = channel, |
|
|
|
connected = connected, |
|
|
|
members = members, |
|
|
|
viewModel = vm, |
|
|
|
selectedMember = selectedMember, |
|
|
|
onSendMessage = { vm.sendMessage(it) }, |
|
|
|
viewModel = vm, |
|
|
|
onMembersClick = { screen = Screen.MemberList }, |
|
|
|
onBack = { screen = Screen.Chat } |
|
|
|
onInvite = { showInvite(channel) }, |
|
|
|
) |
|
|
|
onBack = { screen = Screen.ChannelList }, |
|
|
|
|
|
|
|
onPlayVideo = { msg -> videoMessage = msg }, |
|
|
|
|
|
|
|
onViewPhoto = { msg -> photoMessage = msg }, |
|
|
|
|
|
|
|
onRecordVideo = { screen = Screen.VideoRecord }, |
|
|
|
|
|
|
|
onCapturePhoto = { screen = Screen.PhotoRecord } |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Screen.MemberList -> { |
|
|
|
Screen.Dm -> { |
|
|
|
val members by vm.members.collectAsState() |
|
|
|
val dmMessages by vm.dmMessages.collectAsState() |
|
|
|
val selectedMember by vm.selectedMember.collectAsState() |
|
|
|
val dm = vm.currentDm.collectAsState().value |
|
|
|
val channel = vm.currentChannel.collectAsState().value |
|
|
|
if (dm != null) { |
|
|
|
if (channel != null) { |
|
|
|
DmChatScreen( |
|
|
|
MemberListScreen( |
|
|
|
conv = dm, |
|
|
|
channel = channel, |
|
|
|
messages = dmMessages, |
|
|
|
members = members, |
|
|
|
onSendMessage = { vm.sendDmMessage(it) }, |
|
|
|
selectedMember = selectedMember, |
|
|
|
onBack = { vm.closeDm(); screen = Screen.ChannelList } |
|
|
|
viewModel = vm, |
|
|
|
) |
|
|
|
onBack = { screen = Screen.Chat } |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Screen.Dm -> { |
|
|
|
Screen.Settings -> SettingsScreen(vm = vm, onBack = { screen = Screen.ChannelList }, onExit = { finish() }) |
|
|
|
val dmMessages by vm.dmMessages.collectAsState() |
|
|
|
Screen.Logs -> LogScreen(onBack = { screen = Screen.ChannelList }) |
|
|
|
val dm = vm.currentDm.collectAsState().value |
|
|
|
Screen.Call -> CallScreen( |
|
|
|
if (dm != null) { |
|
|
|
viewModel = vm, |
|
|
|
DmChatScreen( |
|
|
|
onClose = { vm.dismissCall(); screen = screenBeforeCall ?: Screen.ChannelList }, |
|
|
|
conv = dm, |
|
|
|
onMinimize = { screen = screenBeforeCall ?: Screen.ChannelList } |
|
|
|
messages = dmMessages, |
|
|
|
) |
|
|
|
onSendMessage = { vm.sendDmMessage(it) }, |
|
|
|
Screen.QrScan -> { |
|
|
|
onBack = { vm.closeDm(); screen = Screen.ChannelList } |
|
|
|
QrScanScreen( |
|
|
|
) |
|
|
|
onResult = { link -> |
|
|
|
} |
|
|
|
qrResult = link |
|
|
|
} |
|
|
|
screen = Screen.ChannelList |
|
|
|
Screen.Settings -> SettingsScreen(vm = vm, onBack = { screen = Screen.ChannelList }, onExit = { finish() }) |
|
|
|
}, |
|
|
|
Screen.Logs -> LogScreen(onBack = { screen = Screen.ChannelList }) |
|
|
|
onBack = { screen = Screen.ChannelList } |
|
|
|
Screen.Call -> CallScreen( |
|
|
|
|
|
|
|
viewModel = vm, |
|
|
|
|
|
|
|
onClose = { vm.dismissCall(); screen = screenBeforeCall ?: Screen.ChannelList }, |
|
|
|
|
|
|
|
onMinimize = { screen = screenBeforeCall ?: Screen.ChannelList } |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
Screen.QrScan -> { |
|
|
|
Screen.VideoRecord -> { |
|
|
|
QrScanScreen( |
|
|
|
val channel = vm.currentChannel.collectAsState().value |
|
|
|
onResult = { link -> |
|
|
|
if (channel != null) { |
|
|
|
qrResult = link |
|
|
|
VideoRecordScreen( |
|
|
|
screen = Screen.ChannelList |
|
|
|
onSend = { path -> |
|
|
|
|
|
|
|
vm.sendVideoMessage(path) |
|
|
|
|
|
|
|
screen = Screen.Chat |
|
|
|
|
|
|
|
}, |
|
|
|
}, |
|
|
|
onClose = { screen = Screen.Chat } |
|
|
|
onBack = { screen = Screen.ChannelList } |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Screen.VideoRecord -> { |
|
|
|
|
|
|
|
val channel = vm.currentChannel.collectAsState().value |
|
|
|
|
|
|
|
if (channel != null) { |
|
|
|
|
|
|
|
VideoRecordScreen( |
|
|
|
|
|
|
|
onSend = { path -> |
|
|
|
|
|
|
|
vm.sendVideoMessage(path) |
|
|
|
|
|
|
|
screen = Screen.Chat |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
onClose = { screen = Screen.Chat } |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Screen.PhotoRecord -> { |
|
|
|
|
|
|
|
val channel = vm.currentChannel.collectAsState().value |
|
|
|
|
|
|
|
if (channel != null) { |
|
|
|
|
|
|
|
PhotoRecordScreen( |
|
|
|
|
|
|
|
onSend = { path, width, height -> |
|
|
|
|
|
|
|
vm.sendPhotoMessage(path, width, height) |
|
|
|
|
|
|
|
screen = Screen.Chat |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
onClose = { screen = Screen.Chat } |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Screen.PhotoRecord -> { |
|
|
|
SnackbarHost(snackbarHostState, Modifier.align(Alignment.BottomCenter).navigationBarsPadding()) |
|
|
|
val channel = vm.currentChannel.collectAsState().value |
|
|
|
|
|
|
|
if (channel != null) { |
|
|
|
videoMessage?.let { msg -> |
|
|
|
PhotoRecordScreen( |
|
|
|
if (msg.filePath.isNotEmpty()) { |
|
|
|
onSend = { path, width, height -> |
|
|
|
VideoPlayerScreen(filePath = msg.filePath, onClose = { videoMessage = null }) |
|
|
|
vm.sendPhotoMessage(path, width, height) |
|
|
|
|
|
|
|
screen = Screen.Chat |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
onClose = { screen = Screen.Chat } |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
SnackbarHost(snackbarHostState, Modifier.align(Alignment.BottomCenter).navigationBarsPadding()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
videoMessage?.let { msg -> |
|
|
|
photoMessage?.let { msg -> |
|
|
|
if (msg.filePath.isNotEmpty()) { |
|
|
|
if (msg.filePath.isNotEmpty()) { |
|
|
|
VideoPlayerScreen(filePath = msg.filePath, onClose = { videoMessage = null }) |
|
|
|
PhotoViewerScreen(filePath = msg.filePath, onClose = { photoMessage = null }) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
photoMessage?.let { msg -> |
|
|
|
if (callState?.phase == ChatViewModel.CallPhase.INCOMING) { |
|
|
|
if (msg.filePath.isNotEmpty()) { |
|
|
|
IncomingCallDialog(viewModel = vm) |
|
|
|
PhotoViewerScreen(filePath = msg.filePath, onClose = { photoMessage = null }) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (callState?.phase == ChatViewModel.CallPhase.INCOMING) { |
|
|
|
/* Show join dialog after QR scan returns */ |
|
|
|
IncomingCallDialog(viewModel = vm) |
|
|
|
LaunchedEffect(qrResult) { |
|
|
|
|
|
|
|
qrResult?.let { link -> |
|
|
|
|
|
|
|
joinAutoLink = link |
|
|
|
|
|
|
|
showJoinDialog = true |
|
|
|
|
|
|
|
qrResult = null |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Show join dialog after QR scan returns */ |
|
|
|
if (showJoinDialog) { |
|
|
|
LaunchedEffect(qrResult) { |
|
|
|
JoinChannelDialog( |
|
|
|
qrResult?.let { link -> |
|
|
|
autoLink = joinAutoLink, |
|
|
|
joinAutoLink = link |
|
|
|
onScanQr = { |
|
|
|
showJoinDialog = true |
|
|
|
screen = Screen.QrScan |
|
|
|
qrResult = null |
|
|
|
showJoinDialog = false |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
onConnect = { data: InviteData -> |
|
|
|
|
|
|
|
val addrs = data.serializeAddrs() |
|
|
|
|
|
|
|
NativeLib.joinChannel(data.channelId, data.nodeId, data.joinKey, data.pubkey, addrs, data.addrs.size) |
|
|
|
|
|
|
|
showJoinDialog = false |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
onDismiss = { showJoinDialog = false } |
|
|
|
|
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (showJoinDialog) { |
|
|
|
|
|
|
|
JoinChannelDialog( |
|
|
|
|
|
|
|
autoLink = joinAutoLink, |
|
|
|
|
|
|
|
onScanQr = { |
|
|
|
|
|
|
|
screen = Screen.QrScan |
|
|
|
|
|
|
|
showJoinDialog = false |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
onConnect = { data: InviteData -> |
|
|
|
|
|
|
|
val addrs = data.serializeAddrs() |
|
|
|
|
|
|
|
NativeLib.joinChannel(data.channelId, data.nodeId, data.joinKey, data.pubkey, addrs, data.addrs.size) |
|
|
|
|
|
|
|
showJoinDialog = false |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
onDismiss = { showJoinDialog = false } |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
inviteChannelId?.let { chId -> |
|
|
|
inviteChannelId?.let { chId -> |
|
|
|
val candidates by vm.inviteCandidates.collectAsState() |
|
|
|
val candidates by vm.inviteCandidates.collectAsState() |
|
|
|
val link by vm.inviteLink.collectAsState() |
|
|
|
val link by vm.inviteLink.collectAsState() |
|
|
|
val pending by vm.inviteLinkPending.collectAsState() |
|
|
|
val pending by vm.inviteLinkPending.collectAsState() |
|
|
|
InviteLinkDialog( |
|
|
|
InviteLinkDialog( |
|
|
|
candidates = candidates, |
|
|
|
candidates = candidates, |
|
|
|
link = link, |
|
|
|
link = link, |
|
|
|
pending = pending, |
|
|
|
pending = pending, |
|
|
|
onSelectNode = { nodeId -> vm.requestInviteLink(chId, nodeId) }, |
|
|
|
onSelectNode = { nodeId -> vm.requestInviteLink(chId, nodeId) }, |
|
|
|
onDismiss = { inviteChannelId = null; vm.clearInvite() } |
|
|
|
onDismiss = { inviteChannelId = null; vm.clearInvite() } |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|