Repository navigation
feat(chat): tap-to-show photo viewer chrome with react, reply, save and forward - #1854
Merged
Merged
Conversation
… and forward The viewer opens with no chrome; a tap shows a close button and two bottom pills (Zoom/Reply, Save/Forward). Forward replaces the raw-photo share with the message-link share; Save writes to the gallery through MediaStore. Reply and Forward follow the message's capabilities.
…photo before forward React opens the quick reaction strip above the button and sends the same ToggleReaction event as the context menu's strip. Pinch and double-tap still zoom.
React highlights the live picks, not the ones from when the photo opened, and stays disabled until the strip has entries. Save writes the cached original (JPEG, PNG or WebP) instead of re-encoding. Removes the unused AppRoute.Messaging.PhotoViewer route, screen and view model.
…ct button The button draws up to three of the viewer's own emoji, or the first two and a +N count, in place of the add-reaction icon. It reads the same live self-reactions as the strip highlight, ordered like the pill row.
bmc08gt
added a commit
that referenced
this pull request
Oct 26, 2026
…sule Request details showed "•••" and X as two separate circles; iOS shows them in one capsule. ActionCapsule in chat-ui has the photo viewer's pill shape, surface and spacing (#1854), so the viewer can switch to it once both land. StripSurface becomes internal for it, as #1854 also does. The deck's close X uses the same capsule; its placement is unchanged.
bmc08gt
added a commit
that referenced
this pull request
Oct 26, 2026
…sule Request details showed "•••" and X as two separate circles; iOS shows them in one capsule. ActionCapsule in chat-ui has the photo viewer's pill shape, surface and spacing (#1854), so the viewer can switch to it once both land. StripSurface becomes internal for it, as #1854 also does. The deck's close X uses the same capsule; its placement is unchanged.
bmc08gt
added a commit
that referenced
this pull request
Oct 26, 2026
* 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
bmc08gt
added a commit
that referenced
this pull request
Oct 26, 2026
* feat(messenger): send cash to several members of a private group (#1846) * feat(payments): split a group send's total between its recipients Splits in the currency's minor unit, rounding each part down and giving the leftover one unit each to the first recipients in selection order. GroupSendSplitVectorsTest runs the shared test-vectors/group-send-split.json. * feat(payments): pay a group send's parts in one intent GroupSendPaymentDelegate prices each part from its own fiat amount at one verified state and pays them with GroupPayment metadata: one recipient as a single transfer, more as one multi-transfer in selection order. The pricing and paying move out of ReactionBoostPaymentDelegate into SharedStatePayment so both use the same path. * docs(test-vectors): list group-send-split.json * feat(messenger): pay checked members from a private group's amount sheet A private group's amount sheet now leads to Select People and a confirm step instead of posting a cash link. Checked members are paid in the order they were checked, an 11th check shows the 10 People Max card, and a total under one minor unit per person shows an alert and stays on Select People. Public groups still post a cash link. * feat(messenger): add Select People and the group send confirm sheet * feat(chat): draw group sends as one bubble with a recipients sheet A cash message with multi_receiver shows who sent the total, the per-person amount ("~" on the smallest part when the split is uneven), how many people it went to, and the viewer's own part when they were paid. Tapping it opens a sheet listing the sender and recipients by name. * fix(chat): credit and label only the viewer's own part of a group send The eager balance update credited every non-sender with the full amount of any cash message, so a group send would have credited each member the total. It now credits the viewer's multi_receiver part, or the amount when they are the single receiver, and nothing when the message names others. The cash bubble said "You received" to every non-sender. A one-person group send names its recipient, so other members now read "<sender> sent". * fix(messenger): let the group send confirm sheet change the token The token pill on the confirm sheet drew a chevron but ignored taps. It now opens the token picker, and the send re-checks the balance, since the token can change after the amount step checked it. * fix(chat): loosen the group send bubble and details sheet The details sheet stacked its rows 16dp apart with 24dp avatars, against the design's 20dp rows and 32dp avatars, and had no top gap above Sent. The group bubble now ends on a "to Alex Morgan and 2 Others" line, as on iOS, and a group payment to one other member uses the same layout. * fix(chat): show the group send token name at full opacity * fix(messenger): match the confirm sheet token pill to other cash sheets Use the White10 pill background and padding from the reaction boost sheet, and drop the Each line when only one person is paid. * fix(chat): size the group send token row like the single send bubble Use the single send bubble's token icon size and lowercase "each" in the per-person line. * fix(chat): drop the group send "You received" line, name the viewer first The per-person line already gives the viewer's share. When the viewer was paid, the footer reads "to You and N Others", matching iOS. * fix(chat): trim the group send bubble's vertical padding * fix(chat): show the viewer's avatar on the group send details sheet * feat(messenger): open a recipient's profile from the group send details Each To row now shows a chevron and opens that member's profile. The push waits for the sheet's hide animation, since the sheet pops whatever is on top once it settles. * fix(messenger): show a group payment to you alone as the group card * fix(messenger): list the payee of a one-recipient group payment in its details * feat(messenger): pick the send currency from a half sheet in the chat send flow Both the amount step's token pill and the group send confirm pill open ChatStep.SendCurrency, which reuses the paid-reaction currency list, so choosing a currency returns to the same step instead of leaving the flow. * fix(messenger): open the sender and token from group send details The From row and the Sent token row had no tap target, so an incoming payment gave no way to reach the sender's profile. Both rows now show a chevron: From opens the sender's profile, Sent opens the token info. * fix(messenger): slide Select People in on the amount sheet's own nav host The amount sheet swapped its content for Select People in place, so the screen appeared without the forward slide. The sheet now hosts its own FlowHost with Amount and People steps, driven by the view model's group send stage, so Next and back animate like any other push and pop. * fix(messenger): present group send like the share sheet Group send opens as GroupSendAmountEntry, a FullscreenSheet like ShareMessage, and its steps are NonDraggableRoute, so the amount and Select People screens close by their X or back arrow only. The confirm sheet gets an X, no drag handle, and turns off drag and tap-outside dismissal. * fix(messenger): drop the confirm sheet's X to match iOS The group send confirm closes by system back, which returns to Select People, as iOS's back gesture does. Drag and tap-outside dismissal stay off for the whole flow, and the amount step keeps its X. The X row gave the avatar its top spacing, so the content now pads its own top. * feat(payment-requests): model requests in the domain and hold them in a version-merged store PaymentRequestController and its repository now return a PaymentRequest domain model instead of the generated paymentrequest.v1 types, and create takes a NewSplitRequest plus a 16-byte idempotency key. payment_intent_id travels only in PaymentRequestDetails, as a PaymentIntentId whose toString is redacted. PaymentRequests (shared/chat) opens a private group's request text once through GroupContentCrypto and keeps the greater version per request, EXPIRED over OPEN at an equal version. StatusLine and deckOrder carry the spec's time-left bands and deck order; unit tests cover the spec's cases 1-13. * feat(payment-requests): pay a request share under the server's intent id directTransfer and IntentTransfer.create take an optional intentId; without one they mint a fresh id as before. Submitting twice under the same id lands once, so a retry or a second tap cannot pay a share twice. PaymentRequestPaymentDelegate.prepare refetches the request and returns Ready only for an open request with an unpaid share and an issued payment_intent_id; pay sends that share to the creator in the request's currency with requested-payment app metadata, then refetches. The id stays in memory inside ShareToPay.Ready, whose toString leaves it out. * feat(chat): carry the payment request widget through decode, send, storage and preview A payment request widget now decodes to WidgetContent.PaymentRequest instead of unsupported, encodes for sending, and survives the message row. The chat list previews it as "<name> requested <share>" from the request store; one the device doesn't hold yet previews as nothing. The bubble still draws as unsupported until the request card lands. * feat(chat): seal and send the payment request widget as a pending chat message A failed send leaves the pending row that the transcript already retries, so a created request is posted again without creating it twice. * feat(messenger): model a new payment request's people, deadline and shares Shares go in roster order with the creator first, a group over 100 members starts with nobody selected, and a preset deadline resolves when Send is tapped. The request's idempotency key now uses the chat IdempotencyKey type. * feat(messenger): offer Send Money or Request Money from a private group's cash button Request Money skips the balance gate, since asking for money spends nothing. The Money sheet reuses Add Money's card actions, now public in :shared:funding. * feat(messenger): request money from a private group Request Money opens on the group's request history; New Request walks amount per person, a summary with name, people and time-to-pay editors, and a confirm sheet. Send Request creates the request once per confirm, under one idempotency key, then posts the request widget as a pending chat message, so a failed send retries the message and never re-creates the request. Select People takes its button text and shows the viewer as "You" so the request flow reuses it. The Money sheet change in f8a3308ec stopped four group-send tests from reaching Select People; they now pick Send Money from the sheet. * feat(chat): draw the payment request widget as a request card The card resolves from the store through a pure requestCardState: a request from another chat or creator draws as unsupported, and a DENIED or NOT_FOUND fetch marks the id missing so the card stops refetching. * feat(messenger): poll a group chat's payment requests and draw their cards in the transcript * feat(messenger): show a group chat's payment requests as a deck in the app bar * feat(messenger): add Request details with paying a share, closing and marking shares paid * fix(messenger): match request screens to the design after a device pass The amount keypad had no currency symbol until the user typed, the expanded deck and pill cards let the transcript show through, and the summary and details screens used divider rows where the design has cards. * fix(messenger): match the group request spec on errors, Send, and copy - The Money sheet has two rows, Send Money and Request Money, with no title or Dismiss row. - Create and details-action failures that have no specific copy show the app's network error instead of a new generic string. - Send is hidden on request details when the fetch carried no payment intent id for the viewer (case 20). - A request's widget message is retried twice on the same pending row, then shows the chat's Message Not Sent error with a retry. Create is never called again. - Request copy uses ' rather than ’, as the spec does. - Tests cover cases 11 and 12 through the store merge, the summary copy for 14 to 18, and case 22's "24 hours left to pay". * fix(payments): log a request share's intent id in full only once paid The pay delegate holds the server's payment_intent_id in LiveSecrets before it submits, so the SubmitIntent body and trace show only its last 6 characters. A failed payment logs that suffix and keeps the id held for the retry. A successful one releases the id and logs it in full. * fix(messenger): give Request Money's screens their own nav host Request Money swapped its content in place between history, amount, summary and the editors, so none of them slid in. Each stage is now a step on the screen's own FlowHost, the way group send hosts Select People since dc3692f. The view model's stage still decides which step is on top, and back goes through the view model so the stack follows the stage. The deadline and confirm sheets stay drawn over Summary. * fix(messenger): present Request Money and Request details as full sheets with a close Both now open the way the share sheet does: a fullscreen sheet whose root shows a close X. Drag-to-dismiss is off on both, so the close is the only way out and a half-made request can't be swiped away. * fix(messenger): close Request Money and Request details with an X, as the share sheet does Both are sheets now, so the history root's back arrow becomes the share sheet's close at the end of the app bar. Request details keeps its ••• beside the close. * fix(messenger): collapse the deck from its scrim, share each card piece, push details from history Expanded deck: tapping the scrim collapses it. Title, ring, amounts and status are each their own shared element between the pill and the first expanded card. Request details' options use the haze circle button. Deadline date and time pickers take the app's colors and type. A history row pushes Request details on Request Money's own host. * fix(messenger): edit a request's name in the group name editor The request's Name step now renders through NameEditScaffold, the layout EditGroupNameScreen uses, so both edits share the heading, field, counter and keyboard handling. Done stays the request's button. * fix(messenger): ask what the request is for on its Name screen The heading reads "What is the request for?" from its own string; the summary row keeps "Name". * fix(messenger): space the options and close buttons on request details * fix(messenger): fade the deck card's details in late and out first At 5x animator scale the expanded card's label, progress bar, deadline line and grab handle faded in from the first frame, squeezed into the pill's bounds over the title and amounts, and the handle outlived the card on collapse. They now fade in from about 60% of the card's ~300ms bounds spring and fade out in 90ms on collapse. * fix(messenger): take request card and deck spacing from the grid * fix(messenger): fade the request deck in and out of the app bar * fix(messenger): put the expanded request deck over the long-press backdrop The expanded deck dimmed the chat with its own scrim, which stopped at the app bar. It now uses the blur and dim the long-press message menu uses, over the whole screen, the back button, avatar and title included. Tapping anywhere outside the cards collapses, including the gaps between them. The backdrop was hand-copied in three places (message rows, their reaction pills, the composer). It moves into a shared chatBackdrop modifier with the same 8dp blur and 0.4 alpha; the long-press menu should look and behave as before. * fix(messenger): keep the request deck expanded under Request details Opening a card from the expanded deck collapsed the deck first, so the collapse ran behind the details screen as it came up. The deck now stays expanded, and back from details returns to the list as it was. The expanded deck drops its grab handle; drag up still collapses it. A close button takes the collapsed pill's place in the app bar, above the backdrop. It fades in with the backdrop and out first on collapse, so it is gone before the pill lands where it sits. * fix(messenger): combine Request details' options and close in one capsule Request details showed "•••" and X as two separate circles; iOS shows them in one capsule. ActionCapsule in chat-ui has the photo viewer's pill shape, surface and spacing (#1854), so the viewer can switch to it once both land. StripSurface becomes internal for it, as #1854 also does. The deck's close X uses the same capsule; its placement is unchanged. * fix(messenger): minimize the deck once Request details covers the chat Opening a request from the expanded deck left the deck expanded under the sheet, so back returned to the expanded list. The Request details step now waits for its sheet to be fully up, then sets the deck minimized while it is hidden. Back shows the chat with the deck in the app bar and no collapse animation as the sheet leaves. ChatRequestsViewModel moves to the flow's store so the step can reach the chat's instance. * fix(messenger): carry the next request up with the swipe through the deck The card behind now moves up with the front card as it is swiped and shows its request over the second half of the swipe, so the deck lands on a filled card instead of an empty slot. The swiped card fades in at the back, the pill eases between title lengths, and the press ripple is clipped to the pill. * fix(chats): preview a payment request in the chat list The mapping could word a request's preview, but no list passed it the requests, so a chat whose latest message is a request previewed as nothing. The Chats and Archived lists now read the requests store and fetch a previewed request the device doesn't hold yet, once, so the row fills in on a cold start without opening the chat. * fix(chats): preview a request by who sent it and its title The chat list previewed a request as "You requested $5.00". It now reads "You sent request for Dinner" or "Alice sent request for Dinner", with no amount. A title that can't be opened drops the "for" part. * fix(messenger): keep a deck title whole while the pill widens animateContentSize measured the pill's text at each in-between width, so a longer title arriving in front read "Sund…" until the pill caught up. The title and amounts now measure up to the widest pill's text room and the easing pill clips them, so a title ellipsizes only when it is too long for that width. * fix(messenger): swipe the deck like a stack of cards The front card now stops at one pill height and rubber-bands past it. The card behind scales up into the front slot as you drag and shows its request the whole way, so the pill is never empty. On release the swiped card drops behind and springs into the back slot instead of fading in there. Release and spring-back are springs, so a new drag picks the deck up where it is. * fix(messenger): spring the deck width from release with the cards The pill held the old request's width through the hand-over, then jumped to the new one, so a longer title showed clipped and a shorter one left a gap. The deck width is now its own spring, started when the swipe is let go and run on the cards' spring, so every card stretches with it as on iOS. Release also hands over at once: the swiped card and the cards behind finish their move from where the finger left them instead of first carrying the swipe on to a full pill height. * feat(messenger): move the deck into a strip when the viewer owes While the viewer has an unpaid share in an open request, the deck leaves the app bar for a full-width card under it: title, "You owe <share> · <time left>" and a Pay button that opens Request details straight into paying. Scrolled away from the newest messages, it shrinks to a green app-bar pill instead. Requests the viewer owes on sort to the front. The first time this device sees an owed request, the deck expands by itself with a NEW chip on that card and collapses after 2.5 s. It skips this with a screen reader or reduced motion. The pill, green pill and strip draw one stack, so they swipe the same way. The compact pill toggles when the newest message leaves the screen rather than tracking the scroll. * fix(messenger): shrink the owed strip into the grey pill, drop auto-expand Scrolled away from the newest messages, the strip now shrinks into the normal grey app-bar pill instead of a green one, and grows back at the newest messages. The deck no longer opens itself the first time it sees an owed request: the NEW chip and the per-device store of shown requests are gone. The owed screen-reader label is now read in the strip only. * fix(messenger): keep the owed strip in the pill once scrolled away Scrolling away from the newest messages shrinks the owed strip into the grey app-bar pill as the finger moves; a scroll released partway settles to the nearer end. Once it reaches the pill it stays there for the rest of the chat visit, so scrolling back to the newest messages no longer grows it. The strip comes back when the chat is reopened while still owing, or when a new owed request arrives while it is open. The pill, strip and expanded deck now share one seekable transition, so the front card's shared bounds can morph between any two of them. The strip consumes the scroll while it shrinks, so the timeline doesn't jump. DeckRest holds the pinning rule and replaces deckPlacement. * feat(messenger): show the viewer's standing on deck cards Each deck card now says where the viewer stands on it, so a member can tell which requests they've paid without opening each one (spec "Viewer status", Figma row G). - Paid share: a success badge with a black check at the glyph's bottom-right, ringed in the card surface, and "✓ You paid $5". - Owed share: "You owe $5" in green. Creator: "Your request" in grey. No share: nothing extra. - Pill and strip back cards: the standing leads the line and the " · $15 of $45" totals drop when the line doesn't fit. - Expanded cards keep "$15 paid, $45 requested" on the amounts line and put the standing at the footer's trailing end. - The deck's screen reader label adds the standing after the title. ViewerStanding is resolved in the request model; tests 29-32 cover paid, owed, creator and no share. * feat(messenger): redesign the Time To Pay sheet Replaces the six-row deadline list and the separate date-and-time picker with one sheet: preset chips from 1 Day to 4 Weeks, a grid of today through today + 28 days, and a Time row that appears for a custom day. The custom time defaults to 6:00 PM, is clamped to now + 1 h through now + 28 d - 5 min, and opens Material 3's time picker dialog. Spec tests 34-39 are in TimeToPayTest. * feat(messenger): show the Time To Pay deadline as a range The readout above the calendar becomes two fields: Starts reads "Today, on send", and Closes shows the deadline with a white outline and is announced live. A summary line under them names the preset or the custom day count. The calendar marks today with a grey circle and the closing day with a white one, joined by a 12% white band that breaks at each week's edge. Picking today shows the white circle alone. The Closes value shrinks to fit instead of cutting off a custom day and time on a phone-width field. * feat(messenger): show the Time row for presets and slide the deadline selection Time To Pay now always shows the Time row. A preset reads the current time of day; changing it makes the preset's closing day custom at that time, clamped as before. A tapped day keeps the last time set. The chip highlight, the closing-day circle and the range band now move to a new choice. The band draws as one span per week row so it has no seams while it moves. With animations off they jump straight there. Spec tests 34 and 38 now expect the Time row; 41-45 are new. * fix(messenger): turn a day black only as the sliding circle settles on it While the closing-day circle slid across the grid, each day it passed turned black for a frame before the circle reached it. The text colour now blends from white to black over the last quarter cell of the circle's approach. A band span that had just started growing into a row drew as a thin capsule at the row's edge. Spans narrower than the band is tall now fade in with their width. * feat(messenger): put the month label inside the day cell on the 1st The Time To Pay grid stacks the abbreviated month (8sp semibold, uppercased) above the number on the 1st, centred as one group in the 34dp circle. The label colour follows the cell's state: dimmed, inside the band, closing day, or start circle (tests 46-47). * fix(messenger): keep the day ripple inside the selection circle A press on a Time To Pay day lit the whole 38 dp cell, a square that ran into the neighbouring cells. The ripple is now a 34 dp circle centred on the cell, the size of the selected circle; the whole cell still takes the tap. * fix(messenger): run the deadline band square into the start and end circles Each band span drew with a full rounded cap, so where it met the start or closing-day circle the cap pinched inward under the circle's edge. A span now rounds only the ends where the range carries on past the row edge, and stays square where it starts or ends at a circle, including under the moving circle while it slides. Test 48 covers the per-end rounding. * fix(messenger): animate Request Money's first push and show history as cards The flow host got the current stage's back stack as its initial stack on every recomposition. Until the user has moved off the start, the host treats a new initial stack as a correction and swaps it in without a transition, so History -> amount appeared instantly while later pushes slid. The initial stack is now remembered once. The history list now uses iOS's card rows: 12dp corners on a 6% white fill, 16dp padding, title and date on one baseline, amounts below, and a chevron. The list gets a 20dp inset and 12dp between items.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The chat photo overlay now opens on black with no chrome. A tap shows a close button at the top left and two glass capsules at the bottom: React and Reply on the left, Save and Forward on the right. Another tap hides them.
QuickReactionStripabove the button and dispatchesToggleReaction, the same event the context menu's strip sends. The photo stays open after a pick, and the strip's highlight follows your live reactions.ReplyRequested(bubble). The overlay lives insideMessengerScreen, so no navigation result is involved.Pictures/Flipcash,IS_PENDING) and confirms with a toast. JPEG, PNG and WebP are saved byte for byte. Other formats are re-encoded to JPEG.The close circle, both capsules and the in-viewer reaction strip use
floatingGlass, with the photo as the Haze source.QuickReactionStriptakes an optionalglass: HazeState. Without one it keeps its solid surface, so the context-menu strip is unchanged. The viewer's raw-photo share path is removed:ChatPhotoShare.kt, the share strings and thechat_mediaFileProvider entry. The unusedAppRoute.Messaging.PhotoViewerroute, along withChatPhotoViewerScreenand its ViewModel, is also deleted. Seven lines in the release baseline profile still name them and will drop out on the next regeneration.