From 5513d42777bfe81cb8d7c39b335182c528bce948 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Tue, 9 Mar 2021 13:37:10 -0600 Subject: [PATCH] Send share invites for Tasks.org & sabre/dav --- .../caldav/CaldavCalendarSettingsActivity.kt | 50 +++++++- .../tasks/caldav/CaldavCalendarViewModel.kt | 36 +++++- .../java/org/tasks/caldav/CaldavClient.kt | 35 ++++++ .../main/java/org/tasks/compose/Constants.kt | 34 ++++++ .../java/org/tasks/compose/ShareInvite.kt | 111 ++++++++++++++++++ .../main/java/org/tasks/data/PrincipalDao.kt | 11 +- .../main/java/org/tasks/themes/CustomIcons.kt | 1 + .../res/drawable/ic_outline_person_add_24.xml | 10 ++ .../activity_caldav_calendar_settings.xml | 76 +++++++----- app/src/main/res/values/strings.xml | 3 + 10 files changed, 332 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/org/tasks/compose/ShareInvite.kt create mode 100644 app/src/main/res/drawable/ic_outline_person_add_24.xml diff --git a/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt index c1bfbcdca..fe121e71f 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt @@ -2,13 +2,21 @@ package org.tasks.caldav import android.os.Bundle import androidx.activity.viewModels +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.painterResource import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.tasks.R import org.tasks.compose.ListSettingsComposables.PrincipalList +import org.tasks.compose.ShareInvite.ShareInviteDialog import org.tasks.data.CaldavAccount import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV @@ -55,6 +63,29 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() { } } } + if (caldavAccount.canShare && (isNew || caldavCalendar?.access == ACCESS_OWNER)) { + findViewById(R.id.fab) + .apply { isVisible = true } + .setContent { + tasksTheme.TasksTheme { + val openDialog = rememberSaveable { mutableStateOf(false) } + ShareInviteDialog(openDialog) { email -> + lifecycleScope.launch { + // TODO: remove delay hack after beta02 release + email?.let { share(it) } ?: delay(100) + openDialog.value = false + } + } + FloatingActionButton(onClick = { openDialog.value = true }) { + Icon( + painter = painterResource(R.drawable.ic_outline_person_add_24), + contentDescription = null, + tint = MaterialTheme.colors.onPrimary, + ) + } + } + } + } } private val canRemovePrincipals: Boolean @@ -81,7 +112,7 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() { } override suspend fun createCalendar(caldavAccount: CaldavAccount, name: String, color: Int) { - viewModel.createCalendar(caldavAccount, name, color, selectedIcon) + caldavCalendar = viewModel.createCalendar(caldavAccount, name, color, selectedIcon) } override suspend fun updateNameAndColor( @@ -100,6 +131,17 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() { viewModel.deleteCalendar(caldavAccount, caldavCalendar) } + private suspend fun share(email: String) { + if (isNew) { + viewModel.ignoreFinish = true + try { + save() + } finally { + viewModel.ignoreFinish = false + } + } + caldavCalendar?.let { viewModel.addUser(caldavAccount, it, email) } + } companion object { val CaldavAccount.canRemovePrincipal: Boolean @@ -107,5 +149,11 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() { SERVER_TASKS, SERVER_OWNCLOUD, SERVER_SABREDAV -> true else -> false } + + val CaldavAccount.canShare: Boolean + get() = when (serverType) { + SERVER_TASKS, SERVER_SABREDAV -> true + else -> false + } } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/CaldavCalendarViewModel.kt b/app/src/main/java/org/tasks/caldav/CaldavCalendarViewModel.kt index b34dae7cf..05807784d 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavCalendarViewModel.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavCalendarViewModel.kt @@ -14,9 +14,13 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.withContext import org.tasks.data.CaldavAccount import org.tasks.data.CaldavCalendar +import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE +import org.tasks.data.CaldavCalendar.Companion.INVITE_UNKNOWN import org.tasks.data.CaldavDao import org.tasks.data.Principal import org.tasks.data.PrincipalDao +import org.tasks.sync.SyncAdapters +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -25,12 +29,19 @@ class CaldavCalendarViewModel @Inject constructor( private val caldavDao: CaldavDao, private val principalDao: PrincipalDao, private val taskDeleter: TaskDeleter, + private val syncAdapters: SyncAdapters, ) : ViewModel() { val error = MutableLiveData() val inFlight = MutableLiveData(false) val finish = MutableLiveData() + var ignoreFinish = false - suspend fun createCalendar(caldavAccount: CaldavAccount, name: String, color: Int, icon: Int) = + suspend fun createCalendar( + caldavAccount: CaldavAccount, + name: String, + color: Int, + icon: Int + ): CaldavCalendar? = doRequest { val url = withContext(Dispatchers.IO) { provider.forAccount(caldavAccount).makeCollection(name, color) @@ -44,7 +55,10 @@ class CaldavCalendarViewModel @Inject constructor( setIcon(icon) caldavDao.insert(this) } - finish.value = Intent().putExtra(MainActivity.OPEN_FILTER, CaldavFilter(calendar)) + if (!ignoreFinish) { + finish.value = Intent().putExtra(MainActivity.OPEN_FILTER, CaldavFilter(calendar)) + } + calendar } suspend fun updateCalendar( @@ -77,6 +91,23 @@ class CaldavCalendarViewModel @Inject constructor( finish.value = Intent(TaskListFragment.ACTION_DELETED) } + suspend fun addUser( + account: CaldavAccount, + list: CaldavCalendar, + email: String + ) = doRequest { + withContext(Dispatchers.IO) { + provider.forAccount(account, list.url!!).share(account, email) + } + principalDao.insert(Principal().apply { + this.list = list.id + principal = "mailto:$email" + inviteStatus = INVITE_UNKNOWN + access = ACCESS_READ_WRITE + }) + syncAdapters.sync(true) + } + suspend fun removeUser(account: CaldavAccount, list: CaldavCalendar, principal: Principal) = doRequest { withContext(Dispatchers.IO) { @@ -94,6 +125,7 @@ class CaldavCalendarViewModel @Inject constructor( try { return@withContext action() } catch (e: Exception) { + Timber.e(e) error.value = e return@withContext null } finally { diff --git a/app/src/main/java/org/tasks/caldav/CaldavClient.kt b/app/src/main/java/org/tasks/caldav/CaldavClient.kt index 499849bd9..6b97bad59 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavClient.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavClient.kt @@ -235,6 +235,41 @@ open class CaldavClient( return this } + suspend fun share( + account: CaldavAccount, + email: String, + displayName: String? = null, + comment: String? = null + ) { + when (account.serverType) { + SERVER_TASKS, SERVER_SABREDAV -> shareSabredav(email) + else -> throw IllegalArgumentException() + } + } + + private suspend fun shareSabredav(email: String, displayName: String? = null, comment: String? = null) = + withContext(Dispatchers.IO) { + DavCollection(httpClient, httpUrl!!) + .post(""" + + + mailto:$email + ${displayName?.let { """ + + $it + + """.trimIndent() }} + ${comment?.let { """ + $it + """.trimIndent() }} + + + + + + """.trimIndent().toRequestBody(MEDIATYPE_SHARING)) {} + } + suspend fun removePrincipal( account: CaldavAccount, calendar: CaldavCalendar, diff --git a/app/src/main/java/org/tasks/compose/Constants.kt b/app/src/main/java/org/tasks/compose/Constants.kt index 1893b2b5a..ddc938a1a 100644 --- a/app/src/main/java/org/tasks/compose/Constants.kt +++ b/app/src/main/java/org/tasks/compose/Constants.kt @@ -1,9 +1,43 @@ package org.tasks.compose +import androidx.annotation.StringRes +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.ContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import java.util.* object Constants { const val ICON_ALPHA = 0.54f val KEYLINE_FIRST = 16.dp val HALF_KEYLINE = 8.dp + + @Composable + fun TextButton(@StringRes text: Int, onClick: () -> Unit) { + androidx.compose.material.TextButton( + onClick = onClick, + colors = textButtonColors() + ) { + Text( + stringResource(text).toUpperCase(Locale.getDefault()), + style = MaterialTheme.typography.button + ) + } + } + + @Composable + fun textButtonColors() = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colors.secondary + ) + + @Composable + fun textFieldColors() = TextFieldDefaults.textFieldColors( + cursorColor = MaterialTheme.colors.secondary, + focusedLabelColor = MaterialTheme.colors.secondary.copy(alpha = ContentAlpha.high), + focusedIndicatorColor = MaterialTheme.colors.secondary.copy(alpha = ContentAlpha.high), + ) } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/compose/ShareInvite.kt b/app/src/main/java/org/tasks/compose/ShareInvite.kt new file mode 100644 index 000000000..6ead4e027 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/ShareInvite.kt @@ -0,0 +1,111 @@ +package org.tasks.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.AlertDialog +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.darkColors +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Email +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import org.tasks.R +import org.tasks.compose.Constants.TextButton +import org.tasks.compose.Constants.textFieldColors +import org.tasks.compose.ShareInvite.ShareInvite + +@Preview(showBackground = true, backgroundColor = 0xFFFFFF) +@Composable +private fun Invite() = MaterialTheme { + ShareInvite(mutableStateOf("")) +} + +@Preview(showBackground = true, backgroundColor = 0x202124) +@Composable +private fun InviteDark() = MaterialTheme(darkColors()) { + ShareInvite(mutableStateOf("")) +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFF) +@Composable +private fun InviteFilled() = MaterialTheme { + ShareInvite(mutableStateOf("user@example.com")) +} + +@Preview(showBackground = true, backgroundColor = 0x202124) +@Composable +private fun InviteDarkFilled() = MaterialTheme(darkColors()) { + ShareInvite(mutableStateOf("user@example.com")) +} + +object ShareInvite { + @Composable + fun ShareInviteDialog( + openDialog: MutableState, + invite: (String?) -> Unit, + ) { + val email = rememberSaveable { mutableStateOf("") } + // TODO: remove after beta02 release: https://issuetracker.google.com/issues/181282423 + val enableHack = rememberSaveable { mutableStateOf(true) } + if (openDialog.value) { + AlertDialog( + onDismissRequest = {}, + text = { ShareInvite(email, enableHack) }, + confirmButton = { + TextButton(text = R.string.invite, onClick = { + enableHack.value = false + invite(email.value) + }) + }, + dismissButton = { + TextButton(text = R.string.cancel, onClick = { + enableHack.value = false + invite(null as String?) + }) + }, + ) + } else { + email.value = "" + enableHack.value = true + } + } + + @Composable + fun ShareInvite(email: MutableState, enableHack: MutableState = mutableStateOf(true)) { + Column(modifier = Modifier.fillMaxWidth()) { + Text( + stringResource(R.string.share_list), + style = MaterialTheme.typography.h6, + ) + Spacer(Modifier.height(Constants.KEYLINE_FIRST)) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = email.value, + label = { Text(stringResource(R.string.email)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + onValueChange = { email.value = it }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Email, + contentDescription = stringResource(id = R.string.email) + ) + }, + enabled = enableHack.value, + textStyle = MaterialTheme.typography.body1, + colors = textFieldColors(), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/data/PrincipalDao.kt b/app/src/main/java/org/tasks/data/PrincipalDao.kt index f7295babc..6d5ce04f0 100644 --- a/app/src/main/java/org/tasks/data/PrincipalDao.kt +++ b/app/src/main/java/org/tasks/data/PrincipalDao.kt @@ -1,12 +1,19 @@ package org.tasks.data import androidx.lifecycle.LiveData -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query @Dao interface PrincipalDao { + @Insert + fun insert(principal: Principal) + @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(principal: List) + fun insert(principals: List) @Query(""" DELETE diff --git a/app/src/main/java/org/tasks/themes/CustomIcons.kt b/app/src/main/java/org/tasks/themes/CustomIcons.kt index 5896ecaa4..b4e4330bd 100644 --- a/app/src/main/java/org/tasks/themes/CustomIcons.kt +++ b/app/src/main/java/org/tasks/themes/CustomIcons.kt @@ -207,6 +207,7 @@ object CustomIcons { 1182 to R.drawable.ic_outline_people_outline_24, 1183 to R.drawable.ic_outline_forum_24, 1184 to R.drawable.ic_twitter_logo_black, + 1185 to R.drawable.ic_outline_person_add_24, ) @JvmStatic diff --git a/app/src/main/res/drawable/ic_outline_person_add_24.xml b/app/src/main/res/drawable/ic_outline_person_add_24.xml new file mode 100644 index 000000000..a2a0572a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_person_add_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_caldav_calendar_settings.xml b/app/src/main/res/layout/activity_caldav_calendar_settings.xml index 05f13b417..75a9ef809 100644 --- a/app/src/main/res/layout/activity_caldav_calendar_settings.xml +++ b/app/src/main/res/layout/activity_caldav_calendar_settings.xml @@ -1,48 +1,64 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/root_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:descendantFocusability="beforeDescendants" + android:focusableInTouchMode="true" + android:orientation="vertical"> - - - + android:layout_height="match_parent"> - + - + - + - + - + - + + + + + + + - + - + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3fa6fe4f..066c855bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -691,6 +691,9 @@ File %1$s contained %2$s.\n\n List members Remove user? %1$s will no longer have access to %2$s + Share list + Email + Invite Invite declined Invite awaiting response Invite invalid