add contact filtering to search bar when using @

This commit is contained in:
Blake Kaufman
2026-05-19 11:53:35 -04:00
parent 15f3c01bd8
commit 0cadeca6b0
10 changed files with 459 additions and 290 deletions
@@ -364,11 +364,14 @@ const BusinessCard = React.memo(
style={[
styles.button,
{
paddingTop: item?.description ? 24 : 0,
marginTop: item?.description ? 24 : 0,
borderTopColor: backgroundColor,
borderTopWidth: StyleSheet.hairlineWidth,
},
]}
onPress={handleWebsitePress}
activeOpacity={0.8}
hitSlop={{ top: 20, bottom: 20, left: 10, right: 10 }}
>
<View style={styles.buttonCopy}>
<ThemeIcon
@@ -382,11 +385,7 @@ const BusinessCard = React.memo(
/>
</View>
<ThemeIcon
size={16}
colorOverride={theme ? COLORS.darkModeText : COLORS.lightModeText}
iconName="ArrowUpRight"
/>
<ThemeIcon size={16} iconName="ArrowUpRight" />
</TouchableOpacity>
</View>
);
@@ -474,8 +473,7 @@ const styles = StyleSheet.create({
opacity: 0.85,
},
button: {
paddingTop: 24,
borderRadius: 18,
paddingTop: 16,
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
@@ -1,4 +1,11 @@
import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import {
FlatList,
ScrollView,
StyleSheet,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native';
import {
CENTER,
CONTENT_KEYBOARD_OFFSET,
@@ -35,14 +42,18 @@ import Animated, {
withTiming,
} from 'react-native-reanimated';
import ThemeImage from '../../../../functions/CustomElements/themeImage';
import { useProcessedContacts } from '../contacts/contactsPageComponents/hooks';
import {
useProcessedContacts,
useFilteredContacts,
} from '../contacts/contactsPageComponents/hooks';
import getClipboardText from '../../../../functions/getClipboardText';
import { AddContactOverlay } from '../contacts/addContactOverlay';
import useHandleBackPressNew from '../../../../hooks/useHandleBackPressNew';
import CustomButton from '../../../../functions/CustomElements/button';
import getReceiveAddressAndContactForContactsPayment from '../contacts/internalComponents/getReceiveAddressAndKindForPayment';
import { parse } from 'tldts';
import { hasStringAsync } from 'expo-clipboard';
import { scheduleOnRN } from 'react-native-worklets';
import { KeyboardController } from 'react-native-keyboard-controller';
const ContactRow = ({
contact,
@@ -85,17 +96,6 @@ const ContactRow = ({
transform: [{ rotate: `${chevronRotation.value * 180}deg` }],
}));
const lnurlDomain = useMemo(() => {
try {
if (!contact.isLNURL) return '';
const parsed = parse(contact.receiveAddress);
return parsed.domain;
} catch (err) {
console.log('error parsing lnurl', err);
return '';
}
}, [contact.isLNURL]);
return (
<View
style={styles.contactWrapper}
@@ -130,21 +130,6 @@ const ContactRow = ({
styles={styles.contactName}
content={formatDisplayName(contact) || contact.uniqueName || ''}
/>
{contact.isLNURL && lnurlDomain && (
<View
style={[
styles.lnurlDomainContainer,
{
backgroundColor:
theme && darkModeType
? backgroundColor
: backgroundOffset,
},
]}
>
<ThemeText styles={styles.lnurlDomain} content={lnurlDomain} />
</View>
)}
</View>
<Animated.View style={labelFadeStyle}>
<ThemeText
@@ -153,12 +138,8 @@ const ContactRow = ({
/>
</Animated.View>
</View>
<Animated.View style={[{ opacity: HIDDEN_OPACITY }, chevronStyle]}>
<ThemeIcon
size={20}
iconName={'ChevronDown'}
colorOverride={textColor}
/>
<Animated.View style={chevronStyle}>
<ThemeIcon size={20} iconName={'ChevronDown'} />
</Animated.View>
</TouchableOpacity>
@@ -289,6 +270,49 @@ const ContactRow = ({
);
};
const FilteredContactItem = ({
contact,
cache,
theme,
darkModeType,
backgroundOffset,
backgroundColor,
onPress,
}) => {
return (
<TouchableOpacity
style={styles.contactRowContainer}
onPress={() => onPress(contact)}
>
<View
style={[
styles.contactImageContainer,
{
backgroundColor:
theme && darkModeType ? backgroundColor : backgroundOffset,
},
]}
>
<ContactProfileImage
updated={cache[contact.uuid]?.updated}
uri={cache[contact.uuid]?.localUri}
darkModeType={darkModeType}
theme={theme}
/>
</View>
<View style={styles.nameContainer}>
<ThemeText
CustomEllipsizeMode={'tail'}
CustomNumberOfLines={1}
styles={styles.contactName}
content={formatDisplayName(contact) || contact.uniqueName || ''}
/>
</View>
<ThemeIcon size={20} iconName={'ChevronRight'} />
</TouchableOpacity>
);
};
export default function HalfModalSendOptions({
setIsKeyboardActive,
isKeyboardActive,
@@ -302,6 +326,7 @@ export default function HalfModalSendOptions({
const [inputError, setInputError] = useState('');
const [expandedContact, setExpandedContact] = useState(null);
const [showAddContact, setShowAddContact] = useState(false);
const [noInputMounted, setNoInputMounted] = useState(true);
const [visibleCount, setVisibleCount] = useState(8);
const [scrollViewHeight, setScrollViewHeight] = useState(0);
const [showPasteButton, setShowPasteButton] = useState(true);
@@ -319,12 +344,22 @@ export default function HalfModalSendOptions({
useGlobalContacts();
const { t } = useTranslation();
const { backgroundColor, backgroundOffset, textColor } = GetThemeColors();
const isContactInputMode = inputText.startsWith('@');
const contactInfoList = useProcessedContacts(
decodedAddedContacts,
contactsMessags,
);
const filteredContacts = useFilteredContacts(
isContactInputMode ? contactInfoList : [],
isContactInputMode ? inputText.trim() : '',
false,
);
const contactSearchUsername = useMemo(() => {
return inputText.startsWith('@') ? inputText.slice(1).trim() : '';
}, [inputText]);
const contentOpacity = useSharedValue(1);
const contentTranslateX = useSharedValue(0);
const inputModeProgress = useSharedValue(0);
@@ -347,9 +382,14 @@ export default function HalfModalSendOptions({
}, [showAddContact]);
useEffect(() => {
inputModeProgress.value = withTiming(isInputMode ? 1 : 0, {
duration: 220,
});
if (isInputMode) {
inputModeProgress.value = withTiming(1, { duration: 220 }, finished => {
if (finished) scheduleOnRN(setNoInputMounted, false);
});
} else {
setNoInputMounted(true);
inputModeProgress.value = withTiming(0, { duration: 220 });
}
}, [isInputMode]);
const contentStyle = useAnimatedStyle(() => ({
@@ -357,11 +397,11 @@ export default function HalfModalSendOptions({
transform: [{ translateX: contentTranslateX.value }],
}));
const belowInputStyle = useAnimatedStyle(() => ({
const noInputPageStyle = useAnimatedStyle(() => ({
opacity: 1 - inputModeProgress.value,
}));
const continueButtonStyle = useAnimatedStyle(() => ({
const inputPageStyle = useAnimatedStyle(() => ({
opacity: inputModeProgress.value,
}));
@@ -398,6 +438,7 @@ export default function HalfModalSendOptions({
const normalized = input.startsWith('@')
? input.slice(1).toLowerCase()
: input.toLowerCase();
if (!normalized) return;
const matchedContact = decodedAddedContacts.find(
c => c.uniqueName?.toLowerCase() === normalized,
);
@@ -779,244 +820,342 @@ export default function HalfModalSendOptions({
visibleCount,
]);
const handleFilteredContactPress = useCallback(
async contact => {
handleBackPressFunction(async () => {
navigate.replace('SendAndRequestPage', {
selectedContact: contact,
paymentType: 'send',
imageData: cache[contact.uuid],
endReceiveType: 'BTC',
selectedPaymentMethod: 'BTC',
});
});
},
[navigate, globalContactsInformation, cache, t, handleBackPressFunction],
);
const renderFilteredContact = useCallback(
({ item: { contact } }) => (
<FilteredContactItem
contact={contact}
cache={cache}
theme={theme}
darkModeType={darkModeType}
backgroundOffset={backgroundOffset}
backgroundColor={backgroundColor}
onPress={handleFilteredContactPress}
/>
),
[
cache,
theme,
darkModeType,
backgroundOffset,
backgroundColor,
handleFilteredContactPress,
],
);
const filteredContactKeyExtractor = useCallback(
({ contact }) => contact.uuid,
[],
);
return (
<View style={styles.container}>
<Animated.View style={[styles.mainContent, contentStyle]}>
<ScrollView
ref={scrollViewRef}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
scrollEnabled={!isInputMode}
contentContainerStyle={{
...styles.innerContainer,
paddingBottom: bottomPadding,
}}
stickyHeaderIndices={[4]}
onScroll={e => {
scrollOffsetRef.current = e.nativeEvent.contentOffset.y;
}}
scrollEventThrottle={16}
onLayout={e => {
const h = e.nativeEvent.layout.height;
scrollViewHeightRef.current = h;
setScrollViewHeight(h);
}}
{/* Search Input with Clipboard Icon */}
<View
style={[
styles.searchContainer,
{
backgroundColor: backgroundColor,
maxHeight: Math.round(scrollViewHeight - 50),
},
]}
>
{/* Search Input with Clipboard Icon */}
<View
style={[
styles.searchContainer,
{
backgroundColor: backgroundColor,
maxHeight: Math.round(scrollViewHeight - 50),
},
]}
>
<CustomSearchInput
textInputRef={textInputRef}
placeholderText={t('wallet.halfModal.inputPlaceholder')}
textInputMultiline={true}
inputText={inputText}
setInputText={setInputText}
onBlurFunction={onBlurFunction}
onFocusFunction={onFocusFunction}
textInputStyles={{
paddingRight: showPasteButton || inputText.trim() ? 40 : 10,
}}
returnKeyType="go"
onSubmitEditingFunction={handleManualInputSubmit}
/>
{inputText.trim() ? (
<TouchableOpacity
onPress={onTrimFunction}
style={styles.clipboardButton}
>
<ThemeIcon
colorOverride={
theme && darkModeType
? COLORS.lightModeText
: COLORS.primary
}
size={20}
iconName={'X'}
/>
</TouchableOpacity>
) : showPasteButton ? (
<TouchableOpacity
onPress={handleClipboardPaste}
style={styles.clipboardButton}
>
<ThemeIcon
colorOverride={
theme && darkModeType
? COLORS.lightModeText
: COLORS.primary
}
size={20}
iconName={'Clipboard'}
/>
</TouchableOpacity>
) : null}
</View>
<Animated.View
style={belowInputStyle}
pointerEvents={isInputMode ? 'none' : 'auto'}
>
{/* Scan QR Code Button */}
<CustomSearchInput
textInputRef={textInputRef}
placeholderText={t('wallet.halfModal.inputPlaceholder')}
textInputMultiline={true}
inputText={inputText}
setInputText={setInputText}
onBlurFunction={onBlurFunction}
onFocusFunction={onFocusFunction}
textInputStyles={{
paddingRight: showPasteButton || inputText.trim() ? 40 : 10,
}}
returnKeyType="go"
onSubmitEditingFunction={handleManualInputSubmit}
/>
{inputText.trim() ? (
<TouchableOpacity
style={[styles.scanButton, { marginBottom: 0 }]}
onPress={handleCameraScan}
onPress={onTrimFunction}
style={styles.clipboardButton}
>
<View
style={[
styles.scanIconContainer,
{
backgroundColor:
theme && darkModeType
? backgroundColor
: backgroundOffset,
},
]}
>
<ThemeIcon
colorOverride={
theme && darkModeType ? COLORS.darkModeText : COLORS.primary
}
size={24}
iconName={'ScanQrCode'}
/>
</View>
<View style={styles.scanTextContainer}>
<ThemeText
styles={styles.scanButtonText}
content={t('wallet.halfModal.scanQrCode')}
/>
<ThemeText
styles={styles.scanButtonSubtext}
content={t('wallet.halfModal.tapToScanQr')}
/>
</View>
</TouchableOpacity>
{/* Scan Image Button */}
<TouchableOpacity
style={styles.scanButton}
onPress={handleImageScan}
>
<View
style={[
styles.scanIconContainer,
{
backgroundColor:
theme && darkModeType
? backgroundColor
: backgroundOffset,
},
]}
>
<ThemeIcon
colorOverride={
theme && darkModeType ? COLORS.darkModeText : COLORS.primary
}
size={24}
iconName={'Image'}
/>
</View>
<View style={styles.scanTextContainer}>
<ThemeText
styles={styles.scanButtonText}
content={t('wallet.halfModal.images')}
/>
<ThemeText
styles={styles.scanButtonSubtext}
content={t('wallet.halfModal.tapToScan')}
/>
</View>
</TouchableOpacity>
{/* Divider */}
<View
style={[
styles.divider,
{
borderColor:
theme && darkModeType
? 'rgba(255, 255, 255, 0.1)'
: 'rgba(0, 0, 0, 0.05)',
},
]}
/>
<View
style={[
{
backgroundColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
},
]}
>
<ThemeText
styles={styles.sectionHeader}
content={t('wallet.halfModal.addressBook', {
context: 'send',
})}
<ThemeIcon
colorOverride={
theme && darkModeType ? COLORS.lightModeText : COLORS.primary
}
size={20}
iconName={'X'}
/>
</View>
</TouchableOpacity>
) : showPasteButton ? (
<TouchableOpacity
onPress={handleClipboardPaste}
style={styles.clipboardButton}
>
<ThemeIcon
colorOverride={
theme && darkModeType ? COLORS.lightModeText : COLORS.primary
}
size={20}
iconName={'Clipboard'}
/>
</TouchableOpacity>
) : null}
</View>
{/* Address Book Section */}
{decodedAddedContacts.length > 0 ? (
contactElements
) : (
<View style={styles.emptyContactsContainer}>
<ThemeIcon iconName={'UsersRound'} />
<ThemeText
styles={styles.emptyTitle}
content={t('wallet.halfModal.noAddedContactsTitle')}
/>
<ThemeText
styles={styles.emptySubtext}
content={t('wallet.halfModal.noAddedContactsDesc')}
/>
<CustomButton
buttonStyles={{ width: '100%' }}
textContent={t('contacts.editMyProfilePage.addContactBTN')}
actionFunction={() => setShowAddContact(true)}
<View style={styles.pageContainer}>
{/* Page: No Input */}
{noInputMounted && (
<Animated.View
style={[styles.page, noInputPageStyle]}
pointerEvents={isInputMode ? 'none' : 'auto'}
>
<ScrollView
ref={scrollViewRef}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
contentContainerStyle={{
...styles.innerContainer,
paddingBottom: bottomPadding,
}}
stickyHeaderIndices={[3]}
onScroll={e => {
scrollOffsetRef.current = e.nativeEvent.contentOffset.y;
}}
scrollEventThrottle={16}
onLayout={e => {
const h = e.nativeEvent.layout.height;
scrollViewHeightRef.current = h;
setScrollViewHeight(h);
}}
>
{/* Scan QR Code Button */}
<TouchableOpacity
style={[styles.scanButton, { marginBottom: 0 }]}
onPress={handleCameraScan}
>
<View
style={[
styles.scanIconContainer,
{
backgroundColor:
theme && darkModeType
? backgroundColor
: backgroundOffset,
},
]}
>
<ThemeIcon
colorOverride={
theme && darkModeType
? COLORS.darkModeText
: COLORS.primary
}
size={24}
iconName={'ScanQrCode'}
/>
</View>
<View style={styles.scanTextContainer}>
<ThemeText
styles={styles.scanButtonText}
content={t('wallet.halfModal.scanQrCode')}
/>
<ThemeText
styles={styles.scanButtonSubtext}
content={t('wallet.halfModal.tapToScanQr')}
/>
</View>
</TouchableOpacity>
{/* Scan Image Button */}
<TouchableOpacity
style={styles.scanButton}
onPress={handleImageScan}
>
<View
style={[
styles.scanIconContainer,
{
backgroundColor:
theme && darkModeType
? backgroundColor
: backgroundOffset,
},
]}
>
<ThemeIcon
colorOverride={
theme && darkModeType
? COLORS.darkModeText
: COLORS.primary
}
size={24}
iconName={'Image'}
/>
</View>
<View style={styles.scanTextContainer}>
<ThemeText
styles={styles.scanButtonText}
content={t('wallet.halfModal.images')}
/>
<ThemeText
styles={styles.scanButtonSubtext}
content={t('wallet.halfModal.tapToScan')}
/>
</View>
</TouchableOpacity>
{/* Divider */}
<View
style={[
styles.divider,
{
borderColor:
theme && darkModeType
? 'rgba(255, 255, 255, 0.1)'
: 'rgba(0, 0, 0, 0.05)',
},
]}
/>
<View
style={[
{
backgroundColor:
theme && darkModeType
? backgroundOffset
: backgroundColor,
},
]}
>
<ThemeText
styles={styles.sectionHeader}
content={t('wallet.halfModal.addressBook', {
context: 'send',
})}
/>
</View>
{/* Address Book Section */}
{decodedAddedContacts.length > 0 ? (
contactElements
) : (
<View style={styles.emptyContactsContainer}>
<ThemeIcon iconName={'UsersRound'} />
<ThemeText
styles={styles.emptyTitle}
content={t('wallet.halfModal.noAddedContactsTitle')}
/>
<ThemeText
styles={styles.emptySubtext}
content={t('wallet.halfModal.noAddedContactsDesc')}
/>
<CustomButton
buttonStyles={{ width: '100%' }}
textContent={t(
'contacts.editMyProfilePage.addContactBTN',
)}
actionFunction={() => setShowAddContact(true)}
/>
</View>
)}
</ScrollView>
</Animated.View>
)}
{/* Page: Input */}
{isInputMode && (
<Animated.View
style={[
styles.page,
inputPageStyle,
{ paddingBottom: isKeyboardActive ? 0 : bottomPadding },
]}
>
<View style={styles.inputPageContent}>
{isContactInputMode ? (
filteredContacts.length > 0 ? (
<FlatList
data={filteredContacts}
renderItem={renderFilteredContact}
keyExtractor={filteredContactKeyExtractor}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
/>
) : contactSearchUsername ? (
<View style={styles.noContactContainer}>
<ThemeIcon iconName={'SearchX'} />
<ThemeText
styles={styles.emptyTitle}
content={t('wallet.halfModal.noContactHead', {
username: contactSearchUsername,
})}
/>
<ThemeText
styles={styles.emptySubtext}
content={t('wallet.halfModal.noContactDesc', {
username: contactSearchUsername,
})}
/>
<CustomButton
buttonStyles={{ ...CENTER, marginTop: 'auto' }}
textContent={`${t(
'constants.pay',
)} ${contactSearchUsername}`}
actionFunction={handleManualInputSubmit}
/>
</View>
) : null
) : (
<TouchableWithoutFeedback
onPress={KeyboardController.dismiss}
>
<View style={styles.inputActionContainer}>
{!!inputError && (
<View style={styles.inputErrorContainer}>
<ThemeIcon size={20} iconName={'TriangleAlert'} />
<ThemeText
styles={styles.inputError}
content={inputError}
/>
</View>
)}
<CustomButton
buttonStyles={styles.inputModeButton}
textContent={
inputText?.trim()
? t('constants.continue')
: t('constants.back')
}
actionFunction={
inputText?.trim()
? handleManualInputSubmit
: blurKeyboard
}
/>
</View>
</TouchableWithoutFeedback>
)}
</View>
)}
</Animated.View>
</ScrollView>
{isInputMode && (
<Animated.View
style={[
styles.continueButtonWrapper,
continueButtonStyle,
{ paddingBottom: isKeyboardActive ? 0 : bottomPadding },
]}
pointerEvents={isInputMode ? 'auto' : 'none'}
>
{!!inputError && (
<View style={styles.inputErrorContainer}>
<ThemeIcon size={20} iconName={'TriangleAlert'} />
<ThemeText styles={styles.inputError} content={inputError} />
</View>
)}
<CustomButton
buttonStyles={styles.inputModeButton}
textContent={
inputText?.trim()
? t('constants.continue')
: t('constants.back')
}
actionFunction={
inputText?.trim() ? handleManualInputSubmit : blurKeyboard
}
/>
</Animated.View>
)}
</Animated.View>
)}
</View>
</Animated.View>
<AddContactOverlay
@@ -1036,6 +1175,26 @@ const styles = StyleSheet.create({
mainContent: {
flex: 1,
},
pageContainer: {
flex: 1,
},
page: {
...StyleSheet.absoluteFillObject,
},
inputPageContent: {
width: INSET_WINDOW_WIDTH,
alignSelf: 'center',
paddingTop: 12,
flex: 1,
},
noContactContainer: {
flex: 1,
alignItems: 'center',
},
inputActionContainer: {
flex: 1,
justifyContent: 'flex-end',
},
innerContainer: {
width: INSET_WINDOW_WIDTH,
flexGrow: 1,
@@ -1063,10 +1222,11 @@ const styles = StyleSheet.create({
},
searchContainer: {
width: '100%',
width: INSET_WINDOW_WIDTH,
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: 15,
...CENTER,
},
searchInputContainer: {
justifyContent: 'flex-start',
@@ -1122,11 +1282,6 @@ const styles = StyleSheet.create({
includeFontPadding: false,
fontSize: SIZES.smedium,
},
continueButtonWrapper: {
width: INSET_WINDOW_WIDTH,
alignSelf: 'center',
paddingTop: 12,
},
divider: {
width: '100%',
height: 1,
+3 -1
View File
@@ -1000,7 +1000,9 @@
"payMe": "Schnell bezahlen",
"createInvoice": "Rechnung erstellen",
"invoiceDescription": "Fordern Sie eine Zahlung über einen Link oder QR-Code an.",
"poolsDescription": "Sammeln Sie Beiträge von einer Gruppe für ein gemeinsames Ziel."
"poolsDescription": "Sammeln Sie Beiträge von einer Gruppe für ein gemeinsames Ziel.",
"noContactHead": "{{username}} nicht gefunden",
"noContactDesc": "Füge {{username}} zu deinen Kontakten hinzu, um sie hier in Zukunft zu finden, oder fahre fort, um jetzt eine Zahlung zu senden."
},
"payLinks": {
"skip": "Überspringen",
+3 -1
View File
@@ -1000,7 +1000,9 @@
"payMe": "Quick Pay",
"createInvoice": "Create Invoice",
"invoiceDescription": "Request payment using a link or QR code.",
"poolsDescription": "Collect contributions from a group toward a shared goal."
"poolsDescription": "Collect contributions from a group toward a shared goal.",
"noContactHead": "{{username}} not found",
"noContactDesc": "Add {{username}} to your contacts to find them here in the future, or continue to send a payment now."
},
"payLinks": {
"skip": "Skip",
+3 -1
View File
@@ -1000,7 +1000,9 @@
"payMe": "Pago rápido",
"createInvoice": "Crear factura",
"invoiceDescription": "Solicita un pago mediante un enlace o código QR.",
"poolsDescription": "Reúne contribuciones de un grupo para un objetivo compartido."
"poolsDescription": "Reúne contribuciones de un grupo para un objetivo compartido.",
"noContactHead": "{{username}} no encontrado",
"noContactDesc": "Agrega a {{username}} a tus contactos para encontrarlo aquí en el futuro, o continúa para enviar un pago ahora."
},
"payLinks": {
"skip": "Omitir",
+3 -1
View File
@@ -1000,7 +1000,9 @@
"payMe": "Paiement rapide",
"createInvoice": "Créer une facture",
"invoiceDescription": "Demandez un paiement via un lien ou un code QR.",
"poolsDescription": "Collectez des contributions dun groupe pour un objectif commun."
"poolsDescription": "Collectez des contributions dun groupe pour un objectif commun.",
"noContactHead": "{{username}} introuvable",
"noContactDesc": "Ajoutez {{username}} à vos contacts pour le retrouver ici à lavenir, ou continuez pour envoyer un paiement maintenant."
},
"payLinks": {
"skip": "Passer",
+3 -1
View File
@@ -1000,7 +1000,9 @@
"payMe": "Pagamento rapido",
"createInvoice": "Crea fattura",
"invoiceDescription": "Richiedi un pagamento tramite un link o un codice QR.",
"poolsDescription": "Raccogli contributi da un gruppo per un obiettivo condiviso."
"poolsDescription": "Raccogli contributi da un gruppo per un obiettivo condiviso.",
"noContactHead": "{{username}} non trovato",
"noContactDesc": "Aggiungi {{username}} ai tuoi contatti per trovarlo qui in futuro, oppure continua per inviare un pagamento adesso."
},
"payLinks": {
"skip": "Salta",
+3 -1
View File
@@ -1000,7 +1000,9 @@
"payMe": "Endereço Blitz",
"createInvoice": "Criar Fatura",
"invoiceDescription": "Escolha a moeda e quantia desejada",
"poolsDescription": "Colete pagamentos para um objetivo"
"poolsDescription": "Colete pagamentos para um objetivo",
"noContactHead": "{{username}} não encontrado",
"noContactDesc": "Adicione {{username}} aos seus contatos para encontrá-lo aqui no futuro, ou continue para enviar um pagamento agora."
},
"payLinks": {
"skip": "Pular",
+3 -1
View File
@@ -1000,7 +1000,9 @@
"payMe": "Быстрая оплата",
"createInvoice": "Создать счёт",
"invoiceDescription": "Запросите оплату с помощью ссылки или QR-кода.",
"poolsDescription": "Собирайте взносы от группы для общей цели."
"poolsDescription": "Собирайте взносы от группы для общей цели.",
"noContactHead": "{{username}} не найден",
"noContactDesc": "Добавьте {{username}} в свои контакты, чтобы находить здесь в будущем, или продолжите, чтобы отправить платёж сейчас."
},
"payLinks": {
"skip": "Пропустить",
+3 -1
View File
@@ -1000,7 +1000,9 @@
"payMe": "Snabb betalning",
"createInvoice": "Skapa faktura",
"invoiceDescription": "Begär betalning via en länk eller QR-kod.",
"poolsDescription": "Samla in bidrag från en grupp till ett gemensamt mål."
"poolsDescription": "Samla in bidrag från en grupp till ett gemensamt mål.",
"noContactHead": "{{username}} hittades inte",
"noContactDesc": "Lägg till {{username}} i dina kontakter för att hitta dem här i framtiden, eller fortsätt för att skicka en betalning nu."
},
"payLinks": {
"skip": "Hoppa över",