Send share invites for Tasks.org & sabre/dav

pull/1401/head
Alex Baker 5 years ago
parent 5201bca714
commit 5513d42777

@ -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<ComposeView>(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
}
}
}

@ -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<Throwable?>()
val inFlight = MutableLiveData(false)
val finish = MutableLiveData<Intent>()
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 {

@ -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("""
<D:share-resource xmlns:D="$NS_WEBDAV">
<D:sharee>
<D:href>mailto:$email</D:href>
${displayName?.let { """
<D:prop>
<D:displayname>$it</D:displayname>
</D:prop>
""".trimIndent() }}
${comment?.let { """
<D:comment>$it</D:comment>
""".trimIndent() }}
<D:share-access>
<D:read-write />
</D:share-access>
</D:sharee>
</D:share-resource>
""".trimIndent().toRequestBody(MEDIATYPE_SHARING)) {}
}
suspend fun removePrincipal(
account: CaldavAccount,
calendar: CaldavCalendar,

@ -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),
)
}

@ -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<Boolean>,
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<String>, enableHack: MutableState<Boolean> = 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(),
)
}
}
}

@ -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<Principal>)
fun insert(principals: List<Principal>)
@Query("""
DELETE

@ -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

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M15,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM15,6c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2zM15,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4zM9,18c0.22,-0.72 3.31,-2 6,-2 2.7,0 5.8,1.29 6,2L9,18zM6,15v-3h3v-2L6,10L6,7L4,7v3L1,10v2h3v3z"/>
</vector>

@ -1,48 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
android:orientation="vertical">
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">
<include layout="@layout/toolbar"/>
<include layout="@layout/progress_view"/>
<ScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<LinearLayout
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/name_layout"
style="@style/TagSettingsRow"
android:hint="@string/display_name">
<ScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/name"
style="@style/TagSettingsEditText"
android:inputType="textCapSentences|textAutoCorrect" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/name_layout"
style="@style/TagSettingsRow"
android:hint="@string/display_name">
<include layout="@layout/list_settings_color"/>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/name"
style="@style/TagSettingsEditText"
android:inputType="textCapSentences|textAutoCorrect" />
<include layout="@layout/list_settings_icon"/>
</com.google.android.material.textfield.TextInputLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/people"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<include layout="@layout/list_settings_color"/>
<include layout="@layout/list_settings_icon"/>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/people"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
</ScrollView>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/keyline_first"
android:layout_gravity="end|bottom"
android:padding="0dp"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>

@ -691,6 +691,9 @@ File %1$s contained %2$s.\n\n
<string name="list_members">List members</string>
<string name="remove_user">Remove user?</string>
<string name="remove_user_confirmation">%1$s will no longer have access to %2$s</string>
<string name="share_list">Share list</string>
<string name="email">Email</string>
<string name="invite">Invite</string>
<string name="invite_declined">Invite declined</string>
<string name="invite_awaiting_response">Invite awaiting response</string>
<string name="invite_invalid">Invite invalid</string>

Loading…
Cancel
Save