Skip to content

Commit 7489add

Browse files
committed
Merge remote-tracking branch 'origin/code/cash' into feat/group-requests
* commit '5f2549710405c4215ac47fc972e0c3d22f4e8dfb': feat(history): show boosted reactions in transaction history (#1858) feat(chat): in-app alert for reactions and boosts on your messages (#1857) feat(chat): tap-to-show photo viewer chrome with react, reply, save and forward (#1854) # Conflicts: # apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt
2 parents bce5e16 + 5f25497 commit 7489add

54 files changed

Lines changed: 3099 additions & 289 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/flipcash/app/build.gradle.kts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ dependencies {
235235
implementation(project(":apps:flipcash:shared:notifications"))
236236
implementation(project(":apps:flipcash:shared:chat"))
237237
implementation(project(":apps:flipcash:shared:chat-ui"))
238+
debugImplementation(project(":apps:flipcash:shared:persistence:sources"))
238239
implementation(project(":apps:flipcash:shared:onramp:coinbase"))
239240
implementation(project(":apps:flipcash:shared:onramp:deeplinks"))
240241
implementation(libs.phantom.connect) {

‎apps/flipcash/app/src/debug/AndroidManifest.xml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
<uses-permission android:name="com.getcode.dev.READ_ACCOUNT" />
44
<application>
5-
5+
<receiver
6+
android:name="com.flipcash.app.debug.ReactionAlertDebugReceiver"
7+
android:exported="true">
8+
<intent-filter>
9+
<action android:name="com.flipcash.debug.REACTION_ALERT" />
10+
</intent-filter>
11+
</receiver>
612
</application>
713

814
</manifest>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package com.flipcash.app.debug
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import com.flipcash.app.persistence.sources.ChatMemberDataSource
7+
import com.flipcash.services.models.chat.ChatId
8+
import com.flipcash.services.models.chat.ReactionUpdate
9+
import com.flipcash.shared.chat.alerts.ReactionActorProfiles
10+
import com.flipcash.shared.chat.alerts.ReactionAlertController
11+
import com.flipcash.shared.chat.alerts.ReactionAlertEvent
12+
import dagger.hilt.EntryPoint
13+
import dagger.hilt.InstallIn
14+
import dagger.hilt.android.EntryPointAccessors
15+
import dagger.hilt.components.SingletonComponent
16+
import kotlinx.coroutines.CoroutineScope
17+
import kotlinx.coroutines.Dispatchers
18+
import kotlinx.coroutines.SupervisorJob
19+
import kotlinx.coroutines.delay
20+
import kotlinx.coroutines.flow.first
21+
import kotlinx.coroutines.launch
22+
23+
/**
24+
* Debug builds only: fires canned in-app reaction alerts so the banner can be driven from adb.
25+
*
26+
* adb shell am broadcast -a com.flipcash.debug.REACTION_ALERT --es kind reaction
27+
* adb shell am broadcast -a com.flipcash.debug.REACTION_ALERT --es kind boost
28+
* adb shell am broadcast -a com.flipcash.debug.REACTION_ALERT --es kind stack
29+
*
30+
* The reactor is the first known chat member whose name is `--es name` (default "XCTests"), and the
31+
* photo comes through the same lookup the real stream uses. `stack` sends four reactors one after
32+
* another so the card grows from 1 to 4 people.
33+
*/
34+
class ReactionAlertDebugReceiver : BroadcastReceiver() {
35+
36+
@EntryPoint
37+
@InstallIn(SingletonComponent::class)
38+
interface Deps {
39+
fun controller(): ReactionAlertController
40+
fun members(): ChatMemberDataSource
41+
fun profiles(): ReactionActorProfiles
42+
}
43+
44+
override fun onReceive(context: Context, intent: Intent) {
45+
val deps = EntryPointAccessors.fromApplication(context.applicationContext, Deps::class.java)
46+
val controller = deps.controller()
47+
val members = deps.members()
48+
val profiles = deps.profiles()
49+
val kind = intent.getStringExtra("kind") ?: "reaction"
50+
val name = intent.getStringExtra("name") ?: "XCTests"
51+
val pending = goAsync()
52+
scope.launch {
53+
try {
54+
fire(kind, name, controller, members, profiles)
55+
} finally {
56+
pending.finish()
57+
}
58+
}
59+
}
60+
61+
private suspend fun fire(
62+
kind: String,
63+
name: String,
64+
controller: ReactionAlertController,
65+
members: ChatMemberDataSource,
66+
profiles: ReactionActorProfiles,
67+
) {
68+
val all = members.observeAll().first()
69+
val (chatHex, member) = all.entries.firstNotNullOfOrNull { (hex, list) ->
70+
list.firstOrNull { it.userProfile.displayName == name }?.let { hex to it }
71+
} ?: all.entries.firstNotNullOf { (hex, list) -> list.firstOrNull()?.let { hex to it } }
72+
val chat = ChatId(chatHex.chunked(2).map { it.toInt(16).toByte() }.toByteArray())
73+
val resolved = profiles.resolve(member.userId)
74+
val actorName = resolved?.displayName?.takeIf { it.isNotBlank() } ?: member.userProfile.displayName
75+
val photo = member.userProfile.profilePicture ?: resolved?.profilePicture
76+
77+
fun event(who: List<Byte>, whoName: String, whoPhoto: Boolean, emoji: String, action: ReactionUpdate.Action, cents: Long? = null) =
78+
ReactionAlertEvent(
79+
chatId = chat,
80+
messageId = 1,
81+
emoji = emoji,
82+
actor = who,
83+
action = action,
84+
boostDeltaCents = cents,
85+
actorName = whoName,
86+
chatName = "Moony",
87+
quote = "Me! I'll send my \$40 tonight",
88+
actorPhoto = if (whoPhoto) photo else null,
89+
)
90+
91+
when (kind) {
92+
"boost" -> controller.offer(event(member.userId, actorName, true, "🔥", ReactionUpdate.Action.BOOSTED, 500))
93+
"stack" -> {
94+
val crowd = listOf("Chloe" to "❤️", "Sam" to "😂", "Jo" to "👏")
95+
controller.offer(event(member.userId, actorName, true, "🔥", ReactionUpdate.Action.ADDED))
96+
crowd.forEachIndexed { i, (n, e) ->
97+
delay(900)
98+
controller.offer(event(listOf(i.toByte(), 9), n, false, e, ReactionUpdate.Action.ADDED))
99+
}
100+
}
101+
else -> controller.offer(event(member.userId, actorName, true, "🔥", ReactionUpdate.Action.ADDED))
102+
}
103+
}
104+
105+
private companion object {
106+
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
107+
}
108+
}

‎apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ internal fun App(
201201
TransactionNotificationPromptHost(viewModel.notificationPrompt)
202202
CoinbaseOnRampHandler {
203203
AppContent(
204+
reactionAlerts = viewModel.reactionAlerts,
204205
codeNavigator = codeNavigator,
205206
resultStateRegistry = resultStateRegistry,
206207
barManager = barManager,

‎apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/HomeViewModel.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ internal class HomeViewModel @Inject constructor(
3333
featureFlagController: FeatureFlagController,
3434
userFlags: UserFlagsCoordinator,
3535
val notificationPrompt: com.flipcash.app.tokens.TransactionNotificationPrompt,
36+
val reactionAlerts: com.flipcash.shared.chat.alerts.ReactionAlertController,
3637
) : ViewModel() {
3738

3839
/**

‎apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ import androidx.navigation3.scene.SinglePaneSceneStrategy
4343
import com.flipcash.app.cardexpand.CardExpansionController
4444
import com.flipcash.app.cardexpand.LocalCardExpansion
4545
import com.flipcash.app.core.AppRoute
46+
import com.flipcash.app.core.chat.ChatIdentifier
47+
import com.flipcash.shared.chat.alerts.LocalReactionAlerts
48+
import com.flipcash.shared.chat.alerts.ReactionAlertAction
49+
import com.flipcash.shared.chat.alerts.ReactionAlertController
50+
import com.flipcash.shared.chat.ui.ReactionAlertHost
51+
import kotlinx.coroutines.flow.filterIsInstance
4652
import com.flipcash.app.core.navigation.DeeplinkAction
4753
import com.flipcash.app.core.navigation.LocalTabBarVisibility
4854
import com.flipcash.app.core.navigation.NavBarButton
@@ -108,6 +114,7 @@ internal fun AppContent(
108114
resultStateRegistry: NavResultStateRegistry,
109115
barManager: BarManager,
110116
cardExpansion: CardExpansionController,
117+
reactionAlerts: ReactionAlertController,
111118
deepLink: () -> DeepLink?,
112119
onPendingAction: (DeeplinkAction) -> Unit = {},
113120
canSwitchAccounts: Boolean = false,
@@ -141,6 +148,21 @@ internal fun AppContent(
141148
}
142149
UpdateAvailableToast(navigator = codeNavigator, barVisibility = barVisibility, toasts = toasts)
143150

151+
// A reaction pill tapped outside its chat opens the chat at the message. One tapped inside the
152+
// open chat is handled by the chat screen, which scrolls and pulses instead.
153+
LaunchedEffect(reactionAlerts, codeNavigator) {
154+
reactionAlerts.actions
155+
.filterIsInstance<ReactionAlertAction.OpenChat>()
156+
.collect { action ->
157+
codeNavigator.push(
158+
AppRoute.Messaging.Chat(
159+
identifier = ChatIdentifier.ByChatId(action.chatId),
160+
jumpToMessageId = action.messageId,
161+
)
162+
)
163+
}
164+
}
165+
144166
// A tab press replaces the whole back stack (tab-bar semantics — see AppNavigationBar), so every
145167
// tab home was destroyed and rebuilt on each switch: the wallet re-fetched its balances and the
146168
// chat list scrolled back to the top. Hold each tab home's ViewModels and scroll state outside the
@@ -167,6 +189,7 @@ internal fun AppContent(
167189
LocalCardExpansion provides cardExpansion,
168190
LocalTabBarVisibility provides tabBarVisibility,
169191
LocalFloatingToastHost provides toasts,
192+
LocalReactionAlerts provides reactionAlerts,
170193
) {
171194
Box(modifier = Modifier.fillMaxSize()) {
172195
// Mark the nav content as the haze source so the frosted bar blurs whatever scrolls beneath it.
@@ -301,6 +324,9 @@ internal fun AppContent(
301324
// The search button's glass growing into Search's field, over everything below it.
302325
searchMorph.Overlay(hazeState)
303326

327+
// Top of the app, over the status bar and the camera cutout, so it shows on any screen.
328+
ReactionAlertHost(controller = reactionAlerts, hazeState = hazeState)
329+
304330
// The expanded currency-info overlay is hosted INSIDE the wallet nav entry (see CardExpandHost),
305331
// not here — so a pushed Give/Convert/Withdraw naturally covers it (correct z-order) and the deck
306332
// reorganises behind it. The controller is provided app-root (above) so its fly-state survives the

‎apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import com.flipcash.app.currency.RegionSelectionScreen
4444
import com.flipcash.app.deposit.DepositFlowScreen
4545
import com.flipcash.app.invite.InviteContactScreen
4646
import com.flipcash.app.messenger.ChatFlowScreen
47-
import com.flipcash.app.messenger.ChatPhotoViewerScreen
4847
import com.flipcash.app.messenger.E2eeLearnMoreScreen
4948
import com.flipcash.app.messenger.MuteChatScreen
5049
import com.flipcash.app.messenger.ProfileScreen
@@ -171,7 +170,6 @@ internal fun appEntryProvider(
171170
annotatedEntry<AppRoute.Messaging.SharePicker> { SharePickerScreen() }
172171
annotatedEntry<AppRoute.Messaging.ShareProfileToChats> { key -> ShareProfileToChatsScreen(key) }
173172
annotatedEntry<AppRoute.Messaging.MuteChat> { key -> MuteChatScreen(key.chatId, key.chatType) }
174-
annotatedEntry<AppRoute.Messaging.PhotoViewer> { key -> ChatPhotoViewerScreen(key.chatId, key.messageId) }
175173
annotatedEntry<AppRoute.Messaging.E2eeDmInfo> { E2eeLearnMoreScreen(forGroup = false) }
176174
annotatedEntry<AppRoute.Messaging.E2eeGroupInfo> { E2eeLearnMoreScreen(forGroup = true) }
177175
annotatedEntry<AppRoute.Messaging.E2eePrivateGroupInfo> {

‎apps/flipcash/app/src/main/res/xml/file_paths.xml‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,4 @@
55
<cache-path name="share_previews" path="share_previews/" />
66
<!-- Exported tip codes, PNG + SVG (cacheDir/share_exports/). -->
77
<cache-path name="share_exports" path="share_exports/" />
8-
<!-- Chat photos handed to the share sheet (cacheDir/chat_media/). -->
9-
<cache-path name="chat_media" path="chat_media/" />
108
</paths>

‎apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -563,15 +563,6 @@ sealed interface AppRoute : NavKey, Parcelable {
563563
com.getcode.navigation.Sheet,
564564
com.getcode.navigation.WrapContentSheet
565565

566-
/**
567-
* The full-screen viewer for the photo in message [messageId] of [chatId]. Carries the
568-
* message's address rather than the photo, which is not something a back stack can hold; the
569-
* viewer reads the message back to learn how to fetch and open it.
570-
*/
571-
@Serializable
572-
@Parcelize
573-
data class PhotoViewer(val chatId: ChatId, val messageId: Long) : Messaging
574-
575566
/** The group counterpart of [E2eeDmInfo], opened from Group Info's footer. */
576567
@Serializable
577568
@Parcelize

‎apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/feed/ActivityFeedMessage.kt‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.flipcash.app.core.feed
22

3+
import com.flipcash.services.models.BoostedReaction
34
import com.getcode.opencode.model.core.ID
45
import com.getcode.opencode.model.financial.Fiat
56
import com.getcode.opencode.model.financial.LocalFiat
@@ -19,6 +20,12 @@ data class ActivityFeedMessageWithToken(
1920
val message: ActivityFeedMessage,
2021
val token: Token?,
2122
val toToken: Token? = null,
23+
/**
24+
* The display name of the group a boost was sent in, from the local chat store; null when the
25+
* entry isn't a boost, the chat is a DM (stored without a title) or isn't stored at all (never
26+
* fetched).
27+
*/
28+
val boostChatName: String? = null,
2229
)
2330

2431
data class ActivityFeedMessage(
@@ -103,6 +110,8 @@ sealed interface MessageMetadata {
103110
data class DirectlySentCrypto(
104111
val phoneNumber: String? = null,
105112
val userId: ID? = null,
113+
// Defaulted so a row persisted before this field existed still decodes (as null).
114+
val boostedReaction: BoostedReaction? = null,
106115
) : MessageMetadata
107116

108117
@Serializable
@@ -115,6 +124,8 @@ sealed interface MessageMetadata {
115124
data class ReceivedCrypto(
116125
val phoneNumber: String? = null,
117126
val userId: ID? = null,
127+
// Defaulted so a row persisted before this field existed still decodes (as null).
128+
val boostedReaction: BoostedReaction? = null,
118129
) : MessageMetadata
119130

120131
/**

0 commit comments

Comments
 (0)