From ec28d4eb4c4e9a4ebe64e82efc001841fed4766e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 16:35:47 +0000 Subject: [PATCH 1/2] feat: redesign payment targets modal with QR, copy and pay buttons Each target now renders in a single row with the payment method and address on the left and three trailing icon buttons: QR (opens a dialog showing the raw address as a QR code), Copy (clipboard + toast), and Pay (existing payto:// intent). Adds QrCode2 to MaterialSymbols. https://claude.ai/code/session_01JtJuvSYMusKiDMWo7N8Aj8 --- .../loggedIn/profile/header/PaymentButton.kt | 143 ++++++++++++++++-- .../commons/icons/symbols/MaterialSymbols.kt | 1 + 2 files changed, 128 insertions(+), 16 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt index 173bc6423d..b95bb1f5df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt @@ -21,19 +21,36 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header import android.content.Intent +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.core.net.toUri import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -42,13 +59,14 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.M3ActionDialog -import com.vitorpamplona.amethyst.ui.components.M3ActionRow import com.vitorpamplona.amethyst.ui.components.M3ActionSection import com.vitorpamplona.amethyst.ui.components.util.setText import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.ZeroPadding import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent @@ -86,6 +104,7 @@ fun PaymentButtonWithTargets(targets: List) { val scope = rememberCoroutineScope() var expanded by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf(null) } + var qrContent by remember { mutableStateOf(null) } FilledTonalButton( modifier = @@ -108,36 +127,41 @@ fun PaymentButtonWithTargets(targets: List) { ) { M3ActionSection { targets.forEach { target -> - M3ActionRow( - icon = MaterialSymbols.AccountBalanceWallet, - text = "${target.type.replaceFirstChar(Char::titlecase)}: ${target.authority}", - onClick = { - expanded = false + PaymentTargetRow( + target = target, + onShowQr = { qrContent = target.authority }, + onCopy = { + scope.launch { + clipboardManager.setText(target.authority) + Toast + .makeText( + context, + stringRes(context, R.string.copied_to_clipboard), + Toast.LENGTH_SHORT, + ).show() + } + }, + onPay = { try { val intent = Intent(Intent.ACTION_VIEW, "payto://${target.type}/${target.authority}".toUri()) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK context.startActivity(intent) + expanded = false } catch (e: Exception) { if (e is kotlinx.coroutines.CancellationException) throw e errorMessage = stringRes(context, R.string.no_payment_app_found) } }, ) - M3ActionRow( - icon = MaterialSymbols.ContentCopy, - text = stringRes(R.string.copy_to_clipboard), - onClick = { - expanded = false - scope.launch { - clipboardManager.setText(target.authority) - } - }, - ) } } } } + qrContent?.let { value -> + PaymentTargetQrDialog(value = value, onDismiss = { qrContent = null }) + } + errorMessage?.let { msg -> ErrorMessageDialog( title = stringRes(R.string.error_dialog_payment_error), @@ -146,3 +170,90 @@ fun PaymentButtonWithTargets(targets: List) { ) } } + +@Composable +private fun PaymentTargetRow( + target: PaymentTarget, + onShowQr: () -> Unit, + onCopy: () -> Unit, + onPay: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 10.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = target.type.replaceFirstChar(Char::titlecase), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = target.authority, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + Spacer(modifier = Modifier.width(8.dp)) + IconButton(onClick = onShowQr) { + Icon( + symbol = MaterialSymbols.QrCode2, + contentDescription = stringRes(R.string.show_qr), + modifier = Size20Modifier, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onCopy) { + Icon( + symbol = MaterialSymbols.ContentCopy, + contentDescription = stringRes(R.string.copy_to_clipboard), + modifier = Size20Modifier, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onPay) { + Icon( + symbol = MaterialSymbols.Bolt, + contentDescription = stringRes(R.string.payment_targets), + modifier = Size20Modifier, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@Composable +private fun PaymentTargetQrDialog( + value: String, + onDismiss: () -> Unit, +) { + Dialog(onDismissRequest = onDismiss) { + Surface( + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + QrCodeDrawer( + contents = value, + modifier = Modifier.size(260.dp), + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = value, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt index 1530c9af57..db0d05359a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt @@ -175,6 +175,7 @@ object MaterialSymbols { val Public = MaterialSymbol("\uE80B") val PublicOff = MaterialSymbol("\uF1CA") val PushPin = MaterialSymbol("\uF10D") + val QrCode2 = MaterialSymbol("\uEF6B") val RadioButtonChecked = MaterialSymbol("\uE837") val RadioButtonUnchecked = MaterialSymbol("\uE836") val Recommend = MaterialSymbol("\uE9D2") From 7232456e9e6bdb545f5524c26ada1e650c8446e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 16:53:11 +0000 Subject: [PATCH 2/2] fix: share payment targets dialog with reactions row, restore QR glyph - Extract PaymentTargetsDialog from PaymentButton so the reactions row can render the same QR/Copy/Pay layout instead of the legacy two-row M3ActionRow per target. - PayReaction now subscribes via EventFinderFilterAssemblerSubscription and observes the PaymentTargetsEvent reactively, so targets actually load when the wallet icon is tapped. - Correct the QrCode2 codepoint (U+E00A) and re-subset the bundled Material Symbols Outlined font so the glyph is no longer a tofu box. https://claude.ai/code/session_01JtJuvSYMusKiDMWo7N8Aj8 --- .../amethyst/ui/note/ReactionsRow.kt | 71 ++---------------- .../loggedIn/profile/header/PaymentButton.kt | 41 +++++++--- .../font/material_symbols_outlined.ttf | Bin 427568 -> 429012 bytes .../commons/icons/symbols/MaterialSymbols.kt | 2 +- 4 files changed, 39 insertions(+), 75 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 336f894ecc..77f49a84af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -85,7 +85,6 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.Role @@ -100,7 +99,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties -import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.vitorpamplona.amethyst.R @@ -112,6 +110,8 @@ import com.vitorpamplona.amethyst.model.ReactionRowAction import com.vitorpamplona.amethyst.model.ReactionRowItem import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.ZapPaymentHandler +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReactionCount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReactions import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReferences @@ -129,16 +129,13 @@ import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer -import com.vitorpamplona.amethyst.ui.components.M3ActionDialog -import com.vitorpamplona.amethyst.ui.components.M3ActionRow -import com.vitorpamplona.amethyst.ui.components.M3ActionSection import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorMessage -import com.vitorpamplona.amethyst.ui.components.util.setText import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeReplyTo import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.PaymentTargetsDialog import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.ButtonBorder @@ -352,15 +349,9 @@ fun PayReaction( ) { val authorPubkey = baseNote.author?.pubkeyHex ?: return val address = remember(authorPubkey) { PaymentTargetsEvent.createAddress(authorPubkey) } - val context = LocalContext.current - val clipboardManager = LocalClipboard.current - val scope = rememberCoroutineScope() LoadAddressableNote(address, accountViewModel) { note -> - val targets = remember(note) { (note?.event as? PaymentTargetsEvent)?.paymentTargets() ?: emptyList() } - var expanded by remember { mutableStateOf(false) } - var errorMessage by remember { mutableStateOf(null) } ClickableBox( modifier = iconSizeModifier, @@ -374,58 +365,12 @@ fun PayReaction( ) } - if (expanded) { - M3ActionDialog( - title = stringRes(R.string.payment_targets), - onDismiss = { expanded = false }, - ) { - M3ActionSection { - if (targets.isEmpty()) { - M3ActionRow( - icon = MaterialSymbols.AccountBalanceWallet, - text = stringRes(R.string.no_payment_targets_message), - enabled = false, - onClick = {}, - ) - } else { - targets.forEach { target -> - M3ActionRow( - icon = MaterialSymbols.AccountBalanceWallet, - text = "${target.type.replaceFirstChar(Char::titlecase)}: ${target.authority}", - onClick = { - expanded = false - try { - val intent = Intent(Intent.ACTION_VIEW, "payto://${target.type}/${target.authority}".toUri()) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - context.startActivity(intent) - } catch (e: Exception) { - if (e is kotlinx.coroutines.CancellationException) throw e - errorMessage = stringRes(context, R.string.no_payment_app_found) - } - }, - ) - M3ActionRow( - icon = MaterialSymbols.ContentCopy, - text = stringRes(R.string.copy_to_clipboard), - onClick = { - expanded = false - scope.launch { - clipboardManager.setText(target.authority) - } - }, - ) - } - } - } - } - } + if (expanded && note != null) { + EventFinderFilterAssemblerSubscription(note, accountViewModel) + val event by observeNoteEvent(note, accountViewModel) + val targets = remember(event) { event?.paymentTargets() ?: emptyList() } - errorMessage?.let { msg -> - ErrorMessageDialog( - title = stringRes(R.string.error_dialog_payment_error), - textContent = msg, - onDismiss = { errorMessage = null }, - ) + PaymentTargetsDialog(targets = targets, onDismiss = { expanded = false }) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt index b95bb1f5df..d436464661 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.M3ActionDialog +import com.vitorpamplona.amethyst.ui.components.M3ActionRow import com.vitorpamplona.amethyst.ui.components.M3ActionSection import com.vitorpamplona.amethyst.ui.components.util.setText import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog @@ -99,12 +100,7 @@ fun PaymentButton( @Composable fun PaymentButtonWithTargets(targets: List) { - val context = LocalContext.current - val clipboardManager = LocalClipboard.current - val scope = rememberCoroutineScope() var expanded by remember { mutableStateOf(false) } - var errorMessage by remember { mutableStateOf(null) } - var qrContent by remember { mutableStateOf(null) } FilledTonalButton( modifier = @@ -121,11 +117,34 @@ fun PaymentButtonWithTargets(targets: List) { } if (expanded) { - M3ActionDialog( - title = stringRes(R.string.payment_targets), - onDismiss = { expanded = false }, - ) { - M3ActionSection { + PaymentTargetsDialog(targets = targets, onDismiss = { expanded = false }) + } +} + +@Composable +fun PaymentTargetsDialog( + targets: List, + onDismiss: () -> Unit, +) { + val context = LocalContext.current + val clipboardManager = LocalClipboard.current + val scope = rememberCoroutineScope() + var errorMessage by remember { mutableStateOf(null) } + var qrContent by remember { mutableStateOf(null) } + + M3ActionDialog( + title = stringRes(R.string.payment_targets), + onDismiss = onDismiss, + ) { + M3ActionSection { + if (targets.isEmpty()) { + M3ActionRow( + icon = MaterialSymbols.AccountBalanceWallet, + text = stringRes(R.string.no_payment_targets_message), + enabled = false, + onClick = {}, + ) + } else { targets.forEach { target -> PaymentTargetRow( target = target, @@ -146,7 +165,7 @@ fun PaymentButtonWithTargets(targets: List) { val intent = Intent(Intent.ACTION_VIEW, "payto://${target.type}/${target.authority}".toUri()) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK context.startActivity(intent) - expanded = false + onDismiss() } catch (e: Exception) { if (e is kotlinx.coroutines.CancellationException) throw e errorMessage = stringRes(context, R.string.no_payment_app_found) diff --git a/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf b/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf index 91b81096f3421e1641ef516e197fe725cbe333e2..75fdb739c982d91fd42e87e762796c7264227335 100644 GIT binary patch delta 2819 zcmbtUeNy&+yc64H>HTtf)O3PM#1fm%%v6cH6;K&sa1go%~4eiiMc&Nb56 zuhru@ldZDtw4;u-wWXF4r>=B8v~C{fQbw^yfz}q$6QPZA(_rMI#XXm~t@~qt?Y;Ru zzxVw;Pu}NwKkjWgAboOJ@`3?~C&NL%c;$l&4()z%4bZU{U{4jVDkytGzxyGelY&$O zD^||VDk$BM540#k#me%6@@M5;hk>pE8vnWSN0l{*8EgSQX`r!Z+x)PN@1t}9wtKPb+wrt2#Kz^ADes9b6jg`;L@f`== z_(mCTt*qU__OU7&e@5~4ZI!mIo7OI=I}My9Q5~~m`_5hVl%LV+-=)HgDxZ~IuVu!1 zIV#zkjd_@l#aNC~JP6v9dvdZ9QVI9pVg3#?Vm>=3C5l*mhuOczIVq%OCnzz=ll_#* zIkFR!+0+EZqtbdBJu>0jPBP5wnb=?Jt#!)BYss*C728OTiT1{cc8W{$cqWcGole%| zto3+2XV2C)Ha1dcveCaiRkg=ud19SnRo2PAz1CB!Og!1nQt(=*bHYwj^f{dXk2IvI z8`KSmf({~*kP2Voo>Q}<*Rzedo3@TlsQ%-(@A8F@3M%h6E~o2J*Adsbys3F*dA_{9 z`L6ko&j0=VzvL(67w2CnuotW@s3~}>;L`;&7wlZ{(L((~=fZ;vKVEpd&|EmHa9-i+ z!bb|9E^H_~U3jtZ>%!ZM5*Af0x>)o;k+-P1=*D8};;uXtv~xfLUx8qb+hU8%G5=cQd`?y^_P zhF9jUe6yS{UtRuk`4^LSUL`mEmbEYQooaqSRX=pBrT&$-tC&!)X~~JT(dw$`m54%B zpWrKM*~FOOd*hvrEM?q2Y9F@e*>Bo|_E(d}=?Yy{_97{e)NeN=l_dSizN>zg&+@?2 zwrs$5()Nz+CEGFEo3t1UvV7*Sb%R0+ysNd%k zrIB(r3ZUbEbivZWHsP0LG`RpvA1IhLsUR$uDspP09c-)MdJDikJeXChAw4hD~pqZkjfkJ~T~F*qz`u28@A(#}j57 zpO25#e-vLC-&>z~JR##TK=-Y#LRUxJt-Gw##+``Uqf^D@>SpQ^;u?YIp6KsKJ)&%( zZ`<*WEcPCjiVCJ@1|~6c{pWv}H+w0Lp$JFNh)d{YCCrWcFu)4gD$K@QR*r3KE}CG) z1(uA9%;_sWk)vkoeNX=JluD&q@5_Dfn;2orV>KH#Zm-(3Au&1FuO$MV?%%2~vU*;1tj=3!;5oULMO>W_7- zp0Zv!3YaNv+}|lJTjx9bakOg60k&RQnWo(L+4-rQcMH_lZBP%LfqJ+W>c|+>6Gm_> z6I^r+xVT?~Grs|D>bu~Q2+1FUOM4hxh6h~kQ{d*Az`53fn{NlVko-lvz%6%xE8PQb z_3Pl)TfuDzfZI&kqpQGe&jPoj2waUsI04SPs~6nkXTa?yuI&W3S0E5SIhXJ%1+;_P ze*oMI9pDa8VZYf1?xhiMP5Z&UMw6b{1Fnq{eRvg|{}8y&W^ld7!F@q7*Mi`_TmvrL z5AN0$aJTn?SLK5jg5cxUfVWg&18;i z(tdaZ{A2fm-`NTN$7SHXq&+tm{DDK@59NXX`ChLE-`oJc<;9GAYHkc1SVQj})rGxwy0&(K`YJqUXT(zo8iJOv+Tm{XH?%_PYHkR0dC8Q| zlByl#kQ@QO6p&i|t$v4F)7lVdg67PP=k7ZB2u1=oRbk9u%=|QLby9c>3I6(y}`q)j3g}%BKijNWFF&3v2H3~xEvB6uRuf}d^ zV#YLz9H$F~HSwY*9>Q2%vSti}L*dZim@ZBuDlEi{(2zPdbSoUv#f3xrL{uOFccFy7 WD8=+eDFL39-setA@AIa!D&QaH;F?bW delta 1463 zcmW+!3rtgI6#oA9zdS@Jg>r9cOR0+JoXUKF&jO_diUlbxOebWcyu$z)bPDR$saw#A zGujO2q6CpSoeY60sMBRAS;(SJ&^T6g*?bT;aS|XpCxrdoP0l^9@0{=aC+F5dMSH!% z0RlkEeiVVp)~tjB1r3S7wH$!#Futj?)CRmi30(gj5DleUqZaE*--!oK_p+*7Yo;!< zZp3~N=Vq0IzvPEWc;0ybTzrXZhh`TeYK- z8G(#2lWCvgvf3iqP}M0$t^!VU7PEq+?7@qyVIR28l9EDO!LkKCV?b*w!^e06-VeM( z*g#Fpk0>d(*I4G5_p|&%Ht>bAjrq21J(jP4lOrC#+*VUb9HfBxHyB=1VN;hEZb;dF z1?VyZ;^~zetL;aQ7PHr!XB!B9?oyGbHLj4YSEKxLNIqt=A~u`z=-FV`)Hl;aj;_^@ zv?{GZ`?>aN?1I=0vE8wc;=<$N;`YT|jyK1D8Q&j2ted0D(tWLanvk6EX+oFYM{m;C z>JR8o=+El=^$+z^h9HC1kYOk@)Ef2~eoM?qbS1t_icBg=YDkVw{v>(Km~CuK@kz-} z=}d92NnG>68n?;ERBk$!IxDq$b(%OWJMG!p^=tdmk#0z@N*^}In|GVLGW;@9Gx{t_ zONFJ~I@{{7_Gij7-}21PL^RSO*GjWoz2B}7Ij2sy5jb1h3&|{JP)D>M&oFrm<&8X7 zW=A1$eb<>sX8fZW)5L1VHBU6h!(Xw*J@D)yd?@_BCMY~L{F0{H8Gk0!@v(Xdpzc<8 zsE?@I)Th+Z>NK@6tki=In+mH}Jyn&d_NZFIq^fPI7(g}5Bv`dTB~?}^gOovl(qF0c zpcStaFBL;dlOjCVU=U^i`~W}2Kj(+|7G{_5WBh4; zoG;{i_{fmWA!}trvY`-rNR({1)Iac=)F%Bk(B|O*B!5U=1O`ZYC7-dgtz`bBBv-PX z=|Pt93FzVjwn#()F_QTbSwIuuH|Y1acPWT#$Ju=1A3-9KOC&@}LW$rU?OGL;f;J?f z8BMqcH!+bkEW%@=Cs~NXN@7I?S%DU)aGPlG3t8pL>5dkY9M^#h=R~g5OB2&wmY)`J zCXRR$U*bpn|L+}41lC_g)I>vO62?j9l6m9}vY0F-%g73{ifBn3(J_`pl1x%OtR$Ue z5G%zvnrUgyhBpM7#hwD86`kqB@-9`FUhrxTFnf*1$1*gXbsb~7eTko z1>Kqe`cVLAof&j*18Bnt=%Fp3O>Cg0lpI%j^P| z%VLEQ;L6&-ZM1;fbPL>87Tb9d+%6foy+454zY^S`A#jZ>ckC~4ClugX