Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.flipcash.app.currency.RegionSelectionScreen
import com.flipcash.app.deposit.DepositFlowScreen
import com.flipcash.app.invite.InviteContactScreen
import com.flipcash.app.messenger.ChatFlowScreen
import com.flipcash.app.messenger.ChatPhotoViewerScreen
import com.flipcash.app.messenger.E2eeLearnMoreScreen
import com.flipcash.app.messenger.MuteChatScreen
import com.flipcash.app.messenger.ProfileScreen
Expand Down Expand Up @@ -131,6 +132,7 @@ fun appEntryProvider(
ReportFlowScreen(route = key, resultStateRegistry = resultStateRegistry)
}
annotatedEntry<AppRoute.Messaging.MuteChat> { key -> MuteChatScreen(key.chatId, key.chatType) }
annotatedEntry<AppRoute.Messaging.PhotoViewer> { key -> ChatPhotoViewerScreen(key.chatId, key.messageId) }
annotatedEntry<AppRoute.Messaging.E2eeDmInfo> { E2eeLearnMoreScreen(forGroup = false) }
annotatedEntry<AppRoute.Messaging.E2eeGroupInfo> { E2eeLearnMoreScreen(forGroup = true) }

Expand Down
2 changes: 2 additions & 0 deletions apps/flipcash/app/src/main/res/xml/file_paths.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
<cache-path name="share_previews" path="share_previews/" />
<!-- Exported tip codes, PNG + SVG (cacheDir/share_exports/). -->
<cache-path name="share_exports" path="share_exports/" />
<!-- Chat photos handed to the share sheet (cacheDir/chat_media/). -->
<cache-path name="chat_media" path="chat_media/" />
</paths>
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,15 @@ sealed interface AppRoute : NavKey, Parcelable {
com.getcode.navigation.Sheet,
com.getcode.navigation.WrapContentSheet

/**
* The full-screen viewer for the photo in message [messageId] of [chatId]. Carries the
* message's address rather than the photo, which is not something a back stack can hold; the
* viewer reads the message back to learn how to fetch and open it.
*/
@Serializable
@Parcelize
data class PhotoViewer(val chatId: ChatId, val messageId: Long) : Messaging

/** The group counterpart of [E2eeDmInfo], opened from Group Info's footer. */
@Serializable
@Parcelize
Expand Down
25 changes: 25 additions & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,8 @@
<string name="label_chat_preview_receivedCash">You received %1$s</string>
<string name="label_chat_preview_tippedCash">You tipped %1$s</string>
<string name="label_chat_preview_receivedTip">Someone tipped you %1$s</string>
<string name="label_chat_media_photo">Photo</string>
<string name="label_chat_media_unavailable">This photo can’t be displayed</string>
<string name="label_chat_preview_sentMessage">You: %1$s</string>
<string name="label_chat_preview_senderMessage">%1$s: %2$s</string>
<string name="label_chat_preview_sentCash_bySender">%1$s sent %2$s</string>
Expand Down Expand Up @@ -1367,4 +1369,27 @@
<string name="label_shareOfMaxSupply">Share of max supply</string>
<string name="label_yourCurrentAppreciation">Your current appreciation</string>
<string name="label_marketCapChartAccessibility">Price curve for %1$s with %2$s purchased</string>
<!-- Chat photo messages -->
<string name="description_chatMediaClose">Close</string>
<string name="description_chatMediaShare">Share</string>
<string name="title_chatMediaShare">Share photo</string>
<string name="description_chatPhotoRemove">Remove photo</string>
<string name="description_chatPhotoRetry">Retry photo upload</string>
<string name="description_chatPhotoFailed">Photo can\'t be sent</string>
<string name="description_chatPhotoUploadProgress">Photo upload progress</string>
<string name="action_chatAttachCash">Cash</string>
<string name="action_chatAttachCamera">Camera</string>
<string name="action_chatAttachPhotos">Photos</string>
<string name="description_chatAttachMenu">Attach</string>
<string name="description_chatAttachBack">Back to attach menu</string>
<string name="action_chatTakePhoto">Take photo</string>
<string name="action_chatMoreCameraControls">More camera controls</string>
<string name="action_chatHideCameraControls">Hide camera controls</string>
<string name="action_chatFlashOn">Turn flash on</string>
<string name="action_chatFlashOff">Turn flash off</string>
<string name="action_chatUseFrontCamera">Use front camera</string>
<string name="action_chatUseBackCamera">Use back camera</string>
<string name="subtitle_chatCameraPermissionDenied">Turn on Camera in Settings to send photos</string>
<string name="action_chatOpenSettings">Open Settings</string>
<string name="subtitle_chatCameraUnavailable">Camera unavailable</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.flipcash.app.messenger

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flipcash.app.messenger.internal.ChatPhotoViewerViewModel
import com.flipcash.services.models.chat.ChatId
import com.flipcash.shared.chat.ui.media.ChatMediaViewer
import com.flipcash.shared.chat.ui.media.rememberSharePhoto
import com.getcode.navigation.core.LocalCodeNavigator

/** The full-screen photo, behind `AppRoute.Messaging.PhotoViewer`. Black, dismissed by back or a pull. */
@Composable
fun ChatPhotoViewerScreen(chatId: ChatId, messageId: Long) {
val viewModel = hiltViewModel<ChatPhotoViewerViewModel>()
LaunchedEffect(chatId, messageId) { viewModel.load(chatId, messageId) }
val photo by viewModel.photo.collectAsStateWithLifecycle()
val navigator = LocalCodeNavigator.current
var fullImageLoaded by remember { mutableStateOf(false) }
val share = rememberSharePhoto(photo)

Box(modifier = Modifier.fillMaxSize().background(Color.Black)) {
ChatMediaViewer(
model = photo,
fullImageLoaded = fullImageLoaded,
onFullImageLoaded = { fullImageLoaded = true },
onShare = share,
onDismiss = { navigator.pop() },
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.flipcash.app.messenger.internal
import com.flipcash.shared.chat.ChatDraftReply
import com.flipcash.shared.chat.ChatDraftSnippet
import com.flipcash.shared.chat.models.ChatQuote
import com.flipcash.shared.chat.media.ChatMediaText
import com.flipcash.shared.chat.models.ChatQuoteSnippet
import com.getcode.opencode.model.financial.CurrencyCode
import com.getcode.opencode.model.financial.Fiat
Expand All @@ -22,6 +23,9 @@ internal fun ChatQuote.toDraftReply(): ChatDraftReply = ChatDraftReply(
senderIdHex = senderIdHex,
snippet = when (val snippet = snippet) {
is ChatQuoteSnippet.Text -> ChatDraftSnippet.Text(snippet.body)
// A stored draft carries words only; the thumbnail is not persisted with it.
is ChatQuoteSnippet.Photo ->
ChatDraftSnippet.Text(ChatMediaText.preview(snippet.caption, "Photo"))
is ChatQuoteSnippet.Cash -> ChatDraftSnippet.Cash(
quarks = snippet.amount.quarks,
currencyCode = snippet.amount.currencyCode.name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.flipcash.app.messenger.internal

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.flipcash.services.models.chat.ChatId
import com.flipcash.shared.chat.ChatCoordinator
import com.flipcash.shared.chat.media.ChatPhoto
import com.flipcash.shared.chat.ui.media.photoBody
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

/**
* Reads the photo behind a viewer route. The route carries only the message's address, so this looks
* the message up and builds the Coil model the transcript's bubble loads from, which is what lets the
* viewer open on what the bubble already cached.
*/
@HiltViewModel
internal class ChatPhotoViewerViewModel @Inject constructor(
private val chatCoordinator: ChatCoordinator,
) : ViewModel() {

private val _photo = MutableStateFlow<ChatPhoto?>(null)
val photo: StateFlow<ChatPhoto?> = _photo

private var loaded: Pair<ChatId, Long>? = null

fun load(chatId: ChatId, messageId: Long) {
if (loaded == chatId to messageId) return
loaded = chatId to messageId
viewModelScope.launch {
val message = chatCoordinator.getMessage(chatId, messageId) ?: return@launch
val rendition = message.content.firstNotNullOfOrNull { it.photoBody() }
?.items?.firstOrNull()
?.let { item -> item.rendition(com.flipcash.services.models.chat.MediaItemRendition.Role.ORIGINAL) ?: item.renditions.firstOrNull() }
?: return@launch
_photo.value = ChatPhoto(
chatId = chatId,
rendition = rendition,
senderId = message.senderId,
sealed = message.encryption != null,
redacted = message.redacted,
)
}
}
}
Loading
Loading