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 @@ -5,10 +5,12 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.ContactsContract
import android.provider.MediaStore
import android.provider.Settings
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.flipcash.app.core.contacts.DeviceContact
import com.flipcash.app.core.util.Linkify
import java.io.File

Expand Down Expand Up @@ -59,6 +61,23 @@ object IntentUtils {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}

fun openContact(contact: DeviceContact): Intent = if (!contact.isUnknown) {
openContact(contact.androidContactId)
} else {
insertOrEditContact(contact.e164)
}

fun openContact(contactId: Long) = Intent(Intent.ACTION_VIEW).apply {
data = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, contactId.toString())
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}

fun insertOrEditContact(phoneNumber: String) = Intent(Intent.ACTION_INSERT_OR_EDIT).apply {
type = ContactsContract.Contacts.CONTENT_ITEM_TYPE
putExtra(ContactsContract.Intents.Insert.PHONE, phoneNumber)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}

fun shareFile(context: Context, file: File, mimeType: String): Intent {
val uri = FileProvider.getUriForFile(
context,
Expand Down
3 changes: 3 additions & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -779,4 +779,7 @@
<string name="title_sendFeatureIntro">Send Money To Your Friends</string>
<string name="subtitle_sendFeatureIntro">Send money to friends as easily as a text. Connect your phone number to get started</string>

<string name="label_fromYourContacts">From Your Contacts</string>
<string name="label_addContact">Add Contact</string>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ internal class ChatViewModel @Inject constructor(
sealed interface Event {
data class OnChatOpened(val identifier: ChatIdentifier) : Event
data class OnContactFound(val contact: DeviceContact): Event
data object RefreshContact : Event
data class ChatFound(val chatId: ChatId) : Event
data object OnSendCash: Event
data object OnStartMessageInput: Event
Expand Down Expand Up @@ -303,6 +304,18 @@ internal class ChatViewModel @Inject constructor(
}
).launchIn(viewModelScope)

// Re-resolve the contact from the device (e.g. after adding via system contacts)
eventFlow
.filterIsInstance<Event.RefreshContact>()
.mapNotNull { stateFlow.value.chattingWith?.e164 }
.onEach { e164 ->
val refreshed = contactCoordinator.refreshContact(e164)
if (refreshed != null) {
dispatchEvent(Event.OnContactFound(refreshed))
}
}
.launchIn(viewModelScope)

// Advance read pointer when user scrolls to messages
eventFlow
.filterIsInstance<Event.AdvanceReadPointer>()
Expand Down Expand Up @@ -641,6 +654,7 @@ internal class ChatViewModel @Inject constructor(
chattingWith = event.contact
)
}
is Event.RefreshContact -> { state -> state }
is Event.ChatFound -> { state -> state.copy(chatId = event.chatId) }
Event.OnSendCash -> { state -> state }
Event.OnStartMessageInput -> { state -> state.copy(userState = UserState.Typing) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ internal fun MessengerScreen(viewModel: ChatViewModel) {
onAdvanceReadPointer = { messageId ->
viewModel.dispatchEvent(ChatViewModel.Event.AdvanceReadPointer(messageId))
},
onRefreshContact = {
viewModel.dispatchEvent(ChatViewModel.Event.RefreshContact)
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand All @@ -15,14 +16,19 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.flipcash.app.contacts.ui.ContactAvatar
import com.flipcash.app.core.android.IntentUtils
import com.flipcash.app.core.contacts.DeviceContact
import com.flipcash.features.messenger.R
import com.getcode.theme.CodeTheme
Expand All @@ -31,6 +37,7 @@ import com.getcode.theme.CodeTheme
internal fun ContactInfoContainer(
contact: DeviceContact?,
modifier: Modifier = Modifier,
onRefreshContact: () -> Unit = {},
) {
Column(
modifier = modifier
Expand Down Expand Up @@ -61,44 +68,67 @@ internal fun ContactInfoContainer(
color = CodeTheme.colors.textMain,
)

if (contact?.isUnknown == false) {
Text(
modifier = Modifier.padding(top = CodeTheme.dimens.grid.x1),
text = contact.displayNumber,
style = CodeTheme.typography.textSmall,
color = CodeTheme.colors.textSecondary,
)
}

val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { onRefreshContact() }

ContactPill(
modifier = Modifier.padding(top = CodeTheme.dimens.inset),
contact = contact
)
) {
contact ?: return@ContactPill
val intent = IntentUtils.openContact(contact).apply {
// Remove NEW_TASK so the result callback fires when the user returns,
// not immediately.
flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
}
launcher.launch(intent)
}
}
}

@Composable
private fun ContactPill(
contact: DeviceContact?,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
AnimatedContent(contact) { c ->
if (c == null) {
Spacer(modifier = modifier.fillMaxWidth())
return@AnimatedContent
}

val isContact = !c.isUnknown
val backgroundColor by animateColorAsState(
if (isContact) CodeTheme.colors.surfaceVariant else CodeTheme.colors.warning.copy(alpha = 0.10f)
if (!c.isUnknown) CodeTheme.colors.surfaceVariant else CodeTheme.colors.warning.copy(alpha = 0.10f)
)

val contentColor by animateColorAsState(
if (isContact) CodeTheme.colors.textSecondary else CodeTheme.colors.warning
if (!c.isUnknown) CodeTheme.colors.textSecondary else CodeTheme.colors.warning
)

Row(
modifier = modifier
.background(color = backgroundColor, shape = CircleShape)
.clip(CircleShape)
.clickable { onClick() }
.padding(horizontal = CodeTheme.dimens.grid.x2, vertical = CodeTheme.dimens.grid.x1),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1)
) {
Icon(
modifier = Modifier.size(CodeTheme.dimens.staticGrid.x4),
painter = painterResource(
if (isContact) {
if (!c.isUnknown) {
R.drawable.ic_existing_contact
} else {
R.drawable.ic_unknown_contact
Expand All @@ -109,7 +139,7 @@ private fun ContactPill(
)

Text(
text = if (isContact) "From Your Contacts" else "Unknown Contact",
text = if (!c.isUnknown) stringResource(R.string.label_fromYourContacts) else stringResource(R.string.label_addContact),
color = contentColor,
style = CodeTheme.typography.textSmall,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ internal fun MessageList(
separatorConfig: SeparatorConfig,
otherReadPointer: MessagePointer? = null,
onAdvanceReadPointer: ((Long) -> Unit)? = null,
onRefreshContact: () -> Unit = {},
) {
val keyboard = rememberKeyboardController()
val listState = rememberLazyListState()
Expand Down Expand Up @@ -208,7 +209,8 @@ internal fun MessageList(
ContactInfoContainer(
contact = state.chattingWith,
modifier = Modifier
.padding(horizontal = CodeTheme.dimens.grid.x12)
.padding(horizontal = CodeTheme.dimens.grid.x12),
onRefreshContact = onRefreshContact,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.flipcash.app.contacts.device.DeviceContactLookup
import com.flipcash.app.contacts.device.PickedContactData
import com.flipcash.app.contacts.device.ScopeAwareContactReader
import com.flipcash.app.featureflags.FeatureFlag
Expand Down Expand Up @@ -70,6 +71,7 @@ class ContactCoordinator @Inject constructor(
private val contactVerificationController: ContactVerificationController,
private val resolverController: ResolverController,
private val networkObserver: NetworkConnectivityListener,
private val deviceContactLookup: DeviceContactLookup,
private val contactReader: ScopeAwareContactReader,
private val phoneUtils: PhoneUtils,
private val contactDataSource: ContactDataSource,
Expand Down Expand Up @@ -197,6 +199,13 @@ class ContactCoordinator @Inject constructor(
return resolverController.resolve(ContactMethod.Phone(e164))
}

fun refreshContact(e164: String): DeviceContact? {
val refreshed = deviceContactLookup.lookupContact(e164) ?: return null
val enriched = refreshed.copy(displayNumber = phoneUtils.formatNumber(e164))
_state.update { it.copy(contacts = it.contacts + (e164 to enriched)) }
return enriched
}

fun lookupContact(e164: String): Result<DeviceContact> {
val contact = _state.value.contacts[e164]
?: return Result.failure(NoSuchElementException("No contact found for $e164"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.flipcash.app.contacts.device

import com.flipcash.app.core.contacts.DeviceContact

interface DeviceContactLookup {
fun lookupDisplayName(e164: String): String?
fun lookupPhotoUri(e164: String): String?
fun lookupPhotoBytes(e164: String): ByteArray?
fun lookupProfilePhoto(): ByteArray?
fun lookupContact(e164: String): DeviceContact?
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.content.pm.PackageManager
import android.provider.ContactsContract
import androidx.core.content.ContextCompat
import com.flipcash.app.contacts.device.DeviceContactLookup
import com.flipcash.app.core.contacts.DeviceContact
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
Expand Down Expand Up @@ -50,6 +51,40 @@ internal class AndroidDeviceContactLookup @Inject constructor(
}
}

override fun lookupContact(e164: String): DeviceContact? {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED
) return null

val uri = ContactsContract.PhoneLookup.CONTENT_FILTER_URI
.buildUpon()
.appendPath(e164)
.build()

return try {
context.contentResolver.query(
uri,
arrayOf(
ContactsContract.PhoneLookup._ID,
ContactsContract.PhoneLookup.DISPLAY_NAME,
ContactsContract.PhoneLookup.PHOTO_URI,
),
null, null, null
)?.use { cursor ->
if (cursor.moveToFirst()) {
DeviceContact(
e164 = e164,
androidContactId = cursor.getLong(0),
displayName = cursor.getString(1) ?: e164,
photoUri = cursor.getString(2),
)
} else null
}
} catch (e: Exception) {
null
}
}

private fun lookupContactId(e164: String): Long? =
lookupField(e164, ContactsContract.PhoneLookup._ID)?.toLongOrNull()

Expand Down
Loading